4、因子有效性检验:IC分析、Rank IC、分组回测与t检验
因子选出来了,数据也处理干净了。但问题来了——这个因子到底有没有用?
我见过不少新手,辛辛苦苦构造了一个因子,回测曲线漂亮得不行,结果实盘一跑就崩。为什么?因为没做有效性检验。说白了,因子有效性检验就是给因子做一次「体检」,看看它是不是真的能预测收益,还是纯粹运气好。
这一节,我会带你过一遍我常用的五把「筛子」:IC分析、Rank IC、分组回测、t检验、相关性分析。每把筛子都能筛掉一批伪因子。
4.1 信息系数(IC)分析
IC(Information Coefficient),就是因子值与未来收益的相关系数。简单理解:因子值高的股票,未来收益是不是也高?
IC的计算方式有两种主流选择:
- Pearson相关系数:衡量线性关系。我一般只在因子值近似正态分布时用。
- Spearman秩相关系数:衡量单调关系。更稳健,对异常值不敏感。
我个人习惯用Spearman,因为金融数据里异常值太多了。你想想看,一个极端值就能把Pearson IC拉偏,但Rank IC基本不受影响。
IC的评判标准(我自己的经验值):
- |IC| > 0.05:勉强可用,需要结合其他因子
- |IC| > 0.10:不错,可以考虑单独使用
- |IC| > 0.15:优秀,但要注意是不是过拟合
- IC稳定为正或为负:比绝对值大小更重要
代码实现其实很简单:
import pandas as pd
import numpy as np
from scipy.stats import spearmanr, pearsonr
def calc_ic(factor_series, return_series, method='spearman'):
"""
计算截面IC
factor_series: 某期因子值(index为股票代码)
return_series: 对应未来收益
"""
if method == 'spearman':
ic, p_value = spearmanr(factor_series, return_series)
else:
ic, p_value = pearsonr(factor_series, return_series)
return ic, p_value
# 举个实际例子
# 假设我们有2024-01-05这天的因子值和未来5日收益
factors = pd.Series({'000001': 2.3, '000002': 1.8, '000003': 3.1})
returns = pd.Series({'000001': 0.05, '000002': 0.03, '000003': 0.07})
ic, p = calc_ic(factors, returns)
print(f'IC值: {ic:.4f}, p值: {p:.4f}')
小技巧:我通常会计算滚动12个月的IC均值,再除以IC标准差,得到ICIR(信息系数比率)。ICIR > 0.5 才算稳定。这个指标比单纯看IC均值靠谱得多。
4.2 Rank IC分析
Rank IC其实就是用秩相关系数算的IC。为什么单独拿出来说?因为它在实战中太重要了。
我在项目中遇到过这样的情况:一个因子Pearson IC只有0.02,但Rank IC有0.12。后来发现,因子值分布严重左偏,几个大市值股票把线性关系全破坏了。换成Rank IC后,因子表现一直很稳定。
Rank IC的优势:
- 对极端值不敏感——金融数据里极端值太多了
- 只关心排序,不关心具体数值——选股本质上就是排序
- 更容易做多空组合——按排名分组就行
计算Rank IC,其实就是把因子值和收益都换成排名:
def calc_rank_ic(factor_series, return_series):
"""Rank IC = Spearman相关系数"""
rank_ic, p_value = spearmanr(factor_series, return_series)
return rank_ic, p_value
# 或者手动算排名再算Pearson,结果一样
factor_rank = factor_series.rank()
return_rank = return_series.rank()
rank_ic_manual, _ = pearsonr(factor_rank, return_rank)
注意:Rank IC虽然稳健,但会丢失一些信息。如果因子值和收益之间确实是线性关系,Pearson IC的统计检验力更强。我一般两个都算,差异大的时候就要警惕了。
4.3 分组回测法
IC分析告诉你因子和收益「有没有关系」,但分组回测告诉你「关系长什么样」。
做法很简单:每期按因子值把股票分成N组(通常是5组或10组),然后看每组未来的平均收益。理想情况下,分组收益应该是单调的——第1组收益最高,第10组收益最低(或者反过来)。
我习惯分5组,因为A股股票数量多,分10组每组也有几百只,统计意义够用。
def group_backtest(factor_df, return_df, n_groups=5):
"""
分组回测
factor_df: 日期×股票的因子值矩阵
return_df: 对应的未来收益矩阵
"""
results = {}
for date in factor_df.index:
# 获取当期的因子和收益
f = factor_df.loc[date].dropna()
r = return_df.loc[date]
# 按因子值分组
groups = pd.qcut(f, n_groups, labels=False)
# 计算每组平均收益
for g in range(n_groups):
mask = groups == g
group_return = r[mask.index[mask]].mean()
results.setdefault(g, []).append(group_return)
# 汇总
summary = pd.DataFrame(results).mean()
return summary
# 输出示例
# 分组0(因子值最小): 0.2%
# 分组1: 0.5%
# 分组2: 0.8%
# 分组3: 1.1%
# 分组4(因子值最大): 1.5%
# 多空收益(组4-组0): 1.3%
分组回测的核心指标:
- 单调性:收益是否严格递增/递减?偶尔一两个组偏离可以接受,但整体趋势要清晰
- 多空收益:最高组减最低组的收益。年化5%以上算不错
- 分组稳定性:每组的股票数量是否均匀?如果某组只有几只股票,结果不可靠
4.4 因子收益率t检验
分组回测做完了,多空收益曲线看着不错。但这是不是统计显著的?会不会是随机波动?
这时候就需要t检验了。对多空收益序列做t检验,看均值是否显著不为零。
from scipy import stats
def t_test_long_short(long_short_returns):
"""
对多空收益序列做t检验
long_short_returns: 多空组合每日/每期收益序列
"""
t_stat, p_value = stats.ttest_1samp(long_short_returns, 0)
return t_stat, p_value
# 假设我们有多空收益序列
ls_returns = [0.01, 0.02, -0.005, 0.015, 0.008, ...]
t, p = t_test_long_short(ls_returns)
print(f't统计量: {t:.3f}, p值: {p:.4f}')
# 通常p < 0.05 认为显著
if p < 0.05:
print('多空收益显著不为零,因子有效')
else:
print('多空收益可能来自随机波动,需谨慎')
我的经验:p值小于0.05只是最低门槛。我一般要求p < 0.01,同时t统计量绝对值大于2.5。为什么?因为因子挖掘中多重比较的问题太严重了——你试了100个因子,总有几个能碰巧通过检验。
4.5 因子相关性分析
最后一个筛子:因子之间的相关性。
你辛辛苦苦挖了10个因子,每个单独检验都通过了。但把它们放在一起时,发现其中8个高度相关——说白了,它们反映的是同一个东西。这样不仅浪费,还会导致模型过拟合。
我一般会做两件事:
- 计算因子相关系数矩阵——看哪些因子冗余
- 做聚类分析——把相似因子归为一类,每类只选一个代表
def factor_correlation_analysis(factor_df):
"""
因子相关性分析
factor_df: 日期×因子(多列)的数据框
"""
# 计算截面相关系数的均值
corr_list = []
for date in factor_df.index:
corr = factor_df.loc[date].corr()
corr_list.append(corr)
avg_corr = pd.concat(corr_list).groupby(level=[0,1]).mean()
# 找出高度相关的因子对
high_corr_pairs = []
for i in range(len(avg_corr.columns)):
for j in range(i+1, len(avg_corr.columns)):
if abs(avg_corr.iloc[i, j]) > 0.7:
high_corr_pairs.append(
(avg_corr.columns[i], avg_corr.columns[j], avg_corr.iloc[i, j])
)
return avg_corr, high_corr_pairs
# 输出示例
# 高度相关的因子对:
# 市盈率 vs 市净率: 0.82
# 动量因子 vs 反转因子: -0.75
# 波动率因子 vs 换手率因子: 0.78
避坑指南:我曾经犯过一个错误——只看了截面相关系数,没看时序相关系数。结果两个因子截面相关性很低,但它们的IC时序相关性高达0.9。这意味着它们在不同时期「同时有效、同时失效」,组合后并没有分散化效果。所以,IC时序相关性也要看。
知识体系总览
下面这张图总结了因子有效性检验的完整流程。我建议你把它打印出来贴在工位上——每次挖到新因子,就按这个流程走一遍。
这五步走完,一个因子能不能用,我心里基本就有数了。当然,这只是「体检」的第一步。下一节我们会聊到因子合成和权重优化——但那是后话了。
记住一句话:没有经过有效性检验的因子,就是一张废纸。我在早期踩过太多这样的坑了,希望你不用再踩一遍。
公众号:蓝海资料掘金营,微信deep3321