因子跨越检验:用 GRS 检验判断新因子是否真有增量信息
你造了一个候选新因子,回测漂亮——但它到底是真增量,还是旧模型(比如市场因子)早就解释过的东西换了个皮?因子跨越检验(spanning test)干的就是这件事:检验「新因子的载荷是否联合为零」。本文用 Huberman-Kandel / GRS 型联合 F 检验,在 8 个测试组合上跑两个对照——冗余因子(F=0.19,p=0.99,不拒绝H0)vs 真增量因子(F=6.11,p≈0,拒绝H0,真实载荷 0.5 被还原成 0.51)——把「新因子有没有增量信息」从嘴炮变成可计算的统计判决。附完整 Python 与六类真实陷阱(中阶)。
因子库里最贵的错觉之一:把旧东西换皮,当成新发现。
你辛苦挖出一个因子,IC 漂亮、分层单调、多空年化 20%。然后有人问:「它跟市值、价值、动量相关多少?」你一算,相关性 0.85。那它根本不是新因子——它只是旧模型里早就有的一坨东西,被你换了个名字重新打包。
怎么用统计把这事钉死?答案是 因子跨越检验(spanning test)。它的零假设很直白:新因子没有增量信息 = 所有资产对新因子的载荷联合为零。要么被旧模型「跨越(span)」了,要么真带来了新东西。本文用 GRS 型联合 F 检验,把判决做成可运行的数字。
一、设定:已有旧模型,来了候选新因子#
设我们有一个旧因子模型(本文只有市场因子 ),和 个测试组合(或资产)的超额收益 。现在来了一个候选新因子 。对每个资产 做回归:
- 是资产 对新因子的载荷
- 跨越检验的零假设:(新因子对所有资产都无载荷 = 无增量)
- 备择:至少有一个
如果 成立,加不加 都一样,旧模型已经「跨越」了它能解释的全部; 只是冗余噪声。如果拒绝 ,说明 解释了旧模型解释不了的变动——它是真增量。
二、统计量:受限 vs 无约束的残差平方和之比#
分别跑两个回归,比较残差平方和(SSR):
- 受限模型:,SSR
- 无约束模型:,SSR
加一个新因子若真有用,SSR 应该明显大于 SSR。把它们做成联合 F 统计量:
在 下 。 是限制个数(=资产数), 是无约束参数数(截距+市场+新因子=3)。p 值小 → 拒绝 → 新因子有增量。
三、跑数据:两个对照场景#
我们造 8 个测试组合(K=8),都由市场因子 + 特质驱动,观测 240 个月(T=240)。F 临界值(5%, df=(8,237)) = 1.978。
场景 A — 候选因子是纯噪声(与资产收益正交,应被跨越):
def spanning_test(R, mkt, g):
Tt, Kk = R.shape
Xr = np.column_stack([np.ones(Tt), mkt]) # 受限
Xu = np.column_stack([np.ones(Tt), mkt, g]) # 无约束
def ssr(X, y):
b, *_ = np.linalg.lstsq(X, y, rcond=None)
return float((y - X @ b) @ (y - X @ b))
SSR_r = sum(ssr(Xr, R[:, i]) for i in range(Kk))
SSR_u = sum(ssr(Xu, R[:, i]) for i in range(Kk))
F = ((SSR_r - SSR_u) / Kk) / (SSR_u / (Tt - 3))
pval = 1.0 - stats.f.cdf(F, Kk, Tt - 3)
c = np.array([np.linalg.lstsq(Xu, R[:, i], rcond=None)[0][2]
for i in range(Kk)])
return F, pval, cpython场景 B — 候选因子对部分组合有真实载荷(真增量):
g_B = rng.normal(0.005, 0.025, T) # 独立新因子
R_B = R.copy()
for i in range(K):
if i % 2 == 0: # 第 1/3/5/7 组合对 g_B 有 +0.5 真实载荷
R_B[:, i] += 0.5 * g_Bpython结果:
| 场景 | F 统计量 | p 值 | 判断 |
|---|---|---|---|
| A(冗余因子) | 0.189 | 0.9923 | 不拒绝 H0(无增量) |
| B(增量因子) | 6.108 | ≈0.0000 | 拒绝 H0(有增量) |
场景 A 的 F 远低于临界值 1.978,冗余因子没贡献任何解释力;场景 B 的 F 是临界值的 3 倍,p 值直接贴 0——新因子被判定为真增量。


四、载荷长什么样:把判决拆回资产层面#
检验通过,不代表每个资产都吃新因子。看各资产对新因子的载荷 :
- 场景 A:各资产 绝对值均值 0.071,正负乱跳,贴近 0——冗余因子的典型签名
- 场景 B:偶数组合(真实载荷 0.5)还原出 0.510,奇数组合 −0.017(本就无载荷)——检验不仅判了「有增量」,还把增量落到了对的资产上

再用 R² 增量交叉验证:加入新因子后,场景 A 平均 R² 只多了 0.0014(几乎为 0),场景 B 多了 0.0471——两个角度(联合 F 与 R² 增量)互相印证。

五、六类真实陷阱(实战必看)#
- T 不够大、K 太大:GRS/F 检验的近似要求 。本文 T=240、K=8 很宽裕;若你拿 60 个月数据测 50 个资产,F 临界值本身就不稳,结论不可信。资产数最好 ≤ T/10。
- 一次加多个新因子:本文只加 1 个。若一次加 个新因子,无约束参数 ,自由度变成 ,SSR 的拆分也要按 个载荷联合判。漏改自由度会直接算错 p 值。
- 新因子与旧因子高相关:若 跟 相关系数 0.9,无约束回归共线性爆炸, 估计方差飙升,F 检验的 power 暴跌——你会「假不拒绝」,把真增量漏掉。先查新因子和旧因子的相关,太高就先做正交化再测。
- 数据窥探 / 多重检验:你测了 100 个候选因子,只报那个 p<0.05 的?在 100 次检验下,光靠随机就有约 5 个会「显著」。必须做 Bonferroni 或 FDR(BH)校正,否则跨越检验变成 p-hacking。
- 测试资产样本内挑选:若 8 个测试组合是「专门挑出能拒绝 H0 的那几个」,结论严重样本内偏差。正确做法用独立的、事先固定的测试资产(如标准规模-价值组合网格),不要按结果反选。
- 因子的 look-ahead:候选因子 若用未来信息构造(比如用期末才公布的财务数据去匹配月初收益),F 检验会被人为抬高,把实际上不可交易的「未来信息」误判成增量。因子与收益必须严格对齐同一信息集。
六、小结#
因子跨越检验把「新因子到底新不新」从主观争论变成联合 F 判决:受限模型([1,mkt])vs 无约束模型([1,mkt,g])的 SSR 之比,在 H0 下服从 。本文实证干净利落——冗余因子 F=0.19、p=0.99(不拒绝),真增量因子 F=6.11、p≈0(拒绝),且把 0.5 的真实载荷还原成 0.51。记住它的位置:它是「这个因子值不值得进模型」的守门员,不是「这个因子能不能赚钱」的保证书。 过了跨越检验,只说明它带来了旧模型没有的信息维度,至于实盘赚不赚,那是另一场检验。
代码与图表均由 Python(numpy + scipy + matplotlib)真实计算,数据为带结构的可复现合成面板,非占位符。