Causal Identification / DID

DID 双重差分与事件研究

DID 的核心不是“两次相减”这个算术动作,而是用未处理组的趋势构造处理组在未处理状态下的反事实路径。

Mechanism Lab

动画:DID 如何构造处理组反事实

动画把处理组实际路径、对照组趋势和虚线反事实放在同一张图中,政策效果就是政策后实际路径与反事实路径之间的垂直距离。

Step 1 / 5

Pre trends

先观察政策前处理组和对照组是否沿相近斜率移动。

pre-trend check

Animation Control

Reduced-motion users receive the same step states without continuous motion.

01 / 直觉

核心直觉

DID 估计的是处理组在政策之后的反事实缺口:实际结果减去“如果没有政策,本来会怎样”。

对照组不是为了比较水平,而是为了提供未处理潜在结果 Y(0) 的时间趋势。

平行趋势假设说:如果没有处理,处理组和对照组的平均变化应当相同。事件研究把这个假设放到多个相对时间点上检查。

02 / 数学

从潜在结果到 DID 估计量

01 / 目标参数

设 G=1 表示处理组,Post=1 表示政策后。目标通常是处理组政策后平均处理效应 ATT。

ATT = E[Y(1) - Y(0) | G=1, Post=1]
    = E[Y | G=1, Post=1] - E[Y(0) | G=1, Post=1]

02 / 不可观察反事实

困难在于 E[Y(0)|G=1,Post=1] 不可观察。DID 用处理组政策前水平加上对照组的未处理趋势来构造它。

E[Y(0)|G=1,Post=1]
  = E[Y|G=1,Post=0]
    + {E[Y|G=0,Post=1] - E[Y|G=0,Post=0]}

03 / 平行趋势

上式成立依赖于未处理潜在结果的平均变化在两组之间相同。这是识别假设,不是回归自动保证的事实。

E[Y(0)_post - Y(0)_pre | G=1]
  = E[Y(0)_post - Y(0)_pre | G=0]

04 / DID 估计量

把反事实代回 ATT,就得到双重差分:处理组前后变化减去对照组前后变化。

tau_DID = {E[Y|G=1,Post=1] - E[Y|G=1,Post=0]}
        - {E[Y|G=0,Post=1] - E[Y|G=0,Post=0]}

05 / 回归形式

两组两期 DID 可写成带交互项的线性回归;交互项系数就是 DID 估计量。

Y_it = alpha + gamma G_i + lambda Post_t + tau(G_i * Post_t) + epsilon_it

03 / 代码

Python 代码:DID、聚类标准误与事件研究

下面代码演示两组两期 DID,以及面板事件研究的基本结构。真实论文中还要加入固定效应、聚类标准误、样本限制和稳健性检验。

import pandas as pd
import statsmodels.formula.api as smf

# df columns:
# unit_id, year, outcome, treated, post, first_treat_year
df = pd.read_csv("policy_panel.csv")
df["did"] = df["treated"] * df["post"]

did_model = smf.ols(
    "outcome ~ treated + post + did",
    data=df,
).fit(
    cov_type="cluster",
    cov_kwds={"groups": df["unit_id"]},
)
print(did_model.params["did"])
print(did_model.conf_int().loc["did"])

# Event-study skeleton with unit and year fixed effects.
df["event_time"] = df["year"] - df["first_treat_year"]
event_terms = []
for k in range(-4, 5):
    if k == -1:
        continue  # omitted baseline period
    col = f"event_{k:+d}".replace("+", "p").replace("-", "m")
    df[col] = ((df["event_time"] == k) & (df["treated"] == 1)).astype(int)
    event_terms.append(col)

formula = "outcome ~ " + " + ".join(event_terms) + " + C(unit_id) + C(year)"
event_model = smf.ols(formula, data=df).fit(
    cov_type="cluster",
    cov_kwds={"groups": df["unit_id"]},
)
print(event_model.params[event_terms])

04 / 案例

案例:教育补贴政策的 DID 评估

  • 研究问题:某地区 2020 年开始提供教育补贴,是否提高了学生考试成绩?
  • 处理组是受到补贴覆盖的学校,对照组是同一时期未覆盖但制度环境相近的学校。
  • 关键检查不是政策后处理组是否更高,而是政策前两组趋势是否接近,以及政策后缺口是否超出对照组趋势。
  • 输出应同时报告主 DID 系数、事件研究图、样本流、聚类层级、替代窗口和安慰剂政策时间。

05 / 风险

常见误区

只画政策后差异,不检查政策前趋势。
把 TWFE 系数机械解释为平均处理效应,忽略分期处理和异质性权重问题。
没有说明聚类标准误层级,或在学校/地区面板中错误地按个体独立处理。

参考资料