12 无监督学习

无监督学习概述

什么是无监督学习?

在前面的章节中,我们研究的都是监督学习——为每个观测 \(i\) 拥有特征向量 \(x_i\) 和响应变量 \(y_i\)

无监督学习面对的是完全不同的局面:

  • 只有特征 \(X_1, X_2, \ldots, X_p\)没有响应变量 \(Y\)
  • 目标不是预测,而是发现数据中隐藏的结构和模式
  • 两大核心工具:主成分分析 (PCA)聚类分析 (Clustering)

无监督学习的独特挑战

与监督学习相比,无监督学习面临更深层次的困难:

维度 监督学习 无监督学习
目标 明确(最小化预测误差) 模糊(探索结构)
评估 交叉验证、MSE、AUC 没有统一标准
主观性 较低 较高(结果解读因人而异)
验证 测试集验证 需要领域知识判断

主成分分析 (PCA)

PCA 的核心思想:寻找”最佳拍摄角度”

PCA = 寻找数据的"最佳拍摄角度" PC1 PC2 好角度 (PC1) 数据展开,差异清晰 差角度 (PC2) 数据挤压,信息丢失 PCA 寻找使投影后数据方差最大的方向 → 保留最多信息
Figure 1: PCA 相机类比:从”最佳角度”观察数据,保留最多信息

PCA 的数学定义

给定 \(n\) 个观测、\(p\) 个特征的数据矩阵 \(\mathbf{X}\)(已中心化),第一主成分定义为:

\[Z_1 = \phi_{11}X_1 + \phi_{21}X_2 + \cdots + \phi_{p1}X_p\]

其中载荷向量 \(\boldsymbol{\phi}_1 = (\phi_{11}, \phi_{21}, \ldots, \phi_{p1})^T\) 满足:

\[\max_{\boldsymbol{\phi}_1} \text{Var}(Z_1) \quad \text{s.t.} \quad \|\boldsymbol{\phi}_1\|^2 = 1\]

  • 目标:使投影后的方差最大化
  • 约束:载荷向量为单位向量(防止无穷大解)
  • \(\boldsymbol{\phi}_1\) 是协方差矩阵 \(\mathbf{S}\)最大特征值对应的特征向量

SVD 视角:PCA 的高效计算

PCA 的计算可以通过奇异值分解 (SVD) 高效实现。

对中心化数据矩阵 \(\mathbf{X}\) 进行 SVD:

\[\mathbf{X} = \mathbf{U} \boldsymbol{\Sigma} \mathbf{V}^T\]

  • \(\mathbf{V}\) 的列 = 主成分载荷向量(即特征向量)
  • \(\boldsymbol{\Sigma}\) 的对角元素 \(\sigma_i\) 与特征值的关系:\(\lambda_i = \sigma_i^2 / n\)
  • 方差解释比\(\frac{\lambda_k}{\sum_{j=1}^p \lambda_j} = \frac{\sigma_k^2}{\sum_{j=1}^p \sigma_j^2}\)

中国案例:长三角上市公司财务 PCA

数据准备:长三角上市公司财务指标

import pandas as pd  # 导入pandas库用于数据框操作
import numpy as np  # 导入numpy库用于数值计算
import os  # 导入os模块用于跨平台路径处理

# 根据操作系统选择数据目录
DATA_DIR = 'C:/qiufei/data' if os.name == 'nt' else '/home/ubuntu/r2_data_mount/qiufei/data'  # 跨平台数据路径
path_financial = os.path.join(DATA_DIR, 'stock/financial_statement.h5')  # 财务报表数据路径
path_basic = os.path.join(DATA_DIR, 'stock/stock_basic_data.h5')  # 上市公司基本信息路径

financial_statement_raw = pd.read_hdf(path_financial)  # 读取财务报表数据
stock_basic_data = pd.read_hdf(path_basic)  # 读取上市公司基本信息

# 定义长三角省份列表(上海、江苏、浙江、安徽)
yangtze_river_delta_provinces = ['上海市', '江苏省', '浙江省', '安徽省']  # 长三角四省市(与数据中省份全称匹配)
# 筛选长三角地区上市公司
yrd_stock_codes = stock_basic_data[
    stock_basic_data['province'].isin(yangtze_river_delta_provinces)  # 按省份筛选
]['order_book_id'].tolist()  # 提取股票代码列表

构建五大财务指标

# 筛选长三角公司、最近期的财务数据
yrd_financial_data = financial_statement_raw[
    financial_statement_raw['order_book_id'].isin(yrd_stock_codes)  # 筛选长三角公司
].copy()  # 创建副本防止修改原数据

# 只保留2023年之后的财务数据
yrd_financial_data = yrd_financial_data[
    yrd_financial_data['quarter'] >= '2023q1'  # 筛选最近期数据
]  # 确保数据时效性

# 计算五个核心财务指标
yrd_financial_data['Log_Assets'] = np.log(yrd_financial_data['total_assets'].clip(lower=1))  # 对数总资产(取对数消除量纲差异)
yrd_financial_data['Debt_Ratio'] = (  # 资产负债率
    yrd_financial_data['current_liabilities'].fillna(0) + yrd_financial_data['non_current_liabilities'].fillna(0)
) / yrd_financial_data['total_assets']  # 负债总额除以总资产
yrd_financial_data['Net_Margin'] = (  # 净利润率
    yrd_financial_data['net_profit'] / yrd_financial_data['revenue'].replace(0, np.nan)  # 净利润除以营业收入
)  # 衡量盈利能力
yrd_financial_data['Asset_Turnover'] = (  # 总资产周转率
    yrd_financial_data['revenue'] / yrd_financial_data['total_assets']  # 营业收入除以总资产
)  # 衡量资产使用效率
yrd_financial_data['ROA'] = (  # 总资产收益率
    yrd_financial_data['net_profit'] / yrd_financial_data['total_assets']  # 净利润除以总资产
)  # 衡量资产盈利能力

# 每家公司取最近一期财报
pca_feature_columns = ['Log_Assets', 'Debt_Ratio', 'Net_Margin', 'Asset_Turnover', 'ROA']  # PCA分析的五个特征
yrd_latest_financial = yrd_financial_data.sort_values('quarter').groupby('order_book_id').last().reset_index()  # 取最新一期
yrd_pca_data = yrd_latest_financial[['order_book_id'] + pca_feature_columns].dropna()  # 提取特征列并去除缺失值

数据标准化与 Winsorization

from sklearn.preprocessing import StandardScaler  # 导入标准化工具

# 1% 和 99% 分位数 Winsorization(缩尾处理)
for feature_col in pca_feature_columns:  # 遍历每个特征列
    lower_bound = yrd_pca_data[feature_col].quantile(0.01)  # 计算1%分位数作为下界
    upper_bound = yrd_pca_data[feature_col].quantile(0.99)  # 计算99%分位数作为上界
    yrd_pca_data[feature_col] = yrd_pca_data[feature_col].clip(lower_bound, upper_bound)  # 将极端值截断到上下界

# 标准化(均值为0,标准差为1)
feature_scaler = StandardScaler()  # 初始化标准化器
standardized_features = feature_scaler.fit_transform(yrd_pca_data[pca_feature_columns])  # 拟合并转换数据
print(f'标准化后的数据形状:{standardized_features.shape[0]} 家公司 × {standardized_features.shape[1]} 个特征')  # 输出数据维度
标准化后的数据形状:2015 家公司 × 5 个特征

为什么要标准化?

  • 不同指标量纲差异巨大(总资产数十亿,ROA不到1%)
  • 不标准化时,PCA 会被量纲最大的变量主导
  • 标准化确保每个变量在 PCA 中获得公平权重

PCA 拟合与碎石图

Code
from sklearn.decomposition import PCA  # 导入PCA模型
import matplotlib.pyplot as plt  # 导入绑图库

plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']  # 设置中文字体
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题

pca_model = PCA()  # 初始化PCA模型(保留所有主成分)
pca_scores = pca_model.fit_transform(standardized_features)  # 拟合PCA并获取主成分得分
explained_variance_ratio = pca_model.explained_variance_ratio_  # 提取各主成分的方差解释比
cumulative_variance_ratio = np.cumsum(explained_variance_ratio)  # 计算累积方差解释比

fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))  # 创建1行2列子图

# 左图:碎石图
axes[0].bar(range(1, len(explained_variance_ratio) + 1), explained_variance_ratio, color='#008080', alpha=0.8)  # 柱状图
axes[0].set_xlabel('主成分编号')  # 设置x轴标签
axes[0].set_ylabel('方差解释比')  # 设置y轴标签
axes[0].set_title('碎石图 (Scree Plot)')  # 设置标题

# 右图:累积方差解释比
axes[1].plot(range(1, len(cumulative_variance_ratio) + 1), cumulative_variance_ratio, 'o-', color='#E3120B')  # 折线图
axes[1].axhline(y=0.80, color='gray', linestyle='--', alpha=0.5, label='80% 阈值')  # 80%阈值线
axes[1].set_xlabel('主成分个数')  # 设置x轴标签
axes[1].set_ylabel('累积方差解释比')  # 设置y轴标签
axes[1].set_title('累积方差解释比')  # 设置标题
axes[1].legend()  # 显示图例
plt.tight_layout()  # 自动调整间距
plt.show()  # 显示图表

print(f'PC1 方差解释比: {explained_variance_ratio[0]:.2%}')  # 输出PC1方差解释比
print(f'PC1+PC2 累积: {cumulative_variance_ratio[1]:.2%}')  # 输出前两个PC累积方差
print(f'PC1+PC2+PC3 累积: {cumulative_variance_ratio[2]:.2%}')  # 输出前三个PC累积方差
Figure 2: 碎石图与累积方差解释比(长三角上市公司财务数据PCA)
PC1 方差解释比: 39.90%
PC1+PC2 累积: 67.09%
PC1+PC2+PC3 累积: 86.72%

载荷图 (Biplot):解读主成分含义

Code
fig, ax = plt.subplots(figsize=(8, 6))  # 创建单张图表

# 提取前两个主成分的载荷
loadings_pc1 = pca_model.components_[0]  # PC1的载荷向量
loadings_pc2 = pca_model.components_[1]  # PC2的载荷向量

# 绘制载荷箭头
for i, feature_name in enumerate(pca_feature_columns):  # 遍历每个特征
    ax.annotate('', xy=(loadings_pc1[i], loadings_pc2[i]), xytext=(0, 0),  # 从原点到载荷点画箭头
                arrowprops=dict(arrowstyle='->', color='#E3120B', lw=2))  # 红色箭头
    ax.text(loadings_pc1[i] * 1.15, loadings_pc2[i] * 1.15, feature_name,  # 在箭头端点标注特征名
            fontsize=11, ha='center', color='#2C3E50', fontweight='bold')  # 加粗标注

ax.axhline(y=0, color='gray', linestyle='--', alpha=0.3)  # 水平参考线
ax.axvline(x=0, color='gray', linestyle='--', alpha=0.3)  # 垂直参考线
ax.set_xlabel(f'PC1 ({explained_variance_ratio[0]:.1%})')  # x轴标签(含方差解释比)
ax.set_ylabel(f'PC2 ({explained_variance_ratio[1]:.1%})')  # y轴标签(含方差解释比)
ax.set_title('PCA 载荷图')  # 设置标题
plt.tight_layout()  # 自动调整间距
plt.show()  # 显示图表
Figure 3: PCA 载荷图:各财务指标在前两个主成分上的载荷方向

载荷解读

  • PC1(盈利质量维度):Net_Margin 和 ROA 载荷较高 → 反映企业盈利能力
  • PC2(规模与杠杆维度):Log_Assets 和 Debt_Ratio 载荷较高 → 反映企业规模和负债水平

主成分得分按行业分布

Code
# 合并行业信息
yrd_pca_data_with_industry = yrd_pca_data.merge(  # 合并行业分类
    stock_basic_data[['order_book_id', 'industry_name']], on='order_book_id', how='left'  # 左连接
)  # 获取每家公司的行业归属

# 统计各行业频次,选择前6大行业
top_six_industries = yrd_pca_data_with_industry['industry_name'].value_counts().head(6).index.tolist()  # 前6大行业名称
plot_mask = yrd_pca_data_with_industry['industry_name'].isin(top_six_industries)  # 筛选属于前6行业的公司

fig, ax = plt.subplots(figsize=(10, 6))  # 创建图表
industry_color_palette = ['#E3120B', '#008080', '#F0A700', '#2C3E50', '#8E9EAA', '#6A5ACD']  # 6种行业颜色

for i, industry_name in enumerate(top_six_industries):  # 遍历前6大行业
    industry_mask = yrd_pca_data_with_industry['industry_name'] == industry_name  # 筛选当前行业
    valid_indices = yrd_pca_data_with_industry[industry_mask].index  # 获取行业内公司的索引
    # 获取这些公司在PCA得分矩阵中的位置
    pca_row_indices = [yrd_pca_data.index.get_loc(idx) for idx in valid_indices if idx in yrd_pca_data.index]  # 映射索引
    ax.scatter(pca_scores[pca_row_indices, 0], pca_scores[pca_row_indices, 1],  # 绘制散点
               c=industry_color_palette[i], label=industry_name, alpha=0.6, s=30)  # 按行业着色

ax.set_xlabel(f'PC1 ({explained_variance_ratio[0]:.1%})')  # x轴标签
ax.set_ylabel(f'PC2 ({explained_variance_ratio[1]:.1%})')  # y轴标签
ax.set_title('主成分得分的行业分布')  # 设置标题
ax.legend(fontsize=9, ncol=2)  # 显示两列图例
plt.tight_layout()  # 自动调整间距
plt.show()  # 显示图表
Figure 4: 前两个主成分得分的行业分布(长三角上市公司)

PCA 实践要点

如何选择主成分个数?

方法 规则 适用场景
碎石图 (Scree Plot) 寻找”肘部”(方差急剧下降处) 直观判断
累积方差比 选择累积达到80%~90%的点 通用标准
Kaiser 准则 保留特征值 > 1 的成分 标准化数据

PCA 的局限性

  • 线性假设:只能捕获线性关系,非线性结构需要 t-SNE/UMAP
  • 解释困难:主成分是原始变量的线性组合,业务含义不直接
  • 方差 ≠ 信息:最大方差方向未必是最有业务价值的方向

聚类分析

从 PCA 到聚类:EM 算法的视角

EM 算法 是理解聚类(以及许多其他统计方法)的统一框架:

  • E 步(期望步):给定当前参数估计,计算每个观测属于各类别的后验概率

\[\gamma_{ik} = \frac{\pi_k \mathcal{N}(x_i | \mu_k, \Sigma_k)}{\sum_{j=1}^K \pi_j \mathcal{N}(x_i | \mu_j, \Sigma_j)}\]

  • M 步(最大化步):利用后验概率更新参数
    • 更新混合比例:\(\hat{\pi}_k = \frac{1}{n}\sum_{i=1}^n \gamma_{ik}\)
    • 更新均值:\(\hat{\mu}_k = \frac{\sum_i \gamma_{ik} x_i}{\sum_i \gamma_{ik}}\)
    • 更新协方差:\(\hat{\Sigma}_k = \frac{\sum_i \gamma_{ik}(x_i - \hat{\mu}_k)(x_i - \hat{\mu}_k)^T}{\sum_i \gamma_{ik}}\)

K-Means 是 EM 的特例

K-Means 可以被视为高斯混合模型 EM 算法的硬分配版本:

特性 高斯混合模型 (GMM-EM) K-Means
分配方式 软分配(概率 \(\gamma_{ik} \in [0,1]\) 硬分配(\(\gamma_{ik} \in \{0,1\}\)
均值更新 加权平均 算术平均
簇形状 椭圆形(不同协方差) 球形(等协方差)
目标函数 对数似然 组内平方和 (WCSS)

K-Means 的目标函数:

\[\min_{C_1,\ldots,C_K} \sum_{k=1}^K \sum_{i \in C_k} \| x_i - \mu_k \|^2\]

K-Means 实战:最优 K 值选择

Code
from sklearn.cluster import KMeans  # 导入K-Means聚类模型
from sklearn.metrics import silhouette_score  # 导入轮廓系数

# 使用前3个主成分作为聚类特征
pca_features_for_clustering = pca_scores[:, :3]  # 提取前3个PC得分

k_range = range(2, 10)  # 候选K值范围:2到9
inertia_values = []  # 存储各K值的组内平方和 (SSE/Inertia)
silhouette_values = []  # 存储各K值的轮廓系数

for k in k_range:  # 遍历每个候选K值
    kmeans_model = KMeans(n_clusters=k, random_state=42, n_init=10)  # 初始化K-Means模型
    cluster_labels = kmeans_model.fit_predict(pca_features_for_clustering)  # 拟合并预测聚类标签
    inertia_values.append(kmeans_model.inertia_)  # 记录组内平方和
    silhouette_values.append(silhouette_score(pca_features_for_clustering, cluster_labels))  # 记录轮廓系数

fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))  # 创建1行2列子图

axes[0].plot(list(k_range), inertia_values, 'o-', color='#E3120B', linewidth=2)  # 肘部法折线图
axes[0].set_xlabel('聚类数 K')  # x轴标签
axes[0].set_ylabel('组内平方和 (SSE)')  # y轴标签
axes[0].set_title('肘部法 (Elbow Method)')  # 标题

axes[1].plot(list(k_range), silhouette_values, 's-', color='#008080', linewidth=2)  # 轮廓系数折线图
axes[1].set_xlabel('聚类数 K')  # x轴标签
axes[1].set_ylabel('轮廓系数')  # y轴标签
axes[1].set_title('轮廓系数法 (Silhouette Method)')  # 标题

plt.tight_layout()  # 自动调整间距
plt.show()  # 显示图表
Figure 5: 肘部法与轮廓系数法确定最优聚类数 K

聚类结果解读:K=4 的业务含义

# 以K=4执行最终聚类
optimal_kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)  # K=4的KMeans模型
final_cluster_labels = optimal_kmeans.fit_predict(pca_features_for_clustering)  # 获取聚类标签
yrd_pca_data_copy = yrd_pca_data.copy()  # 创建副本
yrd_pca_data_copy['Cluster'] = final_cluster_labels  # 将聚类标签添加到数据框

# 计算各聚类在原始特征上的均值画像
cluster_profile = yrd_pca_data_copy.groupby('Cluster')[pca_feature_columns].mean().round(4)  # 各聚类的特征均值
cluster_profile.index = [f'聚类 {i}' for i in cluster_profile.index]  # 重命名索引
cluster_profile  # 显示聚类画像表
Table 1: 各聚类的财务特征画像(K=4)
Log_Assets Debt_Ratio Net_Margin Asset_Turnover ROA
聚类 0 22.4302 0.5406 0.0430 0.6622 0.0259
聚类 1 21.6651 0.2440 0.1173 0.3195 0.0358
聚类 2 21.7121 0.5191 -0.3052 0.1977 -0.0396
聚类 3 24.2914 0.4896 0.1269 0.2621 0.0226

聚类在 PCA 空间中的可视化

Code
fig, ax = plt.subplots(figsize=(9, 6))  # 创建图表
cluster_colors = ['#E3120B', '#008080', '#F0A700', '#2C3E50']  # 4个聚类的颜色

for cluster_id in range(4):  # 遍历每个聚类
    cluster_mask = final_cluster_labels == cluster_id  # 筛选当前聚类的样本
    ax.scatter(pca_scores[cluster_mask, 0], pca_scores[cluster_mask, 1],  # 在PC1-PC2空间绑制散点
               c=cluster_colors[cluster_id], label=f'聚类 {cluster_id}', alpha=0.6, s=30)  # 按聚类着色

ax.set_xlabel(f'PC1 ({explained_variance_ratio[0]:.1%})')  # x轴标签
ax.set_ylabel(f'PC2 ({explained_variance_ratio[1]:.1%})')  # y轴标签
ax.set_title('K-Means 聚类结果(PCA空间投影)')  # 标题
ax.legend()  # 显示图例
plt.tight_layout()  # 自动调整间距
plt.show()  # 显示图表
Figure 6: 长三角上市公司 K-Means 聚类结果(PCA空间投影)

层次聚类

层次聚类与树状图

Code
from scipy.cluster.hierarchy import dendrogram, linkage  # 导入层次聚类工具

np.random.seed(42)  # 设置随机种子
sample_size = min(50, len(standardized_features))  # 取样本量(最多50家)
sample_indices = np.random.choice(len(standardized_features), sample_size, replace=False)  # 随机抽样索引
sample_features = standardized_features[sample_indices]  # 提取样本特征

# 合并行业名称用于标签
sample_stock_codes = yrd_pca_data.iloc[sample_indices]['order_book_id'].values  # 获取样本公司代码
sample_industry_names = stock_basic_data.set_index('order_book_id').loc[sample_stock_codes, 'industry_name'].values  # 获取行业名

# Ward 法层次聚类
linkage_matrix = linkage(sample_features, method='ward')  # 使用Ward方法计算链接矩阵

fig, ax = plt.subplots(figsize=(14, 6))  # 创建图表
dendrogram(linkage_matrix, labels=sample_industry_names, leaf_rotation=90, leaf_font_size=8, ax=ax)  # 绘制树状图
ax.set_title('层次聚类树状图(Ward法)')  # 标题
ax.set_ylabel('距离')  # y轴标签
plt.tight_layout()  # 自动调整间距
plt.show()  # 显示图表
Figure 7: 长三角上市公司层次聚类树状图(Ward 法,随机50家企业)

层次聚类的”家谱树”隐喻

  • 树的底部:每家公司是一个叶子节点
  • 向上合并:最相似的公司先合并,形成小组
  • 树的高度:反映组间差异程度——高度越大,差异越显著

聚类方法对比与选择

特性 K-Means 层次聚类 DBSCAN
簇形状 球形 任意 任意
需指定 K 否(后期切割)
大规模数据 适合 不适合 适合
噪声处理 能识别噪声点
可解释性 中等 高(树状图) 中等
from sklearn.metrics import calinski_harabasz_score  # 导入CH指数(方差比准则)

# 使用 Calinski-Harabasz 指数评估 K=4 的聚类质量
ch_index_value = calinski_harabasz_score(pca_features_for_clustering, final_cluster_labels)  # 计算CH指数
print(f'K=4 的 Calinski-Harabasz 指数: {ch_index_value:.2f}')  # 输出CH指数(越大越好)
print('(CH指数越大,表示聚类越紧凑且分离度越高)')  # 解读说明
K=4 的 Calinski-Harabasz 指数: 743.88
(CH指数越大,表示聚类越紧凑且分离度越高)

股票聚类案例

基于收益率相关性的股票聚类

# 选取5大行业各6只代表性股票
industry_stock_mapping = {  # 行业-股票映射字典
    '银行': ['601398.XSHG', '601288.XSHG', '600036.XSHG', '601818.XSHG', '600000.XSHG', '601166.XSHG'],  # 工商银行、农业银行、招商银行等
    '电子': ['002415.XSHE', '603986.XSHG', '600703.XSHG', '002049.XSHE', '300750.XSHE', '688008.XSHG'],  # 海康威视等电子企业
    '食品饮料': ['600519.XSHG', '000858.XSHE', '603369.XSHG', '002304.XSHE', '000568.XSHE', '600887.XSHG'],  # 贵州茅台等食品企业
    '非银金融': ['601318.XSHG', '600030.XSHG', '601688.XSHG', '600837.XSHG', '000776.XSHE', '601211.XSHG'],  # 中国平安等金融企业
    '房地产': ['000002.XSHE', '001979.XSHE', '600048.XSHG', '000069.XSHE', '600383.XSHG', '600340.XSHG']  # 万科A等房地产企业
}  # 5行业 × 6只 = 30只股票

all_selected_stock_codes = []  # 存放所有选中股票代码
for stock_list in industry_stock_mapping.values():  # 展开所有行业的股票
    all_selected_stock_codes.extend(stock_list)  # 追加到总列表

# 读取后复权股价数据
path_price = os.path.join(DATA_DIR, 'stock/stock_price_post_adjusted.h5')  # 股价文件路径
stock_price_data = pd.read_hdf(path_price).reset_index()  # 读取并重置索引

计算收益率相关距离矩阵

# 筛选选中股票的2023年以来数据
selected_stock_data = stock_price_data[
    (stock_price_data['order_book_id'].isin(all_selected_stock_codes)) &  # 筛选目标股票
    (stock_price_data['date'] >= '2023-01-01')  # 筛选2023年后数据
].copy()  # 创建副本

# 计算日收益率
selected_stock_data['daily_return'] = selected_stock_data.groupby('order_book_id')['close'].pct_change()  # 按股票分组计算收益率

# 构建收益率宽表(行=日期,列=股票代码)
daily_returns_wide = selected_stock_data.pivot_table(  # 透视为宽表
    index='date', columns='order_book_id', values='daily_return'  # 行=日期,列=股票
).dropna(axis=1, thresh=100).dropna()  # 去除数据不足的股票和缺失行

# 构建相关性距离矩阵: distance = 1 - correlation
correlation_matrix = daily_returns_wide.corr()  # 计算股票间的Pearson相关系数矩阵
distance_matrix = 1 - correlation_matrix  # 将相关性转换为距离(相关性越高,距离越近)
print(f'有效股票数: {distance_matrix.shape[0]}')  # 输出矩阵维度
有效股票数: 30

股票树状图:行业结构自动浮现

Code
from scipy.spatial.distance import squareform  # 导入距离矩阵压缩工具

# 将方阵转换为压缩距离向量
condensed_distance = squareform(distance_matrix.values, checks=False)  # 压缩距离矩阵
stock_linkage = linkage(condensed_distance, method='ward')  # Ward法层次聚类

# 构建行业标签
industry_labels = []  # 存放行业简称标签
for stock_code in distance_matrix.columns:  # 遍历每只股票
    label_found = False  # 标记是否找到行业
    for ind_name, ind_stocks in industry_stock_mapping.items():  # 遍历行业映射
        if stock_code in ind_stocks:  # 匹配股票代码
            industry_labels.append(f'{ind_name[:2]}')  # 取行业名前两个字作为简称
            label_found = True  # 标记已找到
            break  # 跳出内层循环
    if not label_found:  # 未匹配时
        industry_labels.append(stock_code[:6])  # 使用股票代码前6位作为标签

fig, ax = plt.subplots(figsize=(14, 6))  # 创建图表
dendrogram(stock_linkage, labels=industry_labels, leaf_rotation=0, leaf_font_size=10, ax=ax)  # 绘制树状图
ax.set_title('基于收益率相关性的股票聚类树状图')  # 标题
ax.set_ylabel('距离(1 - 相关系数)')  # y轴标签
plt.tight_layout()  # 自动调整间距
plt.show()  # 显示图表
Figure 8: 基于收益率相关性的30只股票层次聚类树状图

相关性热力图:对角线块状结构

Code
import seaborn as sns  # 导入seaborn高级绑图库

# 使用seaborn的clustermap自动排列行列
cluster_heatmap = sns.clustermap(  # 创建聚类热力图
    correlation_matrix,  # 输入相关系数矩阵
    method='ward',  # 使用Ward法聚类
    cmap='coolwarm',  # 冷暖色调(蓝=低相关,红=高相关)
    vmin=-0.5, vmax=1,  # 颜色范围
    figsize=(10, 8),  # 图表大小
    linewidths=0.5  # 网格线宽度
)  # 生成聚类热力图
cluster_heatmap.ax_heatmap.set_title('股票收益率相关性聚类热力图', fontsize=14, pad=60)  # 设置标题
plt.show()  # 显示图表
Figure 9: 股票收益率相关性聚类热力图

对角线上的明显”块状”结构表明:

  • 同行业股票之间高度正相关(红色块)
  • 不同行业之间相关性较弱(蓝色/白色区域)
  • 应用价值:投资组合分散化、配对交易、风险管理

本章小结

无监督学习方法总结

方法 核心思想 主要输出 评估指标
PCA 寻找最大方差方向 主成分得分、载荷 方差解释比
K-Means 最小化组内平方和 聚类标签 轮廓系数、CH指数
层次聚类 逐步合并/分裂 树状图 树状图视觉判断

最佳实践

  • 数据预处理:标准化(几乎总是必要的)、处理异常值
  • 方法选择:小数据用层次聚类(可视化好),大数据用 K-Means(效率高)
  • 结果验证:结合领域知识,检验聚类结果是否有业务意义
  • 迭代优化:尝试不同参数、不同方法,交叉对比结果