Module 9 双重差分法(Difference-in-Differences, DID)
因果推断的黄金标准:从自然实验到政策评估
本章目标
完成本章后,你将能够:
- 理解双重差分法的核心思想和识别逻辑
- 掌握DID的基本假设(平行趋势假设)及其检验方法
- 实施标准DID回归和事件研究设计
- 进行安慰剂检验和稳健性检验
- 处理违反平行趋势假设的情况
- 使用Python实现DID分析(statsmodels, linearmodels)
- 复现经典的DID研究(Card & Krueger 1994等)
为什么 DID 是社会科学的核心方法?
DID:准实验设计的利器
在社会科学研究中,我们很少有机会进行随机对照试验(RCT)。政策制定者不会为了学术研究而随机分配政策。但是,自然实验(Natural Experiments)为我们提供了"准随机"的机会:
经典场景:某些地区实施了新政策,而另些地区没有
- 处理组(Treatment Group):受到政策影响的地区/个体
- 对照组(Control Group):未受政策影响的地区/个体
核心问题:如何从观察数据中识别政策的因果效应?
DID vs 简单比较
假设我们想评估最低工资提高对就业率的影响。
** 错误方法1:简单对比(事后 vs 事前)**
问题:可能存在时间趋势!即使没有政策,就业率也可能自然增长或下降。
** 错误方法2:横截面对比(处理组 vs 对照组)**
问题:处理组和对照组可能本来就不同(选择性偏差)!
** 正确方法:双重差分(DID)**
直觉:
- 第一次差分:消除截面差异(处理组和对照组的固有差异)
- 第二次差分:消除时间趋势(共同的时间效应)
- 剩余部分:政策的因果效应!
DID 的核心直觉
图示:理想的 DID
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # macOS
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
# 模拟数据
np.random.seed(42)
time = np.arange(0, 20)
treatment_time = 10
# 对照组:稳定增长
control = 50 + 2 * time + np.random.normal(0, 2, len(time))
# 处理组:政策前平行,政策后跳升
treatment = 55 + 2 * time + np.random.normal(0, 2, len(time))
treatment[treatment_time:] += 15 # 政策效应
# 反事实(如果没有政策,处理组会怎样)
counterfactual = 55 + 2 * time + np.random.normal(0, 2, len(time))
fig, ax = plt.subplots(figsize=(14, 8))
# 绘制实际观测
ax.plot(time[:treatment_time], control[:treatment_time],
'o-', color='blue', linewidth=2, label='对照组(Control)', markersize=6)
ax.plot(time[treatment_time:], control[treatment_time:],
'o-', color='blue', linewidth=2, markersize=6)
ax.plot(time[:treatment_time], treatment[:treatment_time],
's-', color='red', linewidth=2, label='处理组(Treatment)', markersize=6)
ax.plot(time[treatment_time:], treatment[treatment_time:],
's-', color='red', linewidth=2, markersize=6)
# 绘制反事实(虚线)
ax.plot(time[treatment_time:], counterfactual[treatment_time:],
'--', color='red', linewidth=2, alpha=0.6, label='反事实(Counterfactual)')
# 标注政策时点
ax.axvline(x=treatment_time, color='green', linestyle='--', linewidth=2, alpha=0.7)
ax.text(treatment_time + 0.3, 45, '政策实施时点', fontsize=12, color='green', fontweight='bold')
# 标注DID效应
did_effect_y = treatment[treatment_time + 5]
counterfactual_y = counterfactual[treatment_time + 5]
ax.annotate('', xy=(treatment_time + 5, did_effect_y),
xytext=(treatment_time + 5, counterfactual_y),
arrowprops=dict(arrowstyle='<->', color='purple', lw=3))
ax.text(treatment_time + 5.5, (did_effect_y + counterfactual_y) / 2,
'DID 效应\n(因果效应)', fontsize=12, color='purple', fontweight='bold',
bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.3))
ax.set_xlabel('时间', fontsize=14, fontweight='bold')
ax.set_ylabel('结果变量(如就业率)', fontsize=14, fontweight='bold')
ax.set_title('双重差分法(DID)的核心逻辑', fontsize=16, fontweight='bold')
ax.legend(loc='upper left', fontsize=12)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()关键观察:
- 政策前:处理组和对照组平行(趋势相同,水平不同)
- 政策后:处理组跳升,对照组继续原趋势
- DID效应:处理组实际值 - 反事实值(虚线)
DID 的数学表达
2×2 DID:最简单的情况
数据结构:
| 政策前(Before) | 政策后(After) | 差值(Δ) | |
|---|---|---|---|
| 处理组(Treated) | |||
| 对照组(Control) |
DID 估计量:
直觉:
- 第一种表达:先比时间(前后差),再比组间差
- 第二种表达:先比组间(处理vs对照),再比时间差
2×2 DID 的回归形式
等价的回归模型:
变量定义:
- 如果个体 在处理组
- 如果时间 在政策后
- :交互项,政策效应的关键!
参数解释:
- :对照组在政策前的基准水平
- :处理组和对照组在政策前的固定差异(截面异质性)
- :对照组政策前后的时间趋势(共同的时间效应)
- :DID 效应(政策的因果效应)
为什么 就是因果效应?
让我们推导四组均值:
| 组合 | 交互项 | 预测值 | ||
|---|---|---|---|---|
| 对照组-前 | 0 | 0 | 0 | |
| 对照组-后 | 0 | 1 | 0 | |
| 处理组-前 | 1 | 0 | 0 | |
| 处理组-后 | 1 | 1 | 1 |
计算 DID:
结论:回归中的交互项系数 就是我们要的 DID 效应!
DID 的核心假设
假设 1:平行趋势假设(Parallel Trends Assumption)⭐
核心假设:如果没有政策干预,处理组和对照组会遵循相同的时间趋势。
数学表达:
白话:
- 在没有政策的反事实世界中,处理组和对照组之间的差距保持不变
- 换句话说,两组的趋势相同(平行),但水平可以不同
为什么重要?
- 如果违反平行趋势,DID 估计会有偏差
- 这是 DID 最关键、也是最容易受质疑的假设
如何检验?(第3节详细讨论)
- 事前趋势检验:查看政策前的趋势是否平行
- 事件研究图(Event Study):绘制动态效应
- 安慰剂检验(Placebo Test):使用假的政策时点
假设 2:外生性假设(Exogeneity)
假设:政策的实施时间和地点与潜在结果无关(条件独立)。
数学表达:
白话:
- 哪些地区/个体被政策影响,不能由未来的结果决定
- 例如:如果政府只在就业率即将上升的地区提高最低工资,DID 会高估效应
实践中的威胁:
- 预期效应:企业提前知道政策,提前调整
- 反向因果:政府根据结果变量选择政策目标(内生性)
假设 3:稳定单位处理值假设(SUTVA)
假设:一个单位的处理状态不影响其他单位的结果(无溢出效应)。
威胁:
- 地理溢出:政策地区的企业迁移到非政策地区
- 一般均衡效应:政策改变市场价格、工资等
DID 的经典应用
案例 1:Card & Krueger (1994) - 最低工资与就业
研究问题:最低工资提高会降低就业吗?
背景:
- 1992年4月,美国新泽西州将最低工资从 $4.25 提高到 $5.05
- 邻近的宾夕法尼亚州没有提高
研究设计:
- 处理组:新泽西州的快餐店
- 对照组:宾夕法尼亚州的快餐店
- 结果变量:全职等效就业人数(FTE)
数据:
- 政策前:1992年2月(政策前2个月)
- 政策后:1992年11月(政策后7个月)
发现:
结论:最低工资提高后,新泽西州的就业增加了2.76人(相对于宾州)!
影响:
- 挑战了传统经济学理论
- 引发了关于最低工资的长期争论
- 成为DID方法的经典案例
案例 2:Bertrand et al. (2004) - DID 的统计推断问题
发现的问题:
- 许多DID研究低估了标准误
- 原因:序列相关(Serial Correlation)
解决方案:
- 聚类标准误(Clustered Standard Errors):在政策单位层面聚类
- Block Bootstrap:重抽样方法
- Randomization Inference:随机化推断
教训:DID 不仅要关注点估计,更要关注统计推断!
️ Python 实现:2×2 DID 示例
简单示例:模拟数据
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
from statsmodels.iolib.summary2 import summary_col
# 设置随机种子
np.random.seed(123)
# 模拟数据
n_units = 50 # 每组单位数
n_periods = 2 # 前后两期
# 创建数据框
data = []
for unit in range(n_units * 2):
treated = 1 if unit >= n_units else 0
for period in range(n_periods):
post = 1 if period == 1 else 0
# 数据生成过程
# Y = 50 + 10*treated + 5*post + 15*(treated*post) + noise
y = 50 + 10 * treated + 5 * post + 15 * (treated * post) + np.random.normal(0, 5)
data.append({
'unit': unit,
'period': period,
'treated': treated,
'post': post,
'y': y
})
df = pd.DataFrame(data)
df['treated_post'] = df['treated'] * df['post']
print("=" * 60)
print("数据预览")
print("=" * 60)
print(df.head(10))
print("\n")
# 计算均值
means = df.groupby(['treated', 'post'])['y'].mean().unstack()
print("=" * 60)
print("2×2 均值表")
print("=" * 60)
print(means)
print("\n")
# 手动计算 DID
treated_diff = means.loc[1, 1] - means.loc[1, 0]
control_diff = means.loc[0, 1] - means.loc[0, 0]
did_manual = treated_diff - control_diff
print("=" * 60)
print("手动计算 DID")
print("=" * 60)
print(f"处理组差值: {treated_diff:.3f}")
print(f"对照组差值: {control_diff:.3f}")
print(f"DID 效应: {did_manual:.3f}")
print("\n")
# 回归估计 DID
model = smf.ols('y ~ treated + post + treated_post', data=df).fit(cov_type='HC1')
print("=" * 60)
print("回归估计 DID")
print("=" * 60)
print(model.summary())输出解读:
treated系数 ≈ 10:处理组和对照组的固有差异post系数 ≈ 5:时间趋势(对所有组相同)treated_post系数 ≈ 15:DID 效应(政策因果效应) ⭐
本章结构
第 1 节:本章介绍(当前)
- DID 的核心思想和直觉
- 2×2 DID 的数学表达和回归形式
- 核心假设:平行趋势、外生性、SUTVA
- 经典应用案例
- Python 基础实现
第 2 节:DID 原理
- DID 的识别逻辑和因果图
- 多期DID(Staggered DID)
- 控制变量和协变量调整
- 标准误的计算(聚类、稳健性)
- 面板数据的DID实现
第 3 节:平行趋势假设
- 事前趋势检验(Pre-trend Test)
- 事件研究设计(Event Study)
- 动态效应图(Dynamic Effects)
- 违反平行趋势的处理方法
第 4 节:安慰剂检验
- 安慰剂检验(Placebo Test)
- 假政策时点检验
- 样本排除检验
- 随机化推断
第 5 节:经典案例和Python实现
- Card & Krueger (1994) 完整复现
- 环境政策的DID评估
- 面板数据处理技巧
- 可视化最佳实践
第 6 节:本章小结
- DID 方法总结
- 常见陷阱和注意事项
- 练习题
- 文献推荐
️ Python 工具包
核心库
| 库 | 主要功能 | 安装 |
|---|---|---|
| pandas | 数据处理 | pip install pandas |
| statsmodels | OLS回归 | pip install statsmodels |
| linearmodels | 面板DID | pip install linearmodels |
| matplotlib | 可视化 | pip install matplotlib |
| seaborn | 高级可视化 | pip install seaborn |
基础设置
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import statsmodels.api as sm
import statsmodels.formula.api as smf
from statsmodels.iolib.summary2 import summary_col
from linearmodels.panel import PanelOLS
# 中文字体设置
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # macOS
plt.rcParams['axes.unicode_minus'] = False
# 设置样式
sns.set_style("whitegrid")
plt.rcParams['figure.figsize'] = (12, 6)
pd.set_option('display.float_format', '{:.4f}'.format)必读文献
奠基性论文
Card, D., & Krueger, A. B. (1994). "Minimum Wages and Employment: A Case Study of the Fast-Food Industry in New Jersey and Pennsylvania." American Economic Review, 84(4), 772-793.
Bertrand, M., Duflo, E., & Mullainathan, S. (2004). "How Much Should We Trust Differences-In-Differences Estimates?" Quarterly Journal of Economics, 119(1), 249-275.
Callaway, B., & Sant'Anna, P. H. (2021). "Difference-in-Differences with Multiple Time Periods." Journal of Econometrics, 225(2), 200-230.
推荐教材
- Angrist & Pischke (2009). Mostly Harmless Econometrics, Chapter 5
- Cunningham (2021). Causal Inference: The Mixtape, Chapter 9
- Huntington-Klein (2022). The Effect, Chapter 18
准备好了吗?
DID 是现代因果推断的基石方法之一。掌握它,你将能够:
- 评估真实世界的政策效应
- 在顶级期刊发表研究
- 为政策制定提供科学证据
记住核心思想:
"The beauty of DID is its simplicity: it differences out fixed differences and common trends, leaving only the treatment effect."
让我们开始学习 第2节:DID 原理!
因果推断从DID开始!