9.2 DID 核心原理
深入理解 DID 的因果推断逻辑
本节目标
- 理解DID的因果推断逻辑(潜在结果框架)
- 掌握面板数据DID模型(TWFE固定效应)
- 学会使用固定效应控制不可观测变量
- 标准误的聚类调整
- DID模型的扩展(控制变量)
- 使用linearmodels实现专业的面板回归
一、DID的因果推断框架
潜在结果框架(Rubin Causal Model)
基本符号
- :个体 接受处理的潜在结果
- :个体 不接受处理的潜在结果
- :是否接受处理(1 = 接受处理)
观测到的结果
个体处理效应(Individual Treatment Effect, ITE)
根本问题:我们永远无法观测到 和 同时存在——这就是因果推断的根本问题。
平均处理效应(Average Treatment Effect, ATE)
定义
处理组平均处理效应(ATT)
为什么不能简单比较?
结论:简单比较混杂了因果效应和选择性偏差,导致有偏估计。
二、DID如何消除偏差
DID的面板数据设定
时间维度: (政策前), (政策后)
潜在结果
- :个体 在时期 不接受政策的潜在结果
- :个体 在时期 接受政策的潜在结果
观测到的结果
其中
DID的关键假设(平行趋势假设):
含义:在没有政策干预的情况下,处理组和对照组的变化趋势是平行的。
DID估计量
结论:在平行趋势假设下,DID估计量无偏地识别了处理组平均处理效应(ATT)。
三、面板数据DID模型
双向固定效应模型(TWFE: Two-Way Fixed Effects)
标准的DID回归方程
各项含义
- :个体固定效应(Unit Fixed Effects)
- 控制个体层面的时不变特征
- 例如:地理位置、文化传统等
- :时间固定效应(Time Fixed Effects)
- 控制所有个体共同面临的时间冲击
- 例如:全国性的经济周期、技术进步等
- :处理虚拟变量()
- :DID估计量(ATT)
固定效应如何消除偏差
个体固定效应 消除截面异质性
- 通过组内去均值(within transformation)实现
时间固定效应 消除共同时间趋势
- 通过时间去均值实现
深入理解 DID的双重差分
使用虚拟变量表示法(更直观):
等价于:
四、Python实现面板DID
方法1:statsmodels (OLS with dummies)
python
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
import matplotlib.pyplot as plt
import seaborn as sns
# 设置
np.random.seed(42)
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False
# 生成模拟面板数据
n_units = 50 # 个体数量
n_periods = 10 # 时期数量
treatment_time = 5 # 政策干预时间
treatment_effect = 20 # 真实政策效应
data = []
for unit in range(n_units):
treated = 1 if unit >= n_units // 2 else 0 # 一半个体接受处理
unit_effect = np.random.normal(10 * treated, 5) # 个体固定效应
for period in range(n_periods):
time_effect = 2 * period # 时间趋势
post = 1 if period >= treatment_time else 0
# 结果变量
y = (50 + unit_effect + time_effect +
treatment_effect * treated * post +
np.random.normal(0, 3))
data.append({
'unit': unit,
'period': period,
'treated': treated,
'post': post,
'did': treated * post,
'y': y
})
df = pd.DataFrame(data)
print("=" * 70)
print("模拟数据描述统计")
print("=" * 70)
print(df.groupby(['treated', 'period'])['y'].mean().unstack())
print("\n")
# 方法1:OLS with fixed effects dummies
# 注意:包含所有虚拟变量会导致共线性问题,需要设置基准组
model_fe = smf.ols('y ~ C(unit) + C(period) + did', data=df).fit(cov_type='HC1')
print("=" * 70)
print("方法1:OLS with FE dummies")
print("=" * 70)
print(f"DID 估计系数: {model_fe.params['did']:.3f}")
print(f"标准误: {model_fe.bse['did']:.3f}")
print(f"95% CI: [{model_fe.conf_int().loc['did', 0]:.3f}, {model_fe.conf_int().loc['did', 1]:.3f}]")
print(f"真实效应: {treatment_effect}")
print("\n")方法2:linearmodels (专业的面板数据工具)
python
from linearmodels.panel import PanelOLS
# 设置数据为面板格式(multi-index)
df_panel = df.set_index(['unit', 'period'])
# 使用 PanelOLS with entity and time effects
model_panel = PanelOLS(
dependent=df_panel['y'],
exog=df_panel[['did']],
entity_effects=True, # 个体固定效应
time_effects=True, # 时间固定效应
).fit(cov_type='clustered', cluster_entity=True) # 聚类标准误
print("=" * 70)
print("方法2:linearmodels PanelOLS (推荐)")
print("=" * 70)
print(model_panel.summary)
print("\n")
# 提取结果
did_coef = model_panel.params['did']
did_se = model_panel.std_errors['did']
did_ci = model_panel.conf_int().loc['did']
print("=" * 70)
print("估计结果总结")
print("=" * 70)
print(f"DID 估计系数: {did_coef:.3f} (SE = {did_se:.3f})")
print(f"95% CI: [{did_ci[0]:.3f}, {did_ci[1]:.3f}]")
print(f"真实效应: {treatment_effect}")
print(f"估计偏差: {did_coef - treatment_effect:.3f}")重要参数
entity_effects=True:添加个体固定效应time_effects=True:添加时间固定效应cov_type='clustered', cluster_entity=True:使用个体层面聚类标准误(Bertrand et al. 2004推荐)
五、可视化平行趋势
事件研究图(Event Study Plot)
python
# 构造相对时间变量
df['rel_period'] = df['period'] - treatment_time
df['rel_period'] = df['rel_period'] * df['treated'] # 只对处理组有效
# 创建leads和lags虚拟变量(以t=-1为基准组)
for t in range(-treatment_time, n_periods - treatment_time):
if t != -1: # 基准组
df[f'lead_lag_{t}'] = ((df['rel_period'] == t) & (df['treated'] == 1)).astype(int)
# 构建回归公式
lead_lag_vars = [f'lead_lag_{t}' for t in range(-treatment_time, n_periods - treatment_time) if t != -1]
# 事件研究回归
formula = 'y ~ C(unit) + C(period) + ' + ' + '.join(lead_lag_vars)
model_es = smf.ols(formula, data=df).fit(cov_type='HC1')
# 提取系数并构造绘图数据
event_study_results = []
for t in range(-treatment_time, n_periods - treatment_time):
if t == -1:
# 基准期系数为0
event_study_results.append({'period': t, 'coef': 0, 'ci_lower': 0, 'ci_upper': 0})
else:
var_name = f'lead_lag_{t}'
coef = model_es.params[var_name]
ci = model_es.conf_int().loc[var_name]
event_study_results.append({
'period': t,
'coef': coef,
'ci_lower': ci[0],
'ci_upper': ci[1]
})
es_df = pd.DataFrame(event_study_results)
# 绘制事件研究图
fig, ax = plt.subplots(figsize=(14, 8))
ax.plot(es_df['period'], es_df['coef'], 'o-', linewidth=2, markersize=8, color='navy', label='DID 系数')
ax.fill_between(es_df['period'], es_df['ci_lower'], es_df['ci_upper'], alpha=0.2, color='navy', label='95% CI')
ax.axhline(y=0, color='black', linestyle='--', linewidth=1, alpha=0.5)
ax.axvline(x=-0.5, color='red', linestyle='--', linewidth=2, alpha=0.7, label='政策实施时间')
ax.set_xlabel('相对于政策实施的时间', fontsize=14, fontweight='bold')
ax.set_ylabel('估计系数', fontsize=14, fontweight='bold')
ax.set_title('事件研究图:DID 动态效应', fontsize=16, fontweight='bold')
ax.legend(fontsize=12)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("=" * 70)
print("平行趋势检验")
print("=" * 70)
print("查看政策前系数(t < 0):")
pre_treatment = es_df[es_df['period'] < 0]
print(pre_treatment[['period', 'coef', 'ci_lower', 'ci_upper']])
print("\n如果平行趋势假设成立,政策前系数应当接近0且不显著")如何解读
- 政策前 ():系数应接近0(平行趋势检验)
- 政策后 ():系数显著为正,说明政策有效应
- 动态效应:观察效应是否随时间增强/减弱/保持稳定
六、标准误的聚类调整
为什么需要聚类标准误
Bertrand et al. (2004) 的重要发现
DID研究中常见的统计问题:
- 序列相关(Serial Correlation):个体在不同时期的误差项相关
- 标准误低估:OLS标准误严重低估真实标准误
- 显著性夸大:导致拒绝原假设的概率过高
解决方法:使用个体层面聚类标准误
Python中的不同选项
python
from linearmodels.panel import PanelOLS
import statsmodels.formula.api as smf
df_panel = df.set_index(['unit', 'period'])
# 1. 朴素OLS(不推荐)
model_ols = PanelOLS(
df_panel['y'],
df_panel[['did']],
entity_effects=True,
time_effects=True
).fit(cov_type='unadjusted')
# 2. 异方差稳健(不推荐)
model_robust = PanelOLS(
df_panel['y'],
df_panel[['did']],
entity_effects=True,
time_effects=True
).fit(cov_type='robust')
# 3. 聚类标准误 - 个体层面聚类(推荐)
model_cluster_entity = PanelOLS(
df_panel['y'],
df_panel[['did']],
entity_effects=True,
time_effects=True
).fit(cov_type='clustered', cluster_entity=True)
# 4. 双向聚类(个体 + 时间)
model_cluster_two_way = PanelOLS(
df_panel['y'],
df_panel[['did']],
entity_effects=True,
time_effects=True
).fit(cov_type='clustered', clusters=df_panel.reset_index()[['unit', 'period']])
# 比较结果
print("=" * 70)
print("标准误比较")
print("=" * 70)
from scipy import stats
results_comparison = pd.DataFrame({
'估计系数': [
model_ols.params['did'],
model_robust.params['did'],
model_cluster_entity.params['did'],
model_cluster_two_way.params['did']
],
'标准误': [
model_ols.std_errors['did'],
model_robust.std_errors['did'],
model_cluster_entity.std_errors['did'],
model_cluster_two_way.std_errors['did']
]
}, index=['OLS', 'Robust', 'Cluster(Entity)', 'Cluster(Two-way)'])
results_comparison['t统计量'] = results_comparison['估计系数'] / results_comparison['标准误']
results_comparison['p值'] = 2 * (1 - stats.t.cdf(np.abs(results_comparison['t统计量']), df=n_units-1))
print(results_comparison)
print("\n")
print("说明:")
print(" • OLS标准误通常会低估真实标准误")
print(" • 推荐使用聚类标准误(entity层面聚类)")最佳实践建议
- 最低要求:使用个体层面聚类(
cluster_entity=True) - 更稳健:双向聚类(个体 + 时间)
- 样本较小:考虑Wild Bootstrap等重抽样方法
七、添加控制变量
带控制变量的DID
回归方程扩展
其中 是时变控制变量(time-varying covariates)
何时添加控制变量
- 为了提高估计效率(precision),减小标准误
- 为了检验平行趋势的稳健性(控制变量不应改变平行趋势)
- 但要注意避免"坏控制"
Python实现
python
# 生成时变控制变量
np.random.seed(42)
df['x1'] = np.random.normal(10, 2, len(df)) # 连续变量
df['x2'] = np.random.binomial(1, 0.5, len(df)) # 二值变量
# 回归(带控制变量)
df_panel = df.set_index(['unit', 'period'])
model_with_controls = PanelOLS(
df_panel['y'],
df_panel[['did', 'x1', 'x2']],
entity_effects=True,
time_effects=True
).fit(cov_type='clustered', cluster_entity=True)
print("=" * 70)
print("带控制变量的DID")
print("=" * 70)
print(model_with_controls.summary)注意事项
- 只控制外生变量:控制变量必须与处理无关
- 不要控制"坏控制"(Bad Controls):不控制政策的中介变量
- 使用Lasso等方法选择控制变量:控制变量过多会降低估计效率
八、DID的扩展
Staggered DID(交错DID)
场景:个体在不同时间接受处理
传统 TWFE的问题(Goodman-Bacon 2021)
当处理时间交错时, 不再是简单的 DID,而是多个异质性DID的加权平均(权重可能为负!)
"坏对照组"问题
- 已处理个体成为后处理个体的对照组
- 可能导致估计量有偏
解决方法
- Callaway & Sant'Anna (2021):估计组别-时间异质性ATT
- Sun & Abraham (2021):构造干净的交互项
- De Chaisemartin & D'Haultfoeuille (2020):变量稳健的DID
如何使用CS估计器
python
# 示例代码(需要安装: pip install csdid (目前仅R包成熟))
# 可以通过rpy2调用R,或等待Python版本完善
# 伪代码示例:
"""
from csdid import ATT
# 估计group-time specific ATT
att_results = ATT(
data=df,
yname='y',
gname='first_treat', # 首次接受处理的时间
tname='period',
idname='unit',
control_group='notyettreated' # 使用尚未处理的个体作为对照组
)
att_results.summary()
att_results.plot()
"""
print("=" * 70)
print("交错DID的处理")
print("=" * 70)
print("若你的数据存在处理时间交错(Staggered DID),建议使用:")
print("1. Callaway & Sant'Anna (2021) 方法")
print("2. Sun & Abraham (2021) 方法")
print("3. 或手动修正 TWFE的潜在问题")九、本节总结
关键要点
DID的因果推断逻辑
- 平行趋势假设是识别的核心
- DID估计量识别了ATT(处理组平均处理效应)
面板DID模型
- 使用双向固定效应(TWFE)模型
- 个体固定效应控制时不变特征
- 时间固定效应控制共同时间趋势
标准误
- 必须使用聚类标准误(使用个体层面聚类)
- 样本较小时考虑Bootstrap等重抽样方法
扩展与注意
- Staggered DID 需要特殊方法
- 建议使用变量稳健的估计器
Python工具箱
| 任务 | 推荐工具包 |
|---|---|
| 基础DID | statsmodels.formula.api.ols() |
| 面板DID | linearmodels.panel.PanelOLS() |
| 聚类标准误 | cov_type='clustered' |
| Staggered DID | csdid, did (R包或Python接口) |
下一步
进入 第3节:平行趋势假设 学习更多内容:
- 如何检验平行趋势假设
- 事件研究图的绘制
- 处理违反平行趋势的情况
掌握面板DID是政策评估的基石!