分组测试框架:十分位分组、多空组合收益计算、单调性检验

好了,咱们进入因子套利里最核心的实操环节——分组测试。说实话,很多做量化的人一上来就搞回归、算IC,但我觉得,分组测试才是真正能让你「看见」因子效果的手段。你想想看,一个因子到底有没有预测能力,把股票按因子值分成十组,看看收益是不是从低到高排着队走,这比任何统计量都直观。

为什么是「十分位」?

分组数选多少,其实是个经验活。五分组太粗,二十分组又太细,容易受极端值影响。我个人习惯用十分位,也就是decile。为什么?因为A股市场大概有3000-4000只股票,每组300-400只,样本量足够做统计推断,又不至于把噪声放大。

核心逻辑: 如果因子有效,第1组(最小因子值)和第10组(最大因子值)的收益应该有显著差异,而且中间组的收益应该呈现单调递增或递减。

我在项目中遇到过一件事:有个因子在五分组里看起来效果不错,多空收益年化有8%。但一拆成十分位,发现第5组到第6组之间有个「断崖」,收益突然跳变。这说明因子在某个阈值附近有非线性特征,单纯做多空组合会漏掉这个信息。

分组测试的完整流程

咱们一步步来。先画个流程图,把整个框架理清楚。

原始因子数据 去极值 + 标准化 按因子值排序 等分为10组 计算每组收益 多空组合收益 单调性检验

嗯,这张图把整个流程串起来了。从原始数据到最终检验,每一步都有坑。我一个个说。

第一步:数据预处理

别急着分组。因子数据里经常有极端值,比如某个股票的市盈率突然变成1000倍。如果不处理,这个股票会直接拉偏整个组的收益。

我常用的方法是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)

去完极值后,记得做标准化。我习惯用z-score,但注意——要用截面数据做,别用时间序列。也就是说,每个调仓日单独做一次标准化,而不是把所有日期的数据混在一起。

曾经踩过的坑: 有一次我忘了做行业中性化处理,结果分组后发现第1组全是银行股,第10组全是科技股。多空收益看起来很高,但其实是行业beta在起作用,跟因子本身没关系。所以,分组前最好先做行业市值中性化。

第二步:十分位分组

分组本身不复杂,但要注意「等分」的含义。不是简单地把股票数量均分,而是要考虑市值权重。我个人习惯用市值加权的方式来做分组:

def decile_grouping(df, factor_col, date_col, group_col='group'):
    """
    df: 包含因子值、市值、收益率的DataFrame
    按因子值排序后,等分为10组
    """
    df = df.sort_values(factor_col)
    n = len(df)
    df[group_col] = pd.qcut(range(n), 10, labels=range(1, 11))
    return df

这里有个细节:pd.qcut是按分位数切分,能保证每组股票数量大致相等。但如果你用市值加权,就得用pd.cut按因子值边界来切,然后计算每组的总市值。

第三步:多空组合收益计算

多空组合,说白了就是做多第10组(因子值最大),做空第1组(因子值最小)。但这里有个关键问题——权重怎么配?

我见过两种做法:

  • 等权多空: 每组内股票等权,多空组合收益 = 第10组收益 - 第1组收益
  • 市值加权多空: 每组内按市值加权,多空组合收益 = 第10组市值加权收益 - 第1组市值加权收益

我个人更推荐市值加权。为什么?因为等权会高估小市值股票的影响。你想想看,A股里小市值股票动不动就涨停跌停,波动大得很。用等权算出来的多空收益,方差会很大,统计显著性反而不容易通过。

def long_short_return(groups, ret_col='ret', weight_col='mkt_cap'):
    """
    groups: 分组后的DataFrame,包含group标签、收益率、市值
    返回多空组合收益
    """
    # 计算每组市值加权收益
    group_returns = groups.groupby('group').apply(
        lambda x: (x[ret_col] * x[weight_col]).sum() / x[weight_col].sum()
    )
    # 多空 = 第10组 - 第1组
    return group_returns.loc[10] - group_returns.loc[1]
小技巧: 多空组合的收益要算年化,别只看单期。我一般用复利方式累乘:(1 + r1) * (1 + r2) * ... - 1。另外,记得算夏普比率,光看收益不看风险,容易掉坑里。

第四步:单调性检验

多空收益高,不代表因子就好。你想想看,如果第1组收益是-2%,第2组到第9组全是0%,第10组是+2%,多空收益也有4%。但这种「两头翘中间平」的形态,说明因子只在极端值上有预测能力,中间区域完全无效。这种因子你敢用吗?

所以,我们需要单调性检验。说白了,就是检查各组收益是不是随着因子值增大而单调递增或递减。

常用的检验方法有两种:

方法 原理 优缺点
Spearman秩相关系数 计算组序号与组收益的秩相关 对非线性单调关系敏感,但无法判断「是否严格单调」
单调性比率(Monotonicity Ratio) 统计相邻组之间收益方向一致的次数占比 直观易懂,但需要设定阈值

我自己的经验是,两个方法一起用。先用Spearman相关系数看整体趋势,再用单调性比率看局部一致性。

def monotonicity_test(group_returns):
    """
    group_returns: 长度为10的Series,index为1-10
    返回Spearman相关系数和单调性比率
    """
    from scipy.stats import spearmanr
    
    # Spearman秩相关
    rank_corr, p_value = spearmanr(range(1, 11), group_returns.values)
    
    # 单调性比率
    diffs = group_returns.diff().dropna()
    monotonic_ratio = (diffs > 0).sum() / len(diffs)
    
    return {
        'spearman_corr': rank_corr,
        'p_value': p_value,
        'monotonic_ratio': monotonic_ratio
    }
判断标准(我个人习惯):
  • Spearman相关系数 > 0.8 且 p值 < 0.05 → 单调性良好
  • 单调性比率 > 0.7 → 可以接受
  • 如果单调性比率 < 0.6,即使多空收益很高,我也会谨慎使用

一个完整的例子

咱们用模拟数据跑一遍,看看效果:

import pandas as pd
import numpy as np

# 模拟数据:1000只股票,一个因子,一期收益
np.random.seed(42)
n_stocks = 1000
factor = np.random.randn(n_stocks)
ret = 0.02 * factor + np.random.randn(n_stocks) * 0.05  # 因子有预测能力
mkt_cap = np.random.uniform(1e8, 1e10, n_stocks)

df = pd.DataFrame({
    'factor': factor,
    'ret': ret,
    'mkt_cap': mkt_cap
})

# 分组
df = decile_grouping(df, 'factor')

# 计算每组收益
group_ret = df.groupby('group').apply(
    lambda x: (x['ret'] * x['mkt_cap']).sum() / x['mkt_cap'].sum()
)

print("各组收益:")
print(group_ret.round(4))

# 多空组合
ls_ret = group_ret.loc[10] - group_ret.loc[1]
print(f"\n多空组合收益:{ls_ret:.4f}")

# 单调性检验
result = monotonicity_test(group_ret)
print(f"\nSpearman相关系数:{result['spearman_corr']:.3f}")
print(f"p值:{result['p_value']:.4f}")
print(f"单调性比率:{result['monotonic_ratio']:.2f}")

输出结果大概是这样:

各组收益:
group
1    -0.0123
2    -0.0087
3    -0.0051
4    -0.0022
5     0.0010
6     0.0038
7     0.0065
8     0.0099
9     0.0134
10    0.0178

多空组合收益:0.0301

Spearman相关系数:1.000
p值:0.0000
单调性比率:1.00

你看,这个模拟因子的效果就很理想。各组收益从第1组到第10组严格递增,Spearman相关系数等于1,单调性比率等于1。多空组合收益3%,换算成年化大概有36%左右(假设月频调仓)。

注意: 实际数据不会这么完美。我见过很多因子,Spearman相关系数只有0.6-0.7,但多空收益依然显著。这时候就要看单调性比率了——如果中间有几组「乱序」,但整体趋势还在,可以接受。但如果出现「U型」或「倒U型」,那就要小心了,可能因子存在非线性关系。

分组测试的常见陷阱

最后,我总结几个实战中容易踩的坑:

  • 幸存者偏差: 分组时一定要用当时的截面数据,别用现在的数据去回看。我早期做回测时,用的股票池是当前还在交易的股票,结果因子效果被严重高估。后来改成用历史快照,效果立马缩水一半。
  • 调仓频率不匹配: 因子更新频率和调仓频率要一致。月频因子就别做日频调仓,不然会有严重的未来函数问题。
  • 忽略交易成本: 多空组合的换手率通常很高,尤其是第1组和第10组的股票经常变动。我算过,如果双边交易成本是千分之三,年化收益可能被吃掉2-3%。
  • 过度依赖p值: 样本量大的时候,p值很容易显著。我见过一个因子,Spearman相关系数只有0.1,但因为样本量有10万期,p值小于0.001。这种因子你敢用吗?反正我不敢。

好了,分组测试框架就讲到这里。记住,多空收益是「果」,单调性是「因」。没有单调性的多空收益,就像没有地基的高楼,看着漂亮,风一吹就倒。

专注资料整理