你训练出一个因子,样本内 IC 0.06,回测年化 18%,但你真知道它「为什么」赚钱吗?绝大多数时候你只知道它和历史收益相关——而相关,不等于因果。
经典反例:某地冰淇淋销量和溺水死亡人数高度正相关。难道少吃冰淇淋能救命?当然不是——两者都被「夏天高温」这个共同因素驱动。在量化里,这种伪相关无处不在:两只股票同涨同跌,可能只是都被同一只 ETF 抛售拖累;一个「低波因子」看起来有效,可能只是恰好样本期里价值风格在涨。
如果你要向上游(风控、合规、投委会)解释一个策略「因为什么而赚钱」,或者要评估「某次交易规则改动 / 某条监管政策」到底带来了多少真实的收益增量,你需要的是因果推断,而不仅是相关。本文聚焦其中最实用、落地成本最低的一种方法:双重差分(Difference-in-Differences, DID)。

一、潜在结果框架:什么是「因果效应」#
因果推断的基石是 Rubin 的潜在结果模型(Potential Outcomes)。对第 i 个个体(在量化里可以是一只股票、一个交易日、一个账户),它有两种潜在结果:
- :如果「接受处理」(如被纳入指数、触发某政策、启用新规则)时的结果;
- :如果「不接受处理」时的结果。
我们关心的个体因果效应是 。但残酷的事实是:你永远只能观测到其中一种,另一种被观测到的那个,叫做反事实(counterfactual)。这就是因果推断的根本困难——缺失数据。
所以实际能估计的,是平均处理效应(ATE)或处理组平均处理效应(ATT):
其中 表示接受处理。
二、双重差分:用「两组两期」挤出因果#
单纯比较「处理组处理前 vs 处理后」会混入时间趋势(比如整个市场在涨);单纯比较「处理组 vs 控制组」会混入组间固有差异(比如处理组本来就更活跃)。DID 同时做两个差分,把这两类混杂都减掉。
设定:有处理组(treatment)和控制组(control),各有处理前(pre)和处理后(post)两期。记四格均值为:
| 处理前 | 处理后 | 差分 | |
|---|---|---|---|
| 处理组 | |||
| 控制组 |
DID 估计量为:
关键识别假设叫「平行趋势(parallel trends)」:如果没有发生处理,处理组和控制组的结果会沿着相同的轨迹演变。换句话说,处理前的差异是可解释的固有差异,但处理前后的变化量对两组应该一致。DID 用控制组的变化量,去「扣除」处理组本就会发生的自然变化,剩下的就是处理的净效应。
三、可复现模拟:一次政策冲击的因果效应#
下面用一个完全可复现的面板数据演示。设定:90 只股票、120 个交易日,第 60 天发生一次事件冲击;30 只为处理组(受事件影响),60 只控制组(不受影响)。每只股票的日异常收益由「个体固定效应 + 共同时间趋势 + 处理效应 + 噪声」构成,处理组仅在事件后获得每天 +0.40% 的异常收益(真实因果效应 )。我们假装不知道 ,用 DID 把它估回来。
import numpy as np
rng = np.random.default_rng(20260711)
N, T, t0 = 90, 120, 60
n_treat = 30
tau_true = 0.0040 # 真实处理效应:事件后每天 +0.40%
treated = np.zeros(N, dtype=bool); treated[:n_treat] = True
alpha = rng.normal(0.0, 0.004, N) # 个体固定效应
gamma = 0.0010 * np.sin(np.arange(T)/9.0) + 0.00002*np.arange(T) # 共同时间趋势
post = (np.arange(T) >= t0).astype(float)
Y = np.zeros((N, T))
for i in range(N):
eps = rng.normal(0.0, 0.010, T)
Y[i] = alpha[i] + gamma + tau_true * treated[i] * post + eps # 真实数据生成过程
def did_estimate(Y, treated, post_bool):
# 四格均值
t_p = Y[np.ix_(treated, post_bool)].mean()
t_pr = Y[np.ix_(treated, ~post_bool)].mean()
c_p = Y[np.ix_(~treated, post_bool)].mean()
c_pr = Y[np.ix_(~treated,~post_bool)].mean()
return (t_p - t_pr) - (c_p - c_pr)
post_bool = post == 1
tau_hat = did_estimate(Y, treated, post_bool)
print(f"DID 估计 = {tau_hat*100:.3f}% (真实 τ = {tau_true*100:.2f}%)")python跑出来:
DID 估计 = 0.415% (真实 τ = 0.40%)plaintextDID 几乎完美还原了真实处理效应(0.415% vs 0.40%)。四格均值分别是:处理·前 0.040%、处理·后 0.570%、控制·前 0.029%、控制·后 0.144%。注意处理组在事件后跳升了约 0.53 个百分点,但控制组同期也上升了约 0.11 个百分点——那 0.11 是共同市场趋势,必须扣掉,否则你会把市场 beta 当成策略 alpha。

四、置信区间:以股票为聚类单位的 bootstrap#
DID 估计量本身是一个数,但你要知道它稳不稳。面板数据里同一只股票的日度收益高度自相关,不能逐日重采样(会把相关性打散)。正确做法是以个体(股票)为聚类单位的 bootstrap:每次整只股票(它所有 120 天)一起抽,重复 2000 次,看 的分布。
B = 2000
boot = np.empty(B)
idx = np.arange(N)
for b in range(B):
samp = rng.choice(idx, size=N, replace=True)
boot[b] = did_estimate(Y[samp], treated[samp], post_bool)
ci_lo, ci_hi = np.percentile(boot, [2.5, 97.5])
print(f"95% CI = [{ci_lo*100:.3f}%, {ci_hi*100:.3f}%]")python得到 95% CI = [0.328%, 0.498%],稳稳覆盖了真实值 0.40%。这说明我们的因果结论在统计上是可信的,而不只是「看起来相关」。
五、事件研究:处理组 − 控制组的累计异常收益#
DID 给的是单一标量效应,但策略上你往往还想知道效应怎么随时间展开。事件研究做法很简单:每天取「处理组均值 − 控制组均值」,这一步直接扣掉了共同时间趋势(gamma),得到纯净的「处理带来的日度异常差」。对其累计,就是 CAR(累计异常收益)。
diff = Y[treated].mean(axis=0) - Y[~treated].mean(axis=0)
car = np.cumsum(diff)
win = 20
car_win = car[(t0 - win):(t0 + win + 1)] # 事件前 20 天到后 20 天python事件前 CAR 在 0 附近平稳游走(说明两组本来同步),事件后稳步抬升,窗口末尾累计约 9.78%(理论值 ,加上噪声略高)。这正是 DID 识别假设「平行趋势」的可视化证据:如果事件前 CAR 已经开始分叉,说明处理组和控制在事件前轨迹就不平行,DID 的前提就破了。

六、DID 在量化策略里的真实用法#
DID 不是学术玩具,它在以下场景能直接落地:
- 政策 / 监管事件评估:某条交易规则、某次印花税调整、某只股票被纳入 / 剔除指数,用处理组(受影响标的)vs 控制组(未受影响同类标的)做 DID,干净地估出事件带来的收益增量或成本。
- 策略 / 规则上线的 A/B 验证:把账户或标的随机(或按可观测特征)分成两组,一组启用新信号 / 新执行算法,一组维持旧方案,用 DID 估「新方案相对旧方案的净提升」——这比单纯看新方案收益更可信。
- 因子改动的归因:改了因子定义后,用 DID 区分「市场本身在涨」和「改动真正带来的改善」。
七、诚实的陷阱:DID 不是万能钥匙#
- 平行趋势不可直接检验。你只能看到处理前的走势是否平行(本文图 3 事件前确实平稳),但「事件后若没发生处理会怎样」是反事实,永远观测不到。一旦你怀疑处理前就不平行(比如处理组恰好是更敏感的成长股),DID 就偏了。缓解:加更多前置期做趋势检验、用协变量调整、或换合成控制。
- 预期效应(anticipation)。如果市场在事件「正式宣布」前就提前反应(利好泄露),那么处理效应会前移,DID 会把一部分效应算到事件前,低估真实效应。事件研究的 CAR 曲线能帮你看到这种前移。
- 只能识别「对处理组的平均效应」。DID 告诉你处理组平均变了 0.4%/天,但不保证每只股票都变了这么多,也不告诉你「哪只股票反应最强」。要细粒度,得上异质性处理效应(Causal Forest 等)。
- 时变混杂。如果有第三个因素在事件后同时改变了处理组和控制组(比如事件只影响了处理组,但宏观冲击同时打了所有人),且这个因素与处理相关,DID 会把它混进去。缓解:加入时间×组固定效应、控制可观测时变协变量。
- 控制组必须「可比」。最危险的隐性错误是控制组选得和处理组根本不是一类资产(例如用债券当股票的控制组)。可比性是 DID 成立的前提,比数学更重要。
八、总结#
相关性告诉你「什么和收益一起动」,因果推断告诉你「动哪个能改变收益」。DID 用「两组两期、两次差分」把共同时间趋势和组间固有差异都减掉,在「有清晰处理 / 控制边界的事件」场景下,是性价比最高的因果工具:一个 did_estimate 函数、一次聚类 bootstrap、一张事件研究 CAR 图,就能把「这个政策 / 这次规则改动到底有没有用」说清楚。
但它的诚实边界也很硬:平行趋势不可证伪、预期效应会前移、只能给平均效应。当你需要更强的反事实构造时,下一步是自然走向合成控制法(Synthetic Control)和因果森林(Causal Forest)——那是把 DID 的「找一组可比控制」从人工升级为数据驱动的文章了。