halo 的技术博客

返回

上篇我们聊了 DID:它靠一个「现成的控制组」把时间趋势剥掉。但你会很快撞上 DID 的死穴——找不到控制组

比如你想评估「某国突然宣布资本管制」对汇率/股市的冲击:受冲击的只有一个国家,全市场里没有第二个「各方面都像它、只是没管制」的平行宇宙对照组。又比如「某只股票被纳入 MSCI 指数」的事件研究——被纳入的只有它一只,同类里有谁「本该被纳入却没被纳入」?

结论先放这:合成控制法 SCM(Synthetic Control Method, Abadie et al. 2010)的聪明之处在于——它不找一个现成对照,而是用一篮子「未受冲击单元」(donor pool)的加权组合,为你「合成」出一个专属对照。 权重怎么定?让合成单元在事件前尽可能贴合受冲击单元——拟合越好,这个合成对照越可信;事件后受冲击单元相对合成对照的 gap,就是处理效应。

在我们的自洽合成面板(1 个受冲击单元 + 39 个未受冲击 donor,事件前 24 期、后 24 期,真实事件后水平偏移=1.08/期)上:事件前贴合 RMSE 仅 0.040,事件后平均 gap 估到 1.08(几乎无偏);权重极度稀疏——39 个 donor 里只有 6 个拿了非零权重,前两大占了 84.6%。留一安慰剂检验中,受冲击单元的事件后 gap 是 40 个单元里的极端离群点(p≈0.025)。 诚实地讲:SCM 对「前拟合好、后崩溃」很敏感,如果合成单元事件前就贴合得勉强,事件后的 gap 很可能是拟合误差而非真效应;而且只有 1 个受冲击单元,你无法像 DID 那样靠「成百上千个控制组」把噪声平均掉。但作为「单一冲击、无现成对照」场景的标配武器,它把「造对照组」这件事变得可量化、可检验。

合成控制:事件前贴合,事件后才显出 gap


1. 核心思想:用 donor 的加权组合当对照组#

设受冲击单元的结果序列是 Y_treat(长度 T,前 T₀ 期是事件前、后 T−T₀ 期是事件后),donor 池是 Y_donors(T × K,K 个未受冲击单元)。

SCM 要找一组权重 W = (W₁, …, W_K),满足:

  • 非负W_j ≥ 0(不接受「做空某个 donor 来凑数」,否则权重失去经济含义)。
  • 和为 1Σ W_j = 1(合成单元是 donor 的凸组合,像一个「虚拟的同类单元」)。
  • 事件前贴合最优W 最小化 Σ_{t≤T₀} (Y_treat,t − Σ_j W_j Y_donors,t,j)²

求出 W 后,合成对照就是 Y_synth = Y_donors @ W。事件后的处理效应:

τ_t = Y_treat,t − Y_synth,t     (对 t > T₀)
plaintext

这跟 DID 的「处理组 − 控制组」一脉相承,只是控制组从「一只现成的」变成了「一篮子凑出来的」。


2. 从零实现:凸约束下的权重求解#

约束「W≥0 且 ΣW=1」是个凸二次规划,用 scipy.optimize.minimize 的 SLSQP 即可。

为什么权重往往极度稀疏? 因为合成一个「像你」的单元,通常只需要少数几个最像的 donor;其余 donor 的噪声反而拖累拟合,优化器自然把它们权重压到 0。这个稀疏性本身就是一种诊断——它告诉你「到底是哪几个单元在替受冲击单元说话」,比 DID 的「一整组混在一起」更透明。

合成控制权重是稀疏的:仅 6/39 个 donor 贡献


3. 推断:留一安慰剂检验#

SCM 只有一个受冲击单元,没法像 DID 那样靠「1000 次随机重排单位」做置换(因为只有 1 个单位能被随机分配为处理组)。替代方案是留一安慰剂检验(leave-one-out placebo)

把 donor 池里的每一个单元,轮流当成「假受冲击单元」,用剩下所有 donor 为它合成对照,算出它的「伪事件后 gap」。如果真实受冲击单元的 gap 明显大于这些伪 gap,说明它不是随机噪音。

def scm_placebo(Y, treat_idx=0):
    N = Y.shape[1]
    gaps = np.zeros(N)
    for i in range(N):
        cols = [c for c in range(N) if c != i]
        others = Y[:, cols]
        y_pre, y_post = Y[:T0, i], Y[T0:, i]
        w = scm_weights(y_pre, others[:T0])
        synth = others @ w
        gaps[i] = (y_post - synth[T0:]).mean()
    return gaps

# 真实受冲击单元 gap = 1.08,在 40 个单元(含自己)中位列极端离群
# 即 p = 1/40 ≈ 0.025:随机单元产生这么大 gap 的概率很低
python

安慰剂检验:真实受冲击单元的 gap 是分布中的极端离群点

注意一个 1/N 的尴尬:只有一个受冲击单元时,留一检验的 p 值下界是 1/N。我们这里 N=40,p≈0.025 是它理论上能到的最好值——这意味着 SCM 的「显著性」本质上受单元数限制,别指望像大样本回归那样拿到 p=0.001。


4. 五类真实陷阱(不藏)#

陷阱 1:外推与负权重(bad controls)。 如果事件前 donor 池里没有哪个组合能贴合受冲击单元(比如受冲击单元有个 donor 都没有的特殊结构),优化器会给出负权重或把权重塞给不相关的 donor 去「凑数」。负权重在经济学上不可解释,且意味着合成对照在事件前已经是一种「外推」。缓解:检查权重是否全非负、是否经济可解释;加进去的 donor 必须「潜在可受冲击」——不能把明显不同的单元塞进池子。

陷阱 2:donor 池太窄→合成对照像「四不像」。 池子越小、越不具代表性,事件前 RMSE 越大,事件后 gap 里混的拟合误差越多。缓解:donor 池要大到能「拼出」受冲击单元,但又不能宽到塞进无关单元(权衡见陷阱 1)。

陷阱 3:事件前贴合好,事件后「崩」了。 这是 SCM 最隐蔽的失败模式:优化器在事件前把 RMSE 压到极低,但这靠的是「过拟合 donor 的噪声」。事件后结构一变,合成对照立刻失真,你读到的 gap 其实是「拟合残差」而非真效应。缓解:看事件前轨迹是否真的逐期贴合(不只看 RMSE 一个数);用「安慰剂斜率比」等更严格的检验。

陷阱 4:唯一解「不唯一」。 凸问题通常有唯一解,但当 donor 高度共线(比如 39 个 donor 其实只由 2 个因子驱动),权重会不唯一——不同优化器/初值给出不同 W,合成对照却一样好。此时权重本身不可解读(别去写「单元 A 贡献了 48%」这种话)。缓解:报告合成对照本身,而非过度解读权重分布。

陷阱 5:小样本过拟合。 当事件前期数 T₀ 远小于 donor 数 K(我们的 24 < 39 还好,但真实里常见 T₀=10、K=100),优化器几乎总能找到一个完美贴合事件前的组合,但这只是记忆噪声。缓解:限制 K、或对权重加 L2 正则(岭化 SCM),牺牲一点事件前贴合换取泛化。


5. 小结:SCM 与 DID 怎么选#

DIDSCM
需要几个受冲击单元最好很多(才能平均噪声、做置换)1 个就够
对照组来源现成挑一只/一组用 donor 加权「造」一个
透明度控制组混在一起,难拆开权重稀疏,能看出谁在说话
最大软肋找不到干净控制组就废了只有 1 个单元,推断力受 N 限制
前拟合失败的信号平行趋势图肉眼可查事件前 RMSE / 轨迹贴合度
  • 它解决什么:单一、外生、不可复制的政策/事件冲击,且你找不到现成对照。
  • 它不解决什么:donor 池的代表性、事件前拟合是否过拟合、只有 1 个单元导致的推断力天花板——这些得靠陷阱 1~5 的诊断和安慰剂检验兜底。
  • 落地路径:真实研究直接用 pysynconSynth(R);永远先报告事件前 RMSE 和逐期贴合轨迹,再谈事件后 gap;至少配一次留一安慰剂检验。

两篇合起来,DID 与 SCM 覆盖了事件研究中「有对照」与「没对照」两端——它们共同的底线是:没有对照组,就没有因果;对照组的干净程度,决定了你结论的可信度。

合成控制法评估政策冲击:当找不到对照组,就「造」一个出来
https://blog.halo26812.eu.org/blog/synthetic-control-method
Author halo
Published at 2026年7月22日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨