分组测试框架:十分位分组、多空组合收益计算、单调性检验
好了,咱们进入因子套利里最核心的实操环节——分组测试。说实话,很多做量化的人一上来就搞回归、算IC,但我觉得,分组测试才是真正能让你「看见」因子效果的手段。你想想看,一个因子到底有没有预测能力,把股票按因子值分成十组,看看收益是不是从低到高排着队走,这比任何统计量都直观。
为什么是「十分位」?
分组数选多少,其实是个经验活。五分组太粗,二十分组又太细,容易受极端值影响。我个人习惯用十分位,也就是decile。为什么?因为A股市场大概有3000-4000只股票,每组300-400只,样本量足够做统计推断,又不至于把噪声放大。
我在项目中遇到过一件事:有个因子在五分组里看起来效果不错,多空收益年化有8%。但一拆成十分位,发现第5组到第6组之间有个「断崖」,收益突然跳变。这说明因子在某个阈值附近有非线性特征,单纯做多空组合会漏掉这个信息。
分组测试的完整流程
咱们一步步来。先画个流程图,把整个框架理清楚。
嗯,这张图把整个流程串起来了。从原始数据到最终检验,每一步都有坑。我一个个说。
第一步:数据预处理
别急着分组。因子数据里经常有极端值,比如某个股票的市盈率突然变成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,但注意——要用截面数据做,别用时间序列。也就是说,每个调仓日单独做一次标准化,而不是把所有日期的数据混在一起。
第二步:十分位分组
分组本身不复杂,但要注意「等分」的含义。不是简单地把股票数量均分,而是要考虑市值权重。我个人习惯用市值加权的方式来做分组:
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%左右(假设月频调仓)。
分组测试的常见陷阱
最后,我总结几个实战中容易踩的坑:
- 幸存者偏差: 分组时一定要用当时的截面数据,别用现在的数据去回看。我早期做回测时,用的股票池是当前还在交易的股票,结果因子效果被严重高估。后来改成用历史快照,效果立马缩水一半。
- 调仓频率不匹配: 因子更新频率和调仓频率要一致。月频因子就别做日频调仓,不然会有严重的未来函数问题。
- 忽略交易成本: 多空组合的换手率通常很高,尤其是第1组和第10组的股票经常变动。我算过,如果双边交易成本是千分之三,年化收益可能被吃掉2-3%。
- 过度依赖p值: 样本量大的时候,p值很容易显著。我见过一个因子,Spearman相关系数只有0.1,但因为样本量有10万期,p值小于0.001。这种因子你敢用吗?反正我不敢。
好了,分组测试框架就讲到这里。记住,多空收益是「果」,单调性是「因」。没有单调性的多空收益,就像没有地基的高楼,看着漂亮,风一吹就倒。