第四章:因子相关性分析

各位好,我是老张。今天咱们聊聊因子相关性分析。说实话,这是多因子模型里最容易被忽视、但又最容易翻车的一环。

我见过不少新手,辛辛苦苦挖了几十个因子,往模型里一塞,回测曲线漂亮得不行。结果实盘一跑,直接崩了。为什么?因为因子之间高度相关,模型本质上是在重复押注同一个风险源。

所以,这一章我们重点解决三个问题:因子之间到底有多像?它们是不是在说同一件事?以及,我们该怎么处理这种“撞车”现象?

核心观点:因子相关性分析不是“可选项”,而是“必选项”。跳过这一步,你的模型就是在裸奔。

因子相关性分析知识体系 因子相关性分析 皮尔逊相关系数 线性关系 · 正态假设 斯皮尔曼秩相关系数 单调关系 · 无分布假设 共线性诊断 特征值 · 条件指数 VIF方差膨胀因子 量化共线性 · 阈值判断 目标:剔除冗余因子,提升模型稳健性

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 实战流程:我一般怎么做

说了这么多,给大家总结一下我个人在项目中的标准流程:

  1. 第一步:计算皮尔逊和斯皮尔曼相关系数矩阵,快速扫描。两两相关性超过0.8的,标记为“候选剔除”。
  2. 第二步:对标记的因子对,结合业务逻辑判断。比如动量因子和反转因子,虽然相关,但逻辑相反,我会都保留。
  3. 第三步:对剩余因子做共线性诊断,计算条件指数。如果条件指数大于30,进入第四步。
  4. 第四步:逐个计算VIF,剔除VIF大于10的因子。每次剔除一个,重新计算,直到所有因子VIF都达标。

最后说一句:因子相关性分析不是一次性的工作。市场在变,因子的相关性也在变。我建议每季度重新做一次相关性分析,看看之前筛选的因子是否还“干净”。


专注资料整理