3. 单因子分析基础:IC分析、IR分析、分组回测、因子收益率的计算

做量化这几年,我见过太多人一上来就搞复杂的机器学习模型,结果因子本身有没有预测能力都没搞清楚。说白了,单因子分析就是给每个因子做「体检」——它到底能不能赚钱?稳定性如何?有没有逻辑支撑?

这一章,我带你把这四个核心工具过一遍:IC分析、IR分析、分组回测、因子收益率计算。它们就像医生的听诊器、血压计、CT机和化验单,缺一不可。

核心观点:单因子分析不是走流程,而是帮你建立对因子的「直觉」。我习惯先跑IC,再看分组,最后算收益率——顺序别搞反了。

单因子分析 IC分析 IR分析 分组回测 因子收益率 Rank IC / Normal IC IC序列 / ICIR IR = mean(IC) / std(IC) 多空组合收益 分层单调性检验 Fama-MacBeth回归 四个维度交叉验证,缺一不可

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 四个指标怎么配合使用?

你可能会问:这四个指标都跑完了,怎么综合判断?我一般按这个顺序来:

  1. 先看IC和IR:IC均值 > 0.02 且 IR > 0.5,进入下一轮
  2. 再看分组回测:分组单调性是否明显?多空收益曲线是否平稳向上?
  3. 最后算因子收益率:t统计量是否显著?因子收益率是否为正?

如果三个环节都通过,这个因子基本可以进入候选池。如果某个环节有问题,我会回头检查数据——是不是有幸存者偏差?是不是未来函数?

记住:单因子分析不是终点,而是起点。它帮你筛掉90%的垃圾因子,剩下的10%才值得你花时间去优化和组合。

嗯,这一章的内容就到这里。代码我都贴出来了,建议你拿自己的数据跑一遍。只有亲手做过,才能真正理解这些指标背后的含义。