21 多维数据可视化-绘制股票相关性热力图
21.1 引言相关性的金融意义
在现代投资组合理论中,资产间的相关性是核心概念。理解相关性对于构建有效投资组合、管理风险和发现套利机会至关重要。本章以股票相关性热力图为主线,完成数据获取、收益率计算、相关性矩阵求解与多维可视化。
21.2 本章学习目标
通过本章学习,你将能够:
- 说明相关系数在投资组合中的角色:分散化、系统性风险、配对交易与因子分析都依赖相关性矩阵
- 用
tushare接口获取多只股票的日线收盘价,并拼接为宽表 DataFrame - 把价格转换为对数收益率,再用
corr()计算相关系数矩阵,并解释”价格相关”与”收益率相关”的口径差异 - 用
sns.heatmap绘制带数值注释的相关性热力图,合理设置cmap、center、vmin/vmax - 用显著性检验、层次聚类热力图、网络图与滚动相关性四种视角审视相关结构的稳定性
先修:章节 5、章节 10、章节 19(第5、10、19章:本章的矩阵运算、宽表组织与热力图绘制分别建立在这三章之上;绘图基础对应第 章节 18、章节 20 章)
21.3 数据获取与预处理
21.3.1 获取股票价格数据
任务要求(归纳自块内任务注释与代码):导入 tushare、pandas、seaborn、matplotlib 并设置 SimHei 中文字体;用 ts.set_token 与 ts.pro_api() 初始化数据接口;对 5 只股票(中信证券、中国平安、招商银行、贵州茅台、中国建筑)循环调用 pro.daily,获取 2023-01-01 至 2023-03-01 的日线数据并提取收盘价,按股票名称拼接为 DataFrame;用 df.corr() 计算相关系数矩阵;用 sns.heatmap(annot=True, cmap='coolwarm') 绘制热力图,添加标题 '股价热力图' 并保存为 1.png。请按平台原始题面(含题面缩进)原样输入教学平台(注释除外),判定以平台为准。
# 注:该代码块存在缩进错误且使用tushare API,请按平台原始题面原样输入,以平台判定为准
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import tushare as ts # 导入tushare模块
import pandas as pd # 导入Pandas数据分析库
import seaborn as sns # 导入Seaborn可视化库
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
plt.rcParams['font.sans-serif']=['SimHei'] # 用黑体显示中文
ts.set_token("1b80258a92e8a547628c8e8477d0c4c590d485d17dfa98eea5a46db7") # 设置Tushare API访问令牌 # 注:正式使用时请替换为你自己的 tushare token(tushare.pro 注册获取)
pro=ts.pro_api() # 初始化Tushare Pro API接口
# 获取股票代码和名称的对应关系
stock_list = {'600030.SH': '中信证券', '601318.SH': '中国平安', '600036.SH': '招商银行', '600519.SH': '贵州茅台','601668.SH':'中国建筑'}
# 获取数据
data = {}
for stock_code, stock_name in stock_list.items(): # 循环遍历
df = pro.daily(ts_code=stock_code, start_date='20230101', end_date='20230301') # 通过API获取df的行情数据
data[stock_name] = df['close'].values.tolist() # 创建列表data[stock_name]
# 转换为DataFrame
df=pd.DataFrame.from_dict(data)
# 绘制热力图
sns.heatmap(df.corr(), annot=True, cmap='coolwarm')
plt.title('股价热力图') # 设置图表标题
plt.savefig("1.png") # 保存图形至文件预期输出(判读要点:本块依赖 tushare 在线接口与题面缩进,本地未实跑,具体以平台运行结果为准):
- 控制台无输出;生成文件
1.png,为一张 5×5 的相关性热力图:行列均为 5 只股票名称,对角线格子全为1(自相关),其余格子的数值为两只股票收盘价序列的皮尔逊相关系数,annot=True在格内标注数值(fmt未指定时默认按两位有效数字显示,如0.85),cmap='coolwarm'下正相关偏红、负相关偏蓝,且配有颜色条 - 由于是对收盘价水平值计算相关,而价格水平普遍带共同趋势,预期非对角格子普遍呈中高正相关(偏红),具体数值以平台实跑为准——口径问题见下方方法论提示
- 区间
20230101至20230301按上交所交易日历共 37 个交易日,样本量说明见下文”样本量要求” - 图上中文(标题、股票名)在平台环境正常显示;未配置中文字体的本地环境会显示为方框
方法论提示: 平台代码直接对收盘价水平值计算相关系数,作为热力图绘制的练习;严格的相关性分析(本章与第34章的方法论)应先把价格转换为对数收益率再计算相关,因为价格水平之间的相关会被共同趋势人为抬高。
代码深度解析:
- 数据来源:
tushare: A股免费数据源- 需要注册获取token
- API限制:每分钟调用次数有限
- 平台代码流程: 依次读取5只股票各自区间的每日收盘价,按列拼接为一个DataFrame,用
df.corr()计算相关系数矩阵,最后用sns.heatmap(annot=True, cmap='coolwarm')绘制热力图。
21.3.2 收益率计算
承接说明: 下方扩展代码块沿用平台任务产出的数据对象——df_prices 对应平台任务中由收盘价拼接而成的数据框 df,log_returns、corr_matrix 由后续代码块依次生成,后文网络图中的 stocks 对应平台的股票字典 stock_list、n 对应股票数量(5只)。
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 计算股票收益率
# =============================================================================
# 承接说明:df_prices 对应平台任务中的收盘价数据框 df(列名为股票名称)
# 本代码块演示如何从价格数据计算收益率。在金融分析中,我们通常使用收益率
# 而非原始价格,因为收益率具有更好的统计特性(平稳性、可加性等)。这里我们
# 同时计算简单收益率和对数收益率,并对比它们的差异。
# ==================== 计算简单收益率 ====================
# pct_change()计算百分比变化:(当前值 - 上期值) / 上期值
# 简单收益率公式: R_t = (P_t - P_{t-1}) / P_{t-1}
simple_returns = df_prices.pct_change().dropna() # 计算简单收益率并删除第一行NaN
# ==================== 计算对数收益率 ====================
# 对数收益率公式: r_t = ln(P_t / P_{t-1}) = ln(P_t) - ln(P_{t-1})
# 对数收益率具有可加性,多期收益率可以相加
log_returns = np.log(df_prices / df_prices.shift(1)).dropna() # 计算对数收益率并删除NaN
# ==================== 输出收益率预览 ====================
print('简单收益率(前5行):') # 打印标题
print(simple_returns.head()) # 打印前5行简单收益率
print(f'\n对数收益率(前5行):') # 打印标题
print(log_returns.head()) # 打印前5行对数收益率
# ==================== 收益率统计对比 ====================
print(f'\n简单收益率统计:') # 打印标题
print(simple_returns.describe().round(4)) # 打印简单收益率的描述统计,保留4位小数
print(f'\n对数收益率统计:') # 打印标题
print(log_returns.describe().round(4)) # 打印对数收益率的描述统计,保留4位小数补充说明:简单收益率 vs 对数收益率
上面代码同时计算了两种收益率,关键差异在于:简单收益率多期不可直接相加、分布偏态,而对数收益率具有时间可加性、分布更接近正态,因此本章后续一律用对数收益率计算相关。两种收益率在公式、可加性、对称性、分布形态与金融应用上的完整对照表,见第 章节 34 章。
21.4 相关性矩阵计算与解读
21.4.1 相关系数计算
理论背景:相关性的数学定义
皮尔逊相关系数(Pearson Correlation Coefficient)以协方差除以两个变量的标准差,衡量线性相关的强度与方向,取值范围为 \([-1, 1]\)。完整的数学定义、公式与逐项解释见第 章节 34 章,本章直接使用这一结论,把重点放在相关结构的计算与可视化上。
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 计算股票收益率相关性矩阵
# =============================================================================
# 本代码块演示如何计算多只股票收益率之间的相关系数矩阵。相关系数矩阵是
# 多变量统计分析的基础,用于量化变量之间的线性关系强度。在金融应用中,
# 相关性矩阵是投资组合优化、风险管理和因子模型的核心输入。
# ==================== 使用对数收益率计算相关性 ====================
# corr()方法计算DataFrame中所有列两两之间的相关系数
# 默认使用皮尔逊相关系数(method='pearson')
# 皮尔逊相关系数衡量两个变量的线性相关程度,取值范围[-1, 1]
corr_matrix = log_returns.corr() # 计算对数收益率的相关系数矩阵
# ==================== 输出相关性矩阵 ====================
print('相关性矩阵:') # 打印标题
print(corr_matrix.round(3)) # 打印相关系数矩阵,保留3位小数
print(f'\n矩阵形状: {corr_matrix.shape}') # 打印矩阵形状(行数,列数)
# ==================== 验证对称性 ====================
# 相关系数矩阵理论上应该是对称矩阵(即矩阵等于其转置)
# 对角线元素应该全为1(变量与自身的相关系数)
print(f'\n矩阵对称性检验:') # 打印标题
print(f'是否对称: {np.allclose(corr_matrix, corr_matrix.T)}') # 检验矩阵是否对称
print(f'对角线元素(应全为1): {np.diag(corr_matrix)}') # 打印对角线元素补充说明: corr() 默认计算皮尔逊相关系数,得到的相关系数矩阵对称、对角线为1;method 参数另支持斯皮尔曼秩相关与肯德尔τ相关。三种 method 的适用场景与相关矩阵的完整数学性质(对称性、正定性等),见第 章节 34 章的代码深度解析。
样本量要求:
- 至少需要30个观测点
- 样本量越大,估计越准确
- 本例:平台代码配置的区间为
start_date='20230101'、end_date='20230301',按上交所交易日历该区间共37个交易日(1月16天、2月20天、3月1日,已扣除春节休市),转为对数收益率后剩36个观测点——满足最低30个观测点的要求,但样本余量不大
21.4.2 相关性解读标准
算出相关系数只是第一步,关键是把它翻译成投资语言:
金融应用中的相关性:
- 分散化(Diversification):
- 投资组合方差: \(\sigma_p^2 = \sum_i \sum_j w_i w_j \sigma_i \sigma_j \rho_{ij}\)
- 相关性越低,组合风险越小
- 马科维茨理论的核心
- 系统性风险:
- 高相关性意味着暴露于共同因子
- 市场崩盘时可能同跌
- 配对交易(Pairs Trading):
- 高相关性股票可能存在价差套利机会
- 统计套利的基础
- 因子模型:
- 相关性聚类识别行业因子
- 风格因子暴露分析
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 相关系数的统计显著性检验
# =============================================================================
# 本代码块演示如何对相关系数进行统计显著性检验,并按照相关性强度进行
# 分类。统计显著性检验帮助我们判断观察到的相关性是否真实存在,还是
# 仅仅由随机波动造成的。显著性水平通常设为0.05、0.01等。
# ==================== 导入统计检验库 ====================
from scipy.stats import pearsonr # 导入皮尔逊相关系数检验函数
# ==================== 提取上三角矩阵(避免重复) ====================
# 相关系数矩阵是对称的,我们只需要上三角元素即可
n = len(corr_matrix) # 获取矩阵维度(股票数量)
pairs = [] # 创建空列表,存储股票对及其相关系数
for i in range(n): # 遍历行
for j in range(i+1, n): # 遍历列(只取上三角,j > i)
stock1 = corr_matrix.index[i] # 获取第i只股票名称
stock2 = corr_matrix.columns[j] # 获取第j只股票名称
corr = corr_matrix.iloc[i, j] # 获取这对股票的相关系数
# ==================== 计算p值 ====================
# pearsonr()返回相关系数和p值
# p值:假设H0(相关系数=0)成立时,观察到当前或更极端统计量的概率
# p值越小,拒绝原假设的证据越强
_, p_value = pearsonr(log_returns[stock1], log_returns[stock2])
# ==================== 存储结果 ====================
pairs.append({
'股票1': stock1, # 第一只股票名称
'股票2': stock2, # 第二只股票名称
'相关系数': corr, # 相关系数
'P值': p_value, # 显著性检验的p值
# 根据p值标注显著性:***极显著,**显著,*边缘显著,ns不显著
'显著性': '***' if p_value < 0.01 else '**' if p_value < 0.05 else '*' if p_value < 0.1 else 'ns'
})
# ==================== 转换为DataFrame并排序 ====================
df_pairs = pd.DataFrame(pairs).sort_values('相关系数', ascending=False) # 按相关系数降序排序
# ==================== 输出结果 ====================
print('股票相关性分析(按相关系数排序):') # 打印标题
print(df_pairs.to_string(index=False)) # 打印所有股票对的分析结果
# ==================== 相关性强度分类 ====================
print(f'\n相关性强度分布:') # 打印标题
# 统计各强度区间的股票对数量
strong_pos = (df_pairs['相关系数'] >= 0.7).sum() # 强正相关(≥0.7)的对数
moderate_pos = ((df_pairs['相关系数'] >= 0.4) & (df_pairs['相关系数'] < 0.7)).sum() # 中度正相关(0.4-0.7)
weak_pos = ((df_pairs['相关系数'] >= 0.1) & (df_pairs['相关系数'] < 0.4)).sum() # 弱正相关(0.1-0.4)
none = ((df_pairs['相关系数'] > -0.1) & (df_pairs['相关系数'] < 0.1)).sum() # 无明显相关(-0.1-0.1)
weak_neg = ((df_pairs['相关系数'] > -0.4) & (df_pairs['相关系数'] <= -0.1)).sum() # 弱负相关(-0.4--0.1)
moderate_neg = ((df_pairs['相关系数'] > -0.7) & (df_pairs['相关系数'] <= -0.4)).sum() # 中度负相关(-0.7--0.4)
strong_neg = (df_pairs['相关系数'] <= -0.7).sum() # 强负相关(≤-0.7)
# ==================== 输出分类统计 ====================
print(f'强正相关(≥0.7): {strong_pos}对') # 打印强正相关对数
print(f'中度正相关(0.4-0.7): {moderate_pos}对') # 打印中度正相关对数
print(f'弱正相关(0.1-0.4): {weak_pos}对') # 打印弱正相关对数
print(f'无明显相关(-0.1-0.1): {none}对') # 打印无明显相关对数
print(f'弱负相关(-0.4--0.1): {weak_neg}对') # 打印弱负相关对数
print(f'中度负相关(-0.7--0.4): {moderate_neg}对') # 打印中度负相关对数
print(f'强负相关(≤-0.7): {strong_neg}对') # 打印强负相关对数补充说明:相关性的统计显著性
相关系数的显著性检验:
- 原假设 H0: \(\rho = 0\)(无相关)
- 检验统计量: \(t = \frac{r\sqrt{n-2}}{\sqrt{1-r^2}}\)
- P值: 观察到当前统计量的概率(假设H0成立)
显著性水平:
***: p < 0.01 (极显著)**: p < 0.05 (显著)*: p < 0.10 (边缘显著)ns: p ≥ 0.10 (不显著)
注意: 即使统计显著,经济意义上的相关性可能仍然很弱。
补充说明:相关性的局限性
相关系数只能衡量线性关系:
- 无法捕捉非线性关系
- 对异常值敏感
- 不能推出因果关系
例如: 如果 \(Y = X^2\),且 \(X\) 对称分布,则 \(\rho_{X,Y} = 0\),但两者有强关系。解读热力图时务必记住这一边界,再进入可视化环节。
21.5 相关性热力图可视化
21.5.1 基础热力图
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 绘制股票收益率相关性热力图
# =============================================================================
# 本代码块演示如何使用seaborn库绘制相关性热力图。热力图是可视化相关系数
# 矩阵最直观的方式,通过颜色深浅表示相关性强弱,可以快速识别股票之间的
# 关系模式。这对于投资组合构建和风险分析非常重要。
# ==================== 导入可视化库 ====================
import seaborn as sns # 导入seaborn库,用于绘制统计图形
# ==================== 创建图形 ====================
plt.figure(figsize=(10, 8)) # 创建图形,设置尺寸为10英寸×8英寸
# ==================== 绘制热力图 ====================
sns.heatmap(
corr_matrix, # 输入相关系数矩阵
annot=True, # 在每个格子中显示数值注释
cmap='RdYlGn', # 设置颜色方案为红-黄-绿渐变
center=0, # 设置颜色中心值为0,使0相关为白色/黄色
square=True, # 设置格子为正方形(而不是矩形)
linewidths=0.5, # 设置格子之间的边框线宽为0.5
fmt='.3f', # 设置数值格式为保留3位小数
cbar_kws={'label': '皮尔逊相关系数'}, # 设置颜色条标签
vmin=-1, vmax=1 # 设置颜色映射范围为[-1, 1],这是相关系数的理论范围
)
# ==================== 设置图形属性 ====================
plt.title('股票收益率相关性热力图\n(2023-01 至 2023-03 平台任务数据)', fontsize=14, pad=20) # 设置标题,字体大小14,下边距20
plt.xlabel('股票', fontsize=12) # 设置x轴标签
plt.ylabel('股票', fontsize=12) # 设置y轴标签
plt.tight_layout() # 自动调整子图参数,使图形紧凑美观
plt.show() # 显示图形代码深度解析:
- 配色方案选择:
coolwarm: 蓝红渐变,适合温度数据RdYlGn: 红黄绿渐变,金融常用RdBu: 红蓝渐变,色盲友好viridis: 感知均匀,适合黑白打印
- 参数说明:
center=0: 强调正负相关对比square=True: 保持格子为正方形annot=True: 显示数值便于精确解读fmt: 数值格式,.3f保留三位小数
- 可读性优化:
- 使用清晰的中文字体
- 合理的图形尺寸
- 清晰的标题和标签
21.5.2 高级热力图
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 绘制带层次聚类的相关性热力图
# =============================================================================
# 本代码块演示如何在热力图基础上添加层次聚类,将相似的股票聚集在一起。
# 层次聚类可以揭示数据中自然的分组结构,在金融中常用于识别行业板块、
# 风格因子等。聚类后的热力图更容易发现股票之间的共同特征。
# ==================== 导入层次聚类库 ====================
from scipy.cluster.hierarchy import linkage, dendrogram # 导入层次聚类相关函数
from scipy.spatial.distance import squareform # 导入距离矩阵转换函数
# ==================== 层次聚类链接 ====================
# linkage()函数执行层次聚类,返回链接矩阵
# method='average'表示使用平均链接法(UPGMA)
# 相关性转换为距离: distance = 1 - abs(correlation)
linkage_matrix = linkage(corr_matrix, method='average') # 计算层次聚类的链接矩阵
# ==================== 绘制带聚类的热力图 ====================
g = sns.clustermap(
corr_matrix, # 输入相关系数矩阵
annot=True, # 显示数值注释
cmap='RdYlGn', # 颜色方案
center=0, # 颜色中心值
square=True, # 正方形格子
linewidths=0.5, # 边框线宽
fmt='.3f', # 数值格式
figsize=(10, 10), # 图形尺寸
row_linkage=linkage_matrix, # 行聚类链接矩阵
col_linkage=linkage_matrix, # 列聚类链接矩阵
cbar_pos=(0.02, 0.8, 0.05, 0.18), # 颜色条位置(左,下,宽,高)
tree_kws={'linewidths': 1.5} # 树状图线宽
)
# ==================== 设置标题 ====================
g.fig.suptitle('股票相关性聚类分析', fontsize=14, y=0.98) # 设置图形总标题
plt.show() # 显示图形
# ==================== 分析聚类结果 ====================
print('聚类树分析:') # 打印标题
print('相近股票被分在同一类,表明受共同因子影响') # 打印解释补充说明:层次聚类方法
层次聚类通过距离度量将相似股票分组:
距离定义:
- 基于相关性: \(distance = 1 - |\rho|\)
- 单链接: 最小距离
- 完全链接: 最大距离
- 平均链接: 平均距离(本例使用)
金融应用:
- 行业识别: 同行业股票聚在一起
- 风格因子: 价值vs成长
- 地域因子: 同地区股票相关
- 风险因子: 暴露于共同风险
21.5.3 网络图可视化
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 绘制股票相关性网络图
# =============================================================================
# 本代码块演示如何使用网络图(Network Graph)可视化股票之间的相关性。
# 网络图将股票表示为节点,相关性表示为边,可以直观展示股票之间的
# 关联结构和系统重要性。网络分析在系统性风险研究中具有重要应用。
# ==================== 导入网络图库 ====================
import networkx as nx # 导入networkx库,用于网络分析
# ==================== 创建网络图(只保留强相关性) ====================
threshold = 0.5 # 设置相关系数阈值为0.5,只保留强相关性
G = nx.Graph() # 创建无向图对象
# ==================== 添加节点 ====================
for stock in stocks.values(): # 遍历所有股票名称
G.add_node(stock) # 为每只股票添加一个节点
# ==================== 添加边(只保留强相关) ====================
for i in range(n): # 遍历股票i
for j in range(i+1, n): # 遍历股票j(j>i,避免重复)
stock1 = corr_matrix.index[i] # 获取股票i的名称
stock2 = corr_matrix.columns[j] # 获取股票j的名称
corr = corr_matrix.iloc[i, j] # 获取这对股票的相关系数
if abs(corr) >= threshold: # 判断相关系数绝对值是否超过阈值
G.add_edge(stock1, stock2, weight=corr) # 添加边,边的权重为相关系数
# ==================== 绘制网络图 ====================
plt.figure(figsize=(10, 8)) # 创建图形
pos = nx.spring_layout(G, k=0.5, seed=42) # 使用弹簧布局算法计算节点位置
# k参数控制节点间的距离,k越大节点越分散
# seed参数确保布局可复现
# ==================== 绘制节点 ====================
# 节点颜色映射度(连接数),度数越大的节点越大
degrees = dict(G.degree()) # 获取每个节点的度数(连接数)
nx.draw_networkx_nodes(
G, pos, # 图对象和位置字典
node_size=[v * 300 for v in degrees.values()], # 节点大小与度数成正比
node_color='steelblue', # 节点颜色为钢蓝色
alpha=0.7 # 透明度0.7
)
# ==================== 绘制边 ====================
# 边的颜色和粗细映射相关性
edges = G.edges() # 获取所有边
weights = [G[u][v]['weight'] for u, v in edges] # 提取每条边的权重(相关系数)
edge_colors = ['red' if w < 0 else 'green' for w in weights] # 负相关用红色,正相关用绿色
nx.draw_networkx_edges(
G, pos, # 图对象和位置字典
width=[abs(w) * 3 for w in weights], # 边的宽度与相关系数绝对值成正比
edge_color=edge_colors, # 边的颜色
alpha=0.6 # 透明度0.6
)
# ==================== 绘制节点标签 ====================
nx.draw_networkx_labels(G, pos, font_size=10, font_family='sans-serif') # 绘制节点标签(股票名称)
# ==================== 设置图形属性 ====================
plt.title(f'股票相关性网络图(阈值={threshold})', fontsize=14) # 设置标题
plt.axis('off') # 关闭坐标轴
plt.tight_layout() # 调整布局
plt.show() # 显示图形
# ==================== 网络分析统计 ====================
print('网络分析:') # 打印标题
print(f'节点数(股票数): {G.number_of_nodes()}') # 打印节点数量
print(f'边数(强相关对): {G.number_of_edges()}') # 打印边数量(强相关股票对数量)
print(f'平均度数: {sum(dict(G.degree()).values()) / G.number_of_nodes():.2f}') # 打印平均度数补充说明:网络图解读
网络图将股票表示为节点,相关性表示为边:
- 节点大小: 度数(连接数),代表与其他股票的相关程度
- 边的颜色: 红色=负相关,绿色=正相关
- 边的粗细: 相关系数绝对值,越粗相关越强
- 节点位置: 相近节点更相关
金融应用:
- 中心节点: 系统重要性高,对市场影响大
- 孤立节点: 独立走势,适合分散化
- 紧密聚类: 行业或风格因子
21.6 相关性的动态变化
21.6.1 滚动窗口相关性
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# =============================================================================
# 题目: 计算和可视化滚动窗口相关性
# =============================================================================
# 本代码块演示如何计算滚动窗口相关性并可视化其动态变化。滚动相关性可以
# 揭示股票关系的时变特征,这对于理解市场状态转换、危机传染和投资组合
# 动态调整非常重要。相关性的不稳定性是现代金融理论的核心发现之一。
# ==================== 选择两只高相关股票 ====================
stock_a = '中信证券' # 第一只股票
stock_b = '中国平安' # 第二只股票
# ==================== 计算滚动相关性(20日窗口) ====================
window = 20 # 设置滚动窗口大小为20个交易日(约1个月)
# rolling(window).corr()计算滚动窗口相关系数
# 对两个Series计算corr()返回滚动相关系数序列
rolling_corr = log_returns[stock_a].rolling(window).corr(log_returns[stock_b])
# ==================== 绘制滚动相关性 ====================
fig, axes = plt.subplots(2, 1, figsize=(14, 10)) # 创建2行1列的子图
# ==================== 子图1:滚动相关性 ====================
axes[0].plot(rolling_corr.index, rolling_corr, linewidth=2) # 绘制滚动相关系数曲线
axes[0].axhline(y=0, color='k', linestyle='--', linewidth=1) # 添加0参考线(黑色虚线)
axes[0].axhline(y=0.5, color='r', linestyle='--', linewidth=1, label='阈值0.5') # 添加0.5阈值线(红色虚线)
axes[0].axhline(y=-0.5, color='r', linestyle='--', linewidth=1, label='阈值-0.5') # 添加-0.5阈值线
axes[0].set_title(f'{stock_a}与{stock_b}的滚动相关性(窗口={window}日)', fontsize=12) # 设置标题
axes[0].set_ylabel('相关系数', fontsize=10) # 设置y轴标签
axes[0].legend(fontsize=10) # 显示图例
axes[0].grid(True, alpha=0.3) # 显示网格,透明度0.3
# ==================== 子图2:价格走势(归一化) ====================
# 归一化价格:将初始价格设为100,便于比较不同价格水平的股票
# 公式: 归一化价格 = (原价格 / 初始价格) × 100
norm_prices = df_prices[[stock_a, stock_b]] / df_prices[[stock_a, stock_b]].iloc[0] * 100
axes[1].plot(norm_prices.index, norm_prices[stock_a], label=stock_a, linewidth=2) # 绘制股票A的归一化价格
axes[1].plot(norm_prices.index, norm_prices[stock_b], label=stock_b, linewidth=2) # 绘制股票B的归一化价格
axes[1].set_title('归一化价格走势', fontsize=12) # 设置标题
axes[1].set_ylabel('初始价格=100', fontsize=10) # 设置y轴标签
axes[1].legend(fontsize=10) # 显示图例
axes[1].grid(True, alpha=0.3) # 显示网格
plt.tight_layout() # 调整布局
plt.show() # 显示图形
# ==================== 滚动相关性统计 ====================
print('滚动相关性分析:') # 打印标题
print(f'平均相关性: {rolling_corr.mean():.3f}') # 打印滚动相关系数的均值
print(f'相关性标准差: {rolling_corr.std():.3f}') # 打印滚动相关系数的标准差
print(f'相关性范围: [{rolling_corr.min():.3f}, {rolling_corr.max():.3f}]') # 打印最小值和最大值金融应用:相关性的不稳定性
相关系数不是恒定的:牛市普遍同涨时相关性升高,市场急剧下跌时相关性整体抬升(correlation spike),危机时期分散化效果会明显减弱。市场状态依赖、2008年金融危机案例与对应的策略启示,在第 章节 34 章有完整展开;本节关注的是如何用滚动窗口把这种不稳定性直观地呈现出来。
21.7 本章小结
要点:
- 相关性是投资组合理论的输入:组合方差 \(\sigma_p^2 = \sum_i \sum_j w_i w_j \sigma_i \sigma_j \rho_{ij}\),相关性越低分散化效果越好;皮尔逊相关系数量度线性关系,取值 \([-1,1]\)
- 严格口径是”先取对数收益率、再算相关”:价格水平相关被共同趋势抬高,收益率相关才反映逐期联动;对数收益率具有可加性、更接近正态
- 相关系数矩阵是对称矩阵、对角线为 1;
DataFrame.corr()默认皮尔逊,可选 spearman/kendall,且按成对完备样本计算 - 热力图三件套:
annot=True标数值、center=0让正负相关颜色对称、vmin=-1, vmax=1固定色标范围;带clustermap的层次聚类可把相关结构相似的股票排到一起 - 相关结构是时变的:滚动窗口相关、显著性检验、网络图阈值化都在回答”这个相关系数可信吗、稳吗”
易错点:
- 直接对价格水平算
corr()是本章平台任务的口径,也是最常见的口径陷阱——练习可以这样做,研究须先转收益率 - 样本量不足会让相关系数极不稳定:本例区间 37 个交易日、转收益率后 36 个观测,刚过最低 30 的门槛
- 相关不等于因果,也不能捕捉非线性关系(\(Y=X^2\) 与 \(X\) 的相关可以为 0)
- 网络图的阈值(threshold)是主观选择,阈值一变,边的数量与”中心股票”的结论随之改变
heatmap的fmt='.3f'与annot配合才能正常显示三位小数,传入cbar_kws的标签是颜色条的标签而非图标题- 平台任务代码存在题面缩进,须按平台原样输入,本地直接复制运行会报
IndentationError
21.8 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证(涉及平台数据的题目可在教学平台完成,或先用模拟数据演练)。
输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。
import pandas as pd df = pd.DataFrame({ 'A': [1, 2, 3, 4, 5], 'B': [2, 4, 6, 8, 10], 'C': [-1, -2, -3, -4, -5], }) print(df.corr().round(2))参考答案(先写下你的预测再点开)
解题思路:先看列之间的函数关系再推相关系数。
B恰好等于A的 2 倍(B = 2A),是完全的正线性关系,相关系数为 1;C恰好等于A的相反数(C = −A),是完全的负线性关系,相关系数为 −1;B与C之间同样是完全负线性(C = −B/2),相关系数为 −1。相关系数只刻画线性关系的方向与强度,对变量的正数缩放完全不变(乘以 2 不改变相关),乘以负数则使符号翻转(乘以 −1 相关由 +1 变 −1)。因此输出矩阵对称、对角线全为 1,非对角元素按上述三组取值 1、−1、−1。# 验证脚本:输出三列数据的相关系数矩阵 import pandas as pd # 导入pandas用于构造数据框并计算相关 df = pd.DataFrame({ # 构造3列5行的数据框 'A': [1, 2, 3, 4, 5], # 基准序列 'B': [2, 4, 6, 8, 10], # B=2A,完全正相关 'C': [-1, -2, -3, -4, -5], # C=-A,完全负相关 }) print(df.corr().round(2)) # 相关系数矩阵,保留两位小数预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
A B C A 1.0 1.0 -1.0 B 1.0 1.0 -1.0 C -1.0 -1.0 1.0回扣本章:对应本章小结“要点”第 3 条——相关系数矩阵是对称矩阵、对角线为 1,皮尔逊相关只度量线性关系且不受线性缩放影响。
概念辨析:对收盘价水平值计算相关、与先转对数收益率再计算相关,两种口径的结果为何可能相差很大甚至符号相反?相关系数矩阵与协方差矩阵又差在哪里(量纲、取值范围、对标准化的依赖)?
参考答案(点开前请先独立完成)
解题思路:逐点作答。第一,两种口径为何可能相差很大甚至符号相反:收盘价水平近似随机游走,每只股票在样本期内的价格路径都包含共同市场路径(共同趋势)与自己的一段独立漂移。对水平值计算相关,衡量的是“累计位置”的联动,共同趋势会把普遍的相关人为抬高,接近“都在涨”;而个股自身的漂移在小样本里随机性极强,甚至可能让个别股票对的相关符号与逐期联动的方向相反——第 3 题的模拟演练会直观呈现这一点。转成对数收益率后序列近似平稳,价格共同趋势被差分消去,相关反映的是逐期同涨同跌的真实联动。第二,相关矩阵与协方差矩阵的差别:协方差 \(\mathrm{cov}(X,Y)=E[(X-\mu_X)(Y-\mu_Y)]\) 保留量纲(股价相乘为元×元),数值大小随变量量级伸缩,不同股票对之间不可直接比较;相关系数 \(\rho=\mathrm{cov}(X,Y)/(\sigma_X\sigma_Y)\) 用两个标准差做标准化,无量纲、取值限于 \([-1,1]\),可跨对、跨数据集比较。一句话:协方差矩阵反映“联动幅度 + 量纲”,相关矩阵只保留“联动方向与强度”。
回扣本章:对应本章小结“要点”第 2 条(严格口径是“先取对数收益率、再算相关”,价格水平相关被共同趋势抬高)与“易错点”第 1 条(直接对价格水平算
corr()是本章平台任务的口径,也是最常见的口径陷阱)。变式任务(平台任务同型改造,可先用模拟数据演练):用
np.random.default_rng(42)生成 5 只”股票”37 个交易日的模拟价格(让共同市场因子明显强于个股噪声后累积成价格),分别对价格水平与对数收益率各画一张热力图(cmap='RdBu'、center=0、vmin=-1, vmax=1),对比两张图传达的相关性强弱差异。参考答案(点开前请先独立完成)
解题思路:按题面设定用
np.random.default_rng(42)生成模拟数据:共同市场因子取日均漂移 0.3%、波动 1.5% 的日对数收益,个股噪声波动 1.0%(明显弱于市场因子),个股对数收益 = 市场因子 + 个股噪声,再从 100 元起按 \(\exp(\text{cumsum})\) 累积成 37 个交易日的价格,与平台任务的样本结构(37 个交易日、5 只股票)一致。两个口径各算一张 5×5 热力图(cmap='RdBu'、center=0、vmin=-1, vmax=1)。结果判读:价格水平口径的非对角相关普遍落在 0.76—0.96(均值约 0.89),对数收益率口径只有 0.57—0.78(均值约 0.68)——共同上涨趋势把价格水平相关整体抬高,收益率口径才反映真实的逐期联动强度(理论上应接近市场因子的方差占比 0.015²/(0.015²+0.010²)≈0.69)。这与本章“方法论提示”一致:平台任务对价格水平算相关仅作绘图练习(列表 21.1),严格的相关性分析应先转对数收益率。# 块一(模拟与计算):模拟5只股票37个交易日,计算两个口径的相关矩阵 import numpy as np # 导入NumPy生成模拟数据 import pandas as pd # 导入pandas组织价格与收益率数据 random_generator = np.random.default_rng(42) # 固定随机种子,模拟结果可复现 trading_days, stock_count = 37, 5 # 37个交易日、5只模拟股票 market_returns = random_generator.normal(0.003, 0.015, trading_days) # 共同市场因子:日均漂移0.3%、波动1.5% idio_returns = random_generator.normal(0, 0.010, (trading_days, stock_count)) # 个股噪声:波动1.0%,明显弱于市场因子 stock_log_returns = market_returns[:, None] + idio_returns # 个股对数收益=共同市场因子+个股噪声 stock_names = ['股票A', '股票B', '股票C', '股票D', '股票E'] # 模拟股票名称 price_df = pd.DataFrame(100 * np.exp(np.cumsum(stock_log_returns, axis=0)), columns=stock_names) # 从100元起按对数收益累积成价格 return_df = np.log(price_df / price_df.shift(1)).dropna() # 价格再转回对数收益率(36个观测) corr_price = price_df.corr() # 口径一:对价格水平值计算相关矩阵 corr_return = return_df.corr() # 口径二:对对数收益率计算相关矩阵 print('价格水平相关矩阵:') # 打印口径一矩阵 print(corr_price.round(3)) # 保留三位小数 print('对数收益率相关矩阵:') # 打印口径二矩阵 print(corr_return.round(3)) # 保留三位小数 off_diagonal_mask = ~np.eye(stock_count, dtype=bool) # 非对角掩码(对角线恒为1不计入) print('价格水平非对角相关系数均值: {:.3f}'.format(corr_price.values[off_diagonal_mask].mean())) # 口径一平均强度 print('对数收益率非对角相关系数均值: {:.3f}'.format(corr_return.values[off_diagonal_mask].mean())) # 口径二平均强度预期输出(本机 peter 环境实际运行结果,随机种子固定故可复现):
价格水平相关矩阵: 股票A 股票B 股票C 股票D 股票E 股票A 1.000 0.821 0.910 0.931 0.962 股票B 0.821 1.000 0.762 0.863 0.850 股票C 0.910 0.762 1.000 0.908 0.906 股票D 0.931 0.863 0.908 1.000 0.963 股票E 0.962 0.850 0.906 0.963 1.000 对数收益率相关矩阵: 股票A 股票B 股票C 股票D 股票E 股票A 1.000 0.648 0.682 0.571 0.656 股票B 0.648 1.000 0.678 0.707 0.631 股票C 0.682 0.678 1.000 0.780 0.643 股票D 0.571 0.707 0.780 1.000 0.750 股票E 0.656 0.631 0.643 0.750 1.000 价格水平非对角相关系数均值: 0.888 对数收益率非对角相关系数均值: 0.675# 块二(绘图):并排绘制两张相关热力图(图形在线版不执行、以教学平台渲染为准) import seaborn as sns # 导入Seaborn绘制热力图 import matplotlib.pyplot as plt # 导入Matplotlib绘图库 fig, axes = plt.subplots(1, 2, figsize=(12, 5)) # 1行2列子图并排 sns.heatmap(corr_price, annot=True, cmap='RdBu', center=0, vmin=-1, vmax=1, fmt='.2f', ax=axes[0]) # 左图:价格水平口径 axes[0].set_title('价格水平相关') # 左图标题 sns.heatmap(corr_return, annot=True, cmap='RdBu', center=0, vmin=-1, vmax=1, fmt='.2f', ax=axes[1]) # 右图:对数收益率口径 axes[1].set_title('对数收益率相关') # 右图标题 plt.tight_layout() # 自动调整布局 plt.savefig('ch21_q3.png') # 保存图形(也可用plt.show()显示)图形判读:两张热力图的非对角格子全部为红色系(正相关),左图(价格水平)颜色普遍更深,右图(对数收益率)明显更浅,对角线均为最深红(1.00);本机未安装中文字体时图上股票名显示为方框,属环境问题,以平台渲染为准。
注意:以上为本题变式的独立演练代码;列表 21.1 对应平台任务的原始代码块仍须按平台原始题面原样输入教学平台,不要用本变式替换。
回扣本章:对应本章小结“要点”第 2、4 条(先收益率再相关的严格口径;热力图三件套
annot=True、center=0、vmin/vmax)与“易错点”第 1 条(价格水平算corr()是平台任务的练习口径)。思考题:把网络图的阈值
threshold从 0.5 提高到 0.7,边的数量与图的连通性会怎么变?这对”哪些股票适合配对交易”的筛选意味着什么?参考答案(点开前请先独立完成)
解题思路:阈值提高的直接后果是“删边”。
threshold从 0.5 提到 0.7 后,所有 \(|\rho|<0.7\) 的边被剔除,边数明显减少,图迅速稀疏化:原本在 0.5 阈值下连成一片的网络会断开成若干小簇,一些只与弱相关股票相连的股票成为孤立节点;度数(连接数)普遍下降,网络图中心—外围结构更突出。对配对交易筛选的含义:候选股票对变少,但留下的都是联动更强的对,价差回复的统计基础更牢、误选“伪相关”对的机会更小——筛选变严了。同时要警惕:阈值本身是主观选择,阈值一变,边的数量与“哪些股票适合同类筛选”的名单随之改变,同一份数据在 0.5 与 0.7 下可能给出不同的中心股票与候选对。实务上不应只依赖单一阈值,应结合协整检验、价差平稳性与样本外验证,并考察阈值敏感性(多个阈值下结论是否稳健)。回扣本章:对应本章小结“要点”第 5 条(网络图阈值化在回答“这个相关系数可信吗、稳吗”)与“易错点”第 4 条(阈值是主观选择,阈值一变,边的数量与“中心股票”的结论随之改变)。
思考题:为什么市场大幅下跌时股票间的相关性往往整体抬升(correlation spike)?这一现象对”靠低相关资产分散风险”的组合意味着什么?
参考答案(点开前请先独立完成)
解题思路:机制上,市场大幅下跌时宏观恐慌、流动性挤兑与去杠杆使个股走势被市场系统性因子主导,个股特质成分被淹没,两两相关整体抬升(correlation spike);流动性差的资产还可能因被迫抛售同向下跌,进一步抬高相关。这一抬升恰恰发生在组合最需要对冲的尾部时刻。对“靠低相关资产分散风险”的组合的含义:平静期测得的低相关会高估危机中的分散化效果——组合方差公式 \(\sigma_p^2=\sum_i\sum_j w_iw_j\sigma_i\sigma_j\rho_{ij}\) 中的 \(\rho_{ij}\) 是时变的,尾部时刻整体变大,分散化收益同时缩水,即“分散化在最需要它的时候失效”。实务应对:用压力情景或尾部(条件)相关性而非全样本平均相关做风险评估;配置危机中真正低相关乃至负相关的资产(如利率债、黄金);并辅以仓位控制与止损等风险管理手段,而不是把组合安全完全寄托在历史平均相关上。
回扣本章:对应本章小结“要点”第 5 条(相关结构是时变的)与“相关性的动态变化”一节——滚动窗口相关正是把这种不稳定性直观呈现出来的工具。