拿到一个 t 值高达 38 的回测结果,你第一反应是什么?兴奋?还是——先数一数,这个样本量是不是在骗你?这篇文章讲的,就是那种"样本量看起来很大、实际上很小"的经典陷阱。
现象
用全样本做回测,t 值高得离谱:
t-statistic = 38.4
样本量 = 12,500
p-value = 0.0000
38 的 t 值意味着"这个结果几乎不可能是巧合",直觉上你会觉得找到了铁证。但问题在于:这 12,500 条信号,真的是 12,500 个彼此独立的观测吗?
根因
不是。 同一时间点上的所有对象,共享了共同的市场因子(宏观环境、行业波动、情绪周期……)。它们在同一个瞬间一起涨、一起跌,本质上是"高度相关"的,不是独立的观测。
把成千上万条信号当成独立样本,会把微弱信号严重说大。池化之后的 t 值可能膨胀 9~25 倍。也就是说,一个真实的 t 值可能是 2 左右(还勉强),但被错当独立样本一算,就变成了 30 多。
站得住的显著性检验,样本量不该按"有多少行"来算,而该按"有多少个彼此独立的时刻"来算:
# 错误:把所有行当独立样本
t = ttest_1samp(df["ret"], 0) # 行数 = 12500,t = 38
# 正确:先按时间点聚合,再检验
daily = df.groupby("date")["ret"].mean() # 时刻数 = 250
t = ttest_1samp(daily, 0) # 有效样本量 = 250
有效样本量从 12,500 掉到 250,t 值立刻回到理性范围——这才是"诚实"的统计量。
解决
两件事必须同时做:
第一,结论必须同时报告"按时间点聚合"的口径。同一时点的所有信号压缩成一个值(比如均值或中位数),再对这张"按时刻"的表做检验。
第二,用配对按时间点超额作为唯一诚实的显著性检验。做法是:对每一个时间点,分别计算"信号组的表现"和"同时点非信号组的表现",两者相减得到逐时点的超额,再对这条超额序列做检验。
# 逐时点配对超额
excess = (group_by_date(df, signals=True)["ret"]
- group_by_date(df, signals=False)["ret"])
t = ttest_1samp(excess.dropna(), 0) # 有效样本量 = 时刻数
这样做的好处是,它把"共同因子的涨跌"从两头同时扣掉了——不管市场当天是涨是跌,你比较的都是"同一时刻信号组相对非信号组的差"。剩下的才是信号本身贡献的部分。
延伸与预防
这个陷阱的通用形态是:样本之间是否共享了某个未被建模的共同因素。只要存在这种共同冲击,样本就不再独立,直接做检验一定会高估显著性。
预防的思路:
- 先问"独立性从哪来"。做任何统计检验前,先确认观测之间是否彼此影响、是否共享时点、是否来自同一个簇。
- 选择合适的聚合单位。时间序列按时刻聚合,分组实验按组聚合,面板数据用聚类稳健标准误(cluster-robust standard error)。
- 报告有效样本量,而不是行数。一张表里写着 12,500 行,但真正独立的时刻只有 250 个,这个差距必须让读者看到。
一句话:样本量不是"有多少行",而是"有多少个彼此独立的时刻"。 记住这句,能挡掉一大半自欺欺人的"显著发现"。