4. 单因子分析框架

单因子分析,说白了就是给每个候选因子做「体检」。

我见过不少新手,一口气挖了几百个因子,直接扔进模型里跑。结果呢?过拟合严重,实盘一塌糊涂。你想想看,连单个因子有没有预测能力都不清楚,组合起来怎么可能稳定?

所以,这一章我们老老实实把单因子分析的框架搭起来。我个人习惯把它拆成四个步骤:因子构建 → IC/IR 分析 → 分组回测 → 因子收益率计算。每一步都有坑,咱们一个一个踩过去。

核心观点:单因子分析不是为了选最好的因子,而是为了理解每个因子的「脾气秉性」——什么时候有效,什么时候失效,跟哪些因子相关。

单因子分析框架流程图 单因子分析框架 因子构建 IC / IR 分析 分组回测 因子收益率 各环节核心产出: • 因子构建 → 标准化后的因子值矩阵(含行业/市值中性化) • IC/IR 分析 → 因子预测能力的统计检验(Rank IC、IC_IR) • 分组回测 → 多空组合净值曲线、单调性检验 • 因子收益率 → 经风险调整后的纯因子收益(Fama-MacBeth / 回归法)

4.1 因子构建流程

因子构建不是简单的「拿数据除一下」。我刚开始做的时候,直接从数据库拉出市盈率、市净率,算个倒数就当成因子了。结果回测曲线漂亮得不行,一查才发现——前向偏差,用了未来数据。

一个规范的因子构建流程,至少包含这几步:

  1. 数据清洗:去极值、补缺失、处理复权。尤其是复权,用错了等于白干。
  2. 因子计算:按公式算出原始因子值。注意截面和时序的区别。
  3. 标准化处理:包括去极值(MAD法或百分位法)、中性化(行业、市值)、标准化(Z-score)。
  4. 滞后对齐:确保因子值和未来收益严格对齐。T 日因子对应 T+1 日收益。

我的习惯:每次构建完因子,先做一次「未来数据检查」。具体做法是:随机挑几只股票,手动核对因子值和下期收益的时间戳。虽然笨,但管用。

举个例子,一个简单的动量因子构建代码大概长这样:

import pandas as pd
import numpy as np

def calc_momentum_factor(price_df, window=20):
    """
    计算动量因子:过去 window 天的累计收益率
    输入:price_df - 股票价格矩阵 (日期 x 股票)
    输出:factor_df - 因子值矩阵
    """
    # 计算收益率
    ret = price_df.pct_change()
    # 滚动求和
    momentum = ret.rolling(window=window).sum()
    # 滞后一期,避免前向偏差
    momentum = momentum.shift(1)
    return momentum

# 去极值(MAD法)
def winsorize_mad(factor, n=5):
    median = factor.median()
    mad = (factor - median).abs().median()
    upper = median + n * mad
    lower = median - n * mad
    return factor.clip(lower, upper)

# 标准化
def standardize(factor):
    return (factor - factor.mean()) / factor.std()

注意:去极值和标准化一定要在截面(每个时间点)上做,而不是在整个时序上做。否则会引入未来信息。我曾经在这个问题上栽过跟头,回测收益凭空多了 3%。

4.2 因子 IC / IR 分析

因子构建完了,怎么知道它有没有预测能力?IC(Information Coefficient)就是干这个的。

IC 的定义很简单:因子值与未来一期收益的相关系数。常用的是 Spearman 秩相关系数(Rank IC),因为它对极端值不敏感。

IR(Information Ratio)则是 IC 的均值除以标准差,衡量的是因子预测能力的稳定性。

指标 公式 含义 经验阈值
Rank IC ρ(rank(factor), rank(return)) 因子排序与收益排序的相关性 |IC| > 0.02 视为有预测力
IC_IR mean(IC) / std(IC) IC 的「性价比」 IC_IR > 0.5 算不错
累计 IC cumsum(IC) IC 的长期趋势 趋势向上且稳定

计算 IC 的代码也很直接:

def calc_rank_ic(factor_df, return_df):
    """
    计算每日 Rank IC
    factor_df: 因子值 (日期 x 股票)
    return_df: 未来一期收益 (日期 x 股票)
    """
    # 确保对齐
    assert factor_df.index.equals(return_df.index)
    assert factor_df.columns.equals(return_df.columns)
    
    # 每日计算 Spearman 相关系数
    ic_series = pd.Series(index=factor_df.index, dtype=float)
    for date in factor_df.index:
        f = factor_df.loc[date]
        r = return_df.loc[date]
        # 去掉缺失值
        mask = f.notna() & r.notna()
        if mask.sum() < 30:  # 太少股票就算了
            continue
        ic_series[date] = f[mask].corr(r[mask], method='spearman')
    return ic_series

# 计算 IR
ic = calc_rank_ic(factor, forward_return)
ic_ir = ic.mean() / ic.std()
print(f"IC 均值: {ic.mean():.4f}, IR: {ic_ir:.4f}")

我个人习惯把 IC 画成时间序列图,一眼就能看出因子有没有「失效期」。如果 IC 在某个时间段持续为负,说明因子发生了翻转,这时候就要警惕了。

4.3 分组回测(分层回测)

IC 分析告诉你因子「有没有用」,分组回测告诉你「怎么用」。

具体做法是:在每个时间截面,按因子值把股票分成 N 组(通常是 5 组或 10 组),等权或市值加权持有下一期。然后观察各组收益是否单调。

为什么强调单调性?

  • 如果第 1 组到第 5 组收益严格递减,说明因子有很好的区分度。
  • 如果中间有交叉,比如第 3 组比第 2 组收益还高,那因子可能不是线性的。
  • 如果多空组合(第 1 组 - 第 5 组)收益显著为正,说明因子有超额收益。

关键点:分组回测一定要做「多空组合」的净值曲线。单看多头组合的收益,你分不清是因子贡献还是市场 Beta 贡献。多空组合剥离了市场风险,才是因子的真实 Alpha。

分组回测的代码框架:

def group_backtest(factor_df, return_df, n_groups=5):
    """
    分层回测
    返回:各组每日收益、多空组合收益
    """
    group_returns = pd.DataFrame(index=return_df.index, 
                                 columns=range(n_groups))
    for date in factor_df.index:
        f = factor_df.loc[date]
        r = return_df.loc[date]
        # 分组
        groups = pd.qcut(f, n_groups, labels=False, duplicates='drop')
        # 计算每组等权收益
        for g in range(n_groups):
            mask = (groups == g)
            if mask.sum() > 0:
                group_returns.loc[date, g] = r[mask].mean()
    
    # 多空组合:第0组 - 最后一组
    long_short = group_returns[0] - group_returns[n_groups-1]
    return group_returns, long_short

# 计算累计净值
group_ret, ls_ret = group_backtest(factor, forward_return)
ls_cum = (1 + ls_ret).cumprod()

避坑指南:我曾经遇到一个因子,分组收益完全单调,但多空组合的夏普比只有 0.3。为什么?因为多头和空头都在亏钱,只是多头亏得少一点。这种因子其实没有实际价值。所以,一定要同时看多头组合的绝对收益。

4.4 因子收益率计算

分组回测给了我们直观的收益表现,但还不够严谨。因子收益率,指的是剥离了其他风险因子之后,该因子带来的纯收益。

常用的方法有两种:

  1. Fama-MacBeth 回归法:两步法,先截面回归得到因子溢价,再时序平均。
  2. 多空组合回归法:把多空组合收益对市场因子、风格因子做回归,截距项就是纯因子收益。

我个人更推荐第二种,因为它更直观,而且容易做风险调整。

import statsmodels.api as sm

def pure_factor_return(ls_returns, market_returns, factor_exposures):
    """
    计算纯因子收益(经市场风险调整后)
    ls_returns: 多空组合收益序列
    market_returns: 市场收益序列
    factor_exposures: 其他风格因子暴露(如市值、动量)
    """
    X = pd.DataFrame({
        'market': market_returns,
        'size': factor_exposures['size'],
        'value': factor_exposures['value']
    })
    X = sm.add_constant(X)
    y = ls_returns
    
    model = sm.OLS(y, X, missing='drop').fit()
    # 截距项就是纯因子收益
    alpha = model.params['const']
    t_stat = model.tvalues['const']
    return alpha, t_stat, model

注意:因子收益率计算对频率很敏感。日频因子收益率通常很小,年化时需要乘以 sqrt(252)。但要注意,如果因子换手率很高,日频年化会高估实际收益。我一般同时看日频和周频的结果,取个保守值。

4.5 小结:单因子分析的「体检报告」

做完上面四步,你应该能产出一份完整的因子体检报告:

检查项 合格标准 常见问题
IC 均值 |IC| > 0.02,且显著不为零 IC 太小,或正负摇摆
IC_IR > 0.5 IR 太低,因子不稳定
分组单调性 各组收益严格单调 中间组交叉,非线性
多空收益 年化 > 5%,夏普 > 1 多空收益来自空头亏损
纯因子 Alpha t 统计量 > 2 Alpha 被其他因子解释

嗯,这套框架我用了好几年,从 A 股到,从日频到高频,基本都能覆盖。你刚开始做因子研究的时候,建议每个因子都走一遍这个流程。虽然慢,但踏实。

记住一句话:没有经过单因子检验的因子,不配进入多因子模型。