3. 单因子分析基础:IC分析、IR分析、分组回测、因子收益率的计算
做量化这几年,我见过太多人一上来就搞复杂的机器学习模型,结果因子本身有没有预测能力都没搞清楚。说白了,单因子分析就是给每个因子做「体检」——它到底能不能赚钱?稳定性如何?有没有逻辑支撑?
这一章,我带你把这四个核心工具过一遍:IC分析、IR分析、分组回测、因子收益率计算。它们就像医生的听诊器、血压计、CT机和化验单,缺一不可。
核心观点:单因子分析不是走流程,而是帮你建立对因子的「直觉」。我习惯先跑IC,再看分组,最后算收益率——顺序别搞反了。
3.1 IC分析:因子的「预测力」体检
IC(Information Coefficient)衡量的是因子值与未来收益之间的相关性。我习惯用Spearman秩相关系数,也就是Rank IC——它对极端值不敏感,更稳健。
为什么用秩相关?举个例子,你有个因子值特别大的股票,但它未来收益一般。如果用Pearson相关系数,这个点会严重拉低结果。但Rank IC只看排名顺序,你排第1,收益也排第1,那就没问题。
我的习惯:每次跑完IC,我都会看一眼IC序列的时序图。如果IC忽正忽负、像心电图一样乱跳,那这个因子基本没戏。稳定的正IC才是好因子。
计算IC的代码其实很简单,但要注意几个坑:
import pandas as pd
import numpy as np
from scipy.stats import spearmanr
def calc_rank_ic(factor_values, forward_returns):
"""
计算Rank IC
factor_values: 当前期因子值,Series
forward_returns: 未来N期收益,Series
"""
# 去掉缺失值
valid_mask = factor_values.notna() & forward_returns.notna()
fv = factor_values[valid_mask]
fr = forward_returns[valid_mask]
if len(fv) < 30: # 样本太少时IC不稳定
return np.nan
ic, p_value = spearmanr(fv, fr)
return ic
# 滚动计算IC序列
def rolling_ic(factor_df, return_df, window=20):
"""
滚动计算IC序列
factor_df: 每期因子值,index=日期,columns=股票
return_df: 每期收益,index=日期,columns=股票
"""
dates = factor_df.index
ic_series = pd.Series(index=dates, dtype=float)
for i, date in enumerate(dates):
if i < window - 1:
continue
# 取过去window期的数据
fv = factor_df.iloc[i-window+1:i+1].values.flatten()
fr = return_df.iloc[i-window+1:i+1].values.flatten()
ic_series[date] = calc_rank_ic(pd.Series(fv), pd.Series(fr))
return ic_series
我曾经踩过的坑:一开始我直接用日频IC,结果发现IC波动特别大。后来改成周频、月频,稳定性好多了。记住,IC的频率要和你的持仓周期匹配——做中长线就别看日频IC。
3.2 IR分析:因子的「性价比」评估
IR(Information Ratio)是IC的均值除以标准差。说白了,它衡量的是因子预测能力的「稳定性」。一个因子IC均值0.05但标准差0.10,IR=0.5;另一个IC均值0.03但标准差0.02,IR=1.5——后者虽然预测力弱一点,但更稳定,实际用起来反而更靠谱。
我见过很多新手只看IC均值,不看IR。结果选了个IC高但波动大的因子,实盘时收益忽上忽下,根本拿不住。
| 因子 | IC均值 | IC标准差 | IR | 评价 |
|---|---|---|---|---|
| 因子A | 0.06 | 0.12 | 0.50 | 不稳定,慎用 |
| 因子B | 0.04 | 0.03 | 1.33 | 稳定,推荐 |
| 因子C | 0.02 | 0.08 | 0.25 | 弱且不稳定,放弃 |
计算IR的代码:
def calc_ir(ic_series):
"""
计算IR = mean(IC) / std(IC)
"""
mean_ic = ic_series.mean()
std_ic = ic_series.std()
if std_ic == 0 or pd.isna(std_ic):
return np.nan
ir = mean_ic / std_ic
return ir
# 实际使用
ic_series = rolling_ic(factor_df, return_df, window=60) # 用60期滚动
ir_value = calc_ir(ic_series)
print(f"IR = {ir_value:.3f}")
我的经验阈值:IR > 0.5 算及格,IR > 1.0 算不错,IR > 2.0 那基本是捡到宝了。但要注意,IR高不一定代表因子好——如果样本量太小,IR可能被高估。
3.3 分组回测:最直观的「眼见为实」
分组回测是我最依赖的工具。为什么?因为IC和IR都是统计量,但分组回测能画出收益曲线——你一眼就能看出因子有没有分层效果。
具体做法:每期按因子值从大到小分成N组(通常5组或10组),然后计算每组未来的平均收益。如果第1组(因子值最大)收益最高,第5组收益最低,而且中间组单调递减——恭喜你,这是个好因子。
def group_backtest(factor_df, return_df, n_groups=5):
"""
分组回测
factor_df: 每期因子值,index=日期,columns=股票
return_df: 每期收益,index=日期,columns=股票
"""
dates = factor_df.index
group_returns = {i: [] for i in range(1, n_groups+1)}
for date in dates:
fv = factor_df.loc[date].dropna()
fr = return_df.loc[date]
# 按因子值分组
sorted_fv = fv.sort_values(ascending=False)
group_size = len(sorted_fv) // n_groups
for g in range(1, n_groups+1):
start = (g-1) * group_size
end = g * group_size if g < n_groups else len(sorted_fv)
group_stocks = sorted_fv.iloc[start:end].index
# 计算该组平均收益
group_ret = fr[group_stocks].mean()
group_returns[g].append(group_ret)
# 计算累计收益
cum_returns = {}
for g in range(1, n_groups+1):
cum_returns[g] = (1 + pd.Series(group_returns[g])).cumprod()
return cum_returns
# 多空组合收益 = 第1组 - 第N组
def long_short_return(group_returns):
long = pd.Series(group_returns[1])
short = pd.Series(group_returns[n_groups])
ls_return = (1 + long).cumprod() - (1 + short).cumprod()
return ls_return
我曾经犯过的错:有次跑分组回测,发现第1组和第5组收益差距很大,但中间几组乱成一团。我当时觉得「多空收益高就行」,结果实盘一塌糊涂。后来才明白——单调性比极端组差距更重要。中间组乱跳说明因子逻辑不连贯。
3.4 因子收益率的计算:Fama-MacBeth回归
因子收益率,说白了就是「这个因子每单位暴露能带来多少收益」。最经典的方法是Fama-MacBeth两步法回归。
第一步:每期做横截面回归,得到因子暴露的系数(即因子收益率)。
第二步:对时间序列上的系数取均值,做t检验。
为什么用两步法?因为直接做面板回归会忽略残差的时间序列相关性,导致标准误被低估。Fama-MacBeth通过两步走,巧妙地解决了这个问题。
import statsmodels.api as sm
def fama_macbeth(factor_df, return_df):
"""
Fama-MacBeth两步法计算因子收益率
factor_df: 每期因子值,index=日期,columns=股票
return_df: 每期收益,index=日期,columns=股票
"""
dates = factor_df.index
coefficients = []
# 第一步:每期横截面回归
for date in dates:
fv = factor_df.loc[date].dropna()
fr = return_df.loc[date]
# 对齐数据
common_stocks = fv.index.intersection(fr.index)
if len(common_stocks) < 30:
continue
X = sm.add_constant(fv[common_stocks].values)
y = fr[common_stocks].values
model = sm.OLS(y, X).fit()
coefficients.append(model.params[1]) # 因子系数(去掉截距项)
# 第二步:对系数序列做t检验
coef_series = pd.Series(coefficients)
mean_coef = coef_series.mean()
std_coef = coef_series.std()
t_stat = mean_coef / (std_coef / np.sqrt(len(coef_series)))
return {
'factor_return': mean_coef,
't_statistic': t_stat,
'p_value': 2 * (1 - stats.t.cdf(abs(t_stat), df=len(coef_series)-1))
}
我的建议:因子收益率一定要看t统计量。t > 2 才算显著,t > 3 那基本稳了。如果t值只有1.5左右,就算因子收益率再高也别急着用——可能是运气。
3.5 四个指标怎么配合使用?
你可能会问:这四个指标都跑完了,怎么综合判断?我一般按这个顺序来:
- 先看IC和IR:IC均值 > 0.02 且 IR > 0.5,进入下一轮
- 再看分组回测:分组单调性是否明显?多空收益曲线是否平稳向上?
- 最后算因子收益率:t统计量是否显著?因子收益率是否为正?
如果三个环节都通过,这个因子基本可以进入候选池。如果某个环节有问题,我会回头检查数据——是不是有幸存者偏差?是不是未来函数?
记住:单因子分析不是终点,而是起点。它帮你筛掉90%的垃圾因子,剩下的10%才值得你花时间去优化和组合。
嗯,这一章的内容就到这里。代码我都贴出来了,建议你拿自己的数据跑一遍。只有亲手做过,才能真正理解这些指标背后的含义。