第四章:因子相关性分析
各位好,我是老张。今天咱们聊聊因子相关性分析。说实话,这是多因子模型里最容易被忽视、但又最容易翻车的一环。
我见过不少新手,辛辛苦苦挖了几十个因子,往模型里一塞,回测曲线漂亮得不行。结果实盘一跑,直接崩了。为什么?因为因子之间高度相关,模型本质上是在重复押注同一个风险源。
所以,这一章我们重点解决三个问题:因子之间到底有多像?它们是不是在说同一件事?以及,我们该怎么处理这种“撞车”现象?
核心观点:因子相关性分析不是“可选项”,而是“必选项”。跳过这一步,你的模型就是在裸奔。
4.1 皮尔逊相关系数:最常用的“线性标尺”
皮尔逊相关系数,说白了就是衡量两个因子之间线性关系的强弱。取值范围在[-1, 1]之间,绝对值越接近1,说明线性相关性越强。
我个人习惯在因子筛选的第一轮就用它。为什么?因为快。计算简单,一眼就能看出哪些因子在“说同样的故事”。
我的经验:如果两个因子的皮尔逊相关系数绝对值超过0.7,我一般会考虑只保留其中一个。但注意,这个阈值不是死的。我在做A股小市值因子时,发现很多因子天然就高度相关,这时候我会把阈值放宽到0.8甚至0.85。
import pandas as pd
import numpy as np
from scipy.stats import pearsonr
# 假设 factor_df 是因子数据,每列一个因子
def calc_pearson_matrix(factor_df):
"""计算因子间的皮尔逊相关系数矩阵"""
return factor_df.corr(method='pearson')
# 示例:计算两个因子的皮尔逊系数
factor_a = np.random.randn(100)
factor_b = factor_a * 0.8 + np.random.randn(100) * 0.2
corr, p_value = pearsonr(factor_a, factor_b)
print(f"皮尔逊相关系数: {corr:.3f}, p值: {p_value:.4f}")
注意:皮尔逊相关系数对异常值非常敏感。我曾经遇到过一个因子,本来和其他因子相关性只有0.3,结果因为某一天的数据异常,相关性直接飙到0.9。所以,用之前最好先做一下极值处理。
4.2 斯皮尔曼秩相关系数:不挑分布的“老好人”
斯皮尔曼秩相关系数,和皮尔逊最大的区别在于:它不关心因子具体的数值,只关心排名。你想想看,如果两个因子对股票的排序几乎一致,那它们是不是在表达类似的信息?
我个人更偏爱斯皮尔曼。为什么?因为金融数据很少服从正态分布。皮尔逊要求线性关系、正态假设,但斯皮尔曼什么都不要求。它只要求单调关系——说白了,一个因子涨,另一个也跟着涨就行,不一定要成直线。
from scipy.stats import spearmanr
def calc_spearman_matrix(factor_df):
"""计算因子间的斯皮尔曼秩相关系数矩阵"""
return factor_df.corr(method='spearman')
# 示例:对比皮尔逊和斯皮尔曼
factor_x = np.array([1, 2, 3, 4, 100]) # 注意最后一个异常值
factor_y = np.array([2, 4, 6, 8, 200])
pearson_corr, _ = pearsonr(factor_x, factor_y)
spearman_corr, _ = spearmanr(factor_x, factor_y)
print(f"皮尔逊: {pearson_corr:.3f}") # 受异常值影响很大
print(f"斯皮尔曼: {spearman_corr:.3f}") # 几乎不受影响
实用建议:我一般两个都算。如果皮尔逊和斯皮尔曼的结果差异很大(比如皮尔逊0.9,斯皮尔曼只有0.5),说明因子之间的关系不是简单的线性关系,或者存在异常值。这时候我会深入检查数据。
4.3 因子共线性诊断:别让因子“抱团”坑了你
相关性矩阵只能看到两两之间的关系。但现实中,三个甚至更多因子之间可能存在多重共线性——单个看都不相关,但合在一起就出问题。
我记得有一次做行业轮动模型,选了5个因子,两两相关性都不超过0.4。结果模型一跑,回归系数的标准误大得离谱。后来一查,原来是三个因子共同反映了同一个宏观因子。
常用的诊断方法有两个:
- 特征值分析法:计算因子协方差矩阵的特征值。如果某个特征值接近0,说明存在共线性。
- 条件指数:最大特征值与最小特征值之比的平方根。条件指数大于30,一般认为存在严重共线性。
from numpy.linalg import eig
def collinearity_diagnosis(factor_df):
"""共线性诊断:特征值和条件指数"""
# 标准化
std_df = (factor_df - factor_df.mean()) / factor_df.std()
# 计算协方差矩阵的特征值
eigenvalues, _ = eig(std_df.cov())
eigenvalues = np.sort(eigenvalues)[::-1] # 降序排列
condition_index = np.sqrt(eigenvalues[0] / eigenvalues)
print("特征值:", eigenvalues)
print("条件指数:", condition_index)
# 判断
if condition_index[-1] > 30:
print("⚠️ 存在严重共线性,建议处理")
else:
print("✅ 共线性在可接受范围内")
4.4 VIF方差膨胀因子:量化共线性的“照妖镜”
VIF,全称Variance Inflation Factor,中文叫方差膨胀因子。它衡量的是:某个因子被其他所有因子解释的程度。
怎么算?很简单:把因子A作为因变量,其他所有因子作为自变量,做一个线性回归。然后看R²。VIF = 1 / (1 - R²)。
如果VIF很大,说明因子A和其他因子高度相关,它的信息大部分是冗余的。
我的经验阈值:
- VIF < 5:安全,可以保留
- 5 ≤ VIF < 10:需要警惕,考虑是否剔除
- VIF ≥ 10:强烈建议剔除
当然,这个阈值不是绝对的。我在做高频因子时,因为因子本身噪音大,VIF阈值会放宽到15。但做基本面因子时,我严格控制在5以内。
from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.tools.tools import add_constant
def calc_vif(factor_df):
"""计算每个因子的VIF值"""
# 添加截距项
X = add_constant(factor_df)
vif_data = pd.DataFrame()
vif_data["因子"] = factor_df.columns
vif_data["VIF"] = [variance_inflation_factor(X.values, i+1)
for i in range(len(factor_df.columns))]
return vif_data.sort_values("VIF", ascending=False)
# 示例
np.random.seed(42)
n = 200
df = pd.DataFrame({
'因子A': np.random.randn(n),
'因子B': np.random.randn(n) * 0.5 + 0.5,
'因子C': df['因子A'] * 0.9 + np.random.randn(n) * 0.1, # 高度依赖A
'因子D': np.random.randn(n)
})
vif_result = calc_vif(df)
print(vif_result)
避坑指南:我曾经犯过一个错误——把所有因子一次性丢进去算VIF。结果发现VIF都很大,但两两相关性却不高。后来才意识到,是因子数量太多,导致多重共线性。所以,VIF最好在因子筛选的后期使用,先通过皮尔逊/斯皮尔曼筛掉一批高度相关的,再用VIF做精细诊断。
4.5 实战流程:我一般怎么做
说了这么多,给大家总结一下我个人在项目中的标准流程:
- 第一步:计算皮尔逊和斯皮尔曼相关系数矩阵,快速扫描。两两相关性超过0.8的,标记为“候选剔除”。
- 第二步:对标记的因子对,结合业务逻辑判断。比如动量因子和反转因子,虽然相关,但逻辑相反,我会都保留。
- 第三步:对剩余因子做共线性诊断,计算条件指数。如果条件指数大于30,进入第四步。
- 第四步:逐个计算VIF,剔除VIF大于10的因子。每次剔除一个,重新计算,直到所有因子VIF都达标。
最后说一句:因子相关性分析不是一次性的工作。市场在变,因子的相关性也在变。我建议每季度重新做一次相关性分析,看看之前筛选的因子是否还“干净”。