3. 单因子分析基础:因子值计算、因子分组回测、多空组合收益与IC/IR分析

各位同学,欢迎来到《量化选股因子实战应用》的第一章。今天咱们聊点实在的——单因子分析。说白了,就是检验你手里这个因子到底有没有用。

我刚开始做量化那会儿,也犯过傻。拿到一个因子,觉得逻辑挺漂亮,直接扔进模型里跑。结果呢?回测曲线惨不忍睹。后来我才明白,因子分析不是拍脑袋,而是有一套标准流程的。今天我就把这套流程拆开揉碎了讲给你听。

核心逻辑:单因子分析 = 因子值计算 + 分组回测 + 多空组合 + IC/IR检验。四步走完,你才能说“这个因子我摸透了”。

单因子分析知识体系 单因子分析 因子值计算 分组回测(5组/10组) 多空组合收益 IC/IR分析 标准化/中性化 去极值处理 Rank IC / 累计IC IR

3.1 因子值计算——别让数据坑了你

因子值计算,听起来简单吧?把公式套进去就行。但这里有个坑——数据预处理。我见过太多人直接拿原始数据算因子,结果跑出来的IC值忽高忽低,根本没法用。

举个例子,你算一个“市盈率倒数”因子。原始数据里可能有极端值,比如某只股票市盈率1000倍,或者负值。这些异常值会严重扭曲分组结果。所以第一步,去极值

我的习惯做法:用MAD(中位数绝对偏差)法去极值,比3-sigma更稳健。尤其是金融数据,分布往往有厚尾,MAD更靠谱。

第二步,标准化。把因子值映射到0-1或者Z-score。我一般用Z-score,因为后续做IC分析时,标准化后的因子更容易解释。

import pandas as pd
import numpy as np

def factor_preprocess(factor_series):
    # 去极值:MAD方法
    median = factor_series.median()
    mad = np.abs(factor_series - median).median()
    upper = median + 5 * mad
    lower = median - 5 * mad
    factor_clipped = factor_series.clip(lower, upper)
    
    # 标准化:Z-score
    factor_z = (factor_clipped - factor_clipped.mean()) / factor_clipped.std()
    return factor_z

嗯,这里要注意:中性化处理也很关键。比如市值因子和很多因子都相关,你不做市值中性化,分组回测时可能把市值效应误认为是因子效应。我一般用回归取残差的方式做中性化。

3.2 分组回测——5组还是10组?

分组回测,说白了就是把股票按因子值从高到低排序,分成N组,然后看每组未来的收益表现。如果高分组收益显著高于低分组,说明因子有区分度。

我个人习惯先做5组,快速验证因子方向。如果5组有单调性,再细化到10组看细节。为什么?因为5组样本量大,统计更稳定。10组虽然精细,但每组股票数量少,容易受个别股票影响。

分组数 优点 缺点 适用场景
5组 统计稳定,不易受噪声干扰 区分度较粗 初步筛选因子
10组 细节丰富,能看到边际效应 每组样本少,波动大 精细调参、因子增强

我曾经在做一个动量因子时,5组结果很漂亮,单调性完美。但细化到10组后,发现第8组和第9组收益倒挂了。后来一查,是某只妖股在作怪。所以你看,分组回测不是走形式,而是帮你发现问题的

def group_backtest(factor_df, return_df, n_groups=5):
    """
    factor_df: 因子值,index为日期,columns为股票
    return_df: 下期收益,index为日期,columns为股票
    """
    results = {}
    for date in factor_df.index:
        # 按因子值排序分组
        factors = factor_df.loc[date].dropna()
        groups = pd.qcut(factors, n_groups, labels=False)
        
        # 计算每组等权收益
        group_returns = {}
        for g in range(n_groups):
            stocks = factors[groups == g].index
            group_returns[g] = return_df.loc[date, stocks].mean()
        
        results[date] = group_returns
    
    return pd.DataFrame(results).T

避坑指南:我曾经在分组时直接用因子值排序,忽略了行业分布。结果高分组全是科技股,低分组全是银行股。收益差异其实是行业效应,不是因子效应。所以分组时最好做行业中性化,或者按行业分层分组。

3.3 多空组合收益——真正的alpha来源

多空组合,就是做多高分组、做空低分组。这个指标能直接告诉你:剔除市场整体涨跌后,因子能赚多少钱

你想想看,如果高分组涨了10%,但市场整体涨了15%,那因子其实是负贡献。多空组合收益把市场风险对冲掉了,剩下的才是因子真正的alpha。

计算方式很简单:

# 假设group_returns是分组收益DataFrame
long_short_ret = group_returns.iloc[:, -1] - group_returns.iloc[:, 0]
cumulative_ret = (1 + long_short_ret).cumprod()

# 年化收益率
annual_ret = cumulative_ret.iloc[-1] ** (252 / len(cumulative_ret)) - 1
# 年化波动率
annual_vol = long_short_ret.std() * np.sqrt(252)
# 夏普比率
sharpe = annual_ret / annual_vol

我一般要求多空组合的年化夏普比率至少大于1,才考虑把这个因子纳入模型。低于0.5的因子,说实话,噪声成分太大。

3.4 IC/IR分析——因子的“体检报告”

IC(信息系数)衡量的是因子值与未来收益的相关性。IR(信息比率)则是IC的均值除以标准差,衡量因子的稳定性。

说白了,IC告诉你因子准不准,IR告诉你因子稳不稳。一个因子IC很高但IR很低,说明它时灵时不灵,你敢用吗?

我常用的IC有两种:

  • Pearson IC:因子值与收益的线性相关系数。对异常值敏感。
  • Rank IC:因子值排名与收益排名的相关系数。更稳健,我推荐用这个。
def calculate_ic(factor_df, return_df, method='rank'):
    ic_series = pd.Series(index=factor_df.index)
    for date in factor_df.index:
        f = factor_df.loc[date].dropna()
        r = return_df.loc[date, f.index]
        if method == 'rank':
            ic_series[date] = f.corr(r, method='spearman')
        else:
            ic_series[date] = f.corr(r, method='pearson')
    return ic_series

# IR = mean(IC) / std(IC)
ir = ic_series.mean() / ic_series.std()

我的经验阈值:

  • |IC| > 0.02:有预测能力
  • |IC| > 0.05:预测能力较强
  • IR > 0.5:因子稳定可用
  • IR > 1.0:优秀因子

当然,这些阈值因市场而异。A股市场因子IR普遍偏低,0.3以上就算不错了。

最后,别忘了看IC的累计曲线。如果累计IC稳步上升,说明因子持续有效。如果忽上忽下,甚至长期横盘,那就要警惕了——可能因子已经失效了。

好了,单因子分析的基础框架就这些。记住:因子值计算是基础,分组回测看区分度,多空组合看alpha,IC/IR看稳定性。四步走完,你就能对这个因子有个全面的判断。

一个小技巧:我每次做完单因子分析,都会把结果整理成一张“因子体检表”。包括:分组收益图、多空累计收益曲线、IC时序图、IC分布直方图。一张表看下来,因子好坏一目了然。

专注资料整理