9.3 平行趋势假设(Parallel Trends Assumption)
DID成立的核心前提条件
本节目标
- 深入理解平行趋势假设的定义和直觉含义
- 掌握前趋势检验(Pre-trend Test)的方法
- 学会绘制事件研究图(Event Study)
- 掌握多种检验和标准误的聚类调整
- 学会处理违反平行趋势的情况
- 了解稳健性检验和替代方法
一、平行趋势假设的定义
为什么平行趋势是DID成立的核心
数学表达:DID的识别假设
含义:在没有政策干预的反事实情形下,处理组和对照组的结果变量趋势是平行的。
关键理解
- 我们不需要处理组和对照组的水平相同——允许存在固定差异
- 重要的是在政策前,两组的变化趋势必须相同
- 平行趋势假设是关于反事实的假设——不可直接观测
图示示例:
python
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
time = np.arange(0, 10)
treatment_time = 5
# 场景1:满足平行趋势
control_1 = 50 + 2 * time
treatment_1 = 55 + 2 * time
treatment_1[treatment_time:] += 15
axes[0].plot(time, control_1, 'o-', label='对照组', linewidth=2)
axes[0].plot(time[:treatment_time], treatment_1[:treatment_time], 's-', label='处理组', linewidth=2)
axes[0].plot(time[treatment_time:], treatment_1[treatment_time:], 's-', linewidth=2)
axes[0].plot(time[treatment_time:], 55 + 2 * time[treatment_time:], '--', color='red', alpha=0.6, label='反事实')
axes[0].axvline(treatment_time - 0.5, color='green', linestyle='--', alpha=0.7)
axes[0].set_title(' 平行趋势成立', fontsize=14, fontweight='bold')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 场景2:政策前违反平行趋势
control_2 = 50 + 2 * time
treatment_2 = 55 + 3 * time # 趋势不同
treatment_2[treatment_time:] += 15
axes[1].plot(time, control_2, 'o-', label='对照组', linewidth=2)
axes[1].plot(time[:treatment_time], treatment_2[:treatment_time], 's-', label='处理组', linewidth=2)
axes[1].plot(time[treatment_time:], treatment_2[treatment_time:], 's-', linewidth=2)
axes[1].plot(time[treatment_time:], 55 + 3 * time[treatment_time:], '--', color='red', alpha=0.6, label='真实反事实')
axes[1].axvline(treatment_time - 0.5, color='green', linestyle='--', alpha=0.7)
axes[1].set_title(' 违反平行趋势(政策前趋势不同)', fontsize=14, fontweight='bold', color='red')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
# 场景3:政策前平行但政策后动态效应改变
control_3 = 50 + 2 * time
treatment_3 = 55 + 2 * time
treatment_3[treatment_time:] = 55 + 2 * treatment_time + 4 * (time[treatment_time:] - treatment_time)
axes[2].plot(time, control_3, 'o-', label='对照组', linewidth=2)
axes[2].plot(time[:treatment_time], treatment_3[:treatment_time], 's-', label='处理组', linewidth=2)
axes[2].plot(time[treatment_time:], treatment_3[treatment_time:], 's-', linewidth=2)
axes[2].plot(time[treatment_time:], 55 + 2 * time[treatment_time:], '--', color='red', alpha=0.6, label='如果没有政策的反事实')
axes[2].axvline(treatment_time - 0.5, color='green', linestyle='--', alpha=0.7)
axes[2].set_title('️ 政策后动态趋势变化(合理)', fontsize=14, fontweight='bold', color='orange')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()场景说明
- 场景1(绿色):完全满足DID,估计无偏
- 场景2(红色):政策前趋势已不同,DID估计量有偏
- 场景3(橙色️):政策前平行但政策后趋势改变——允许的情形
二、前趋势检验(Pre-trend Test)
方法1:可视化检验
最直观的方法:绘图
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# 生成模拟数据
np.random.seed(42)
n_units = 50
n_pre_periods = 8 # 政策前时期
n_post_periods = 5 # 政策后时期
treatment_time = n_pre_periods
data = []
for unit in range(n_units):
treated = 1 if unit >= n_units // 2 else 0
unit_effect = 10 * treated + np.random.normal(0, 3)
for period in range(n_pre_periods + n_post_periods):
time_effect = 2 * period
post = 1 if period >= treatment_time else 0
# 政策效应(仅在政策后)
treatment_effect = 20 * treated * post
y = 50 + unit_effect + time_effect + treatment_effect + np.random.normal(0, 4)
data.append({
'unit': unit,
'period': period,
'treated': treated,
'post': post,
'y': y
})
df = pd.DataFrame(data)
# 计算各组均值
means_by_group = df.groupby(['treated', 'period'])['y'].mean().unstack()
# 绘制趋势图
fig, ax = plt.subplots(figsize=(14, 8))
ax.plot(means_by_group.columns, means_by_group.loc[0], 'o-',
label='对照组', linewidth=3, markersize=8, color='blue')
ax.plot(means_by_group.columns, means_by_group.loc[1], 's-',
label='处理组', linewidth=3, markersize=8, color='red')
# 政策实施时间
ax.axvline(treatment_time - 0.5, color='green', linestyle='--',
linewidth=2, alpha=0.7, label='政策实施时间')
# 添加背景区域
ax.axvspan(-0.5, treatment_time - 0.5, alpha=0.1, color='gray', label='政策前')
ax.axvspan(treatment_time - 0.5, n_pre_periods + n_post_periods - 0.5,
alpha=0.1, color='yellow', label='政策后')
ax.set_xlabel('时期', fontsize=14, fontweight='bold')
ax.set_ylabel('平均结果', fontsize=14, fontweight='bold')
ax.set_title('平行趋势可视化检验:处理组 vs 对照组', 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(" 观察:政策前两组的趋势大致平行")
print(" 观察:政策前没有明显的不同趋势")
print("️ 但是我们需要统计检验来确认")方法2:前趋势检验回归
思路:检验政策前处理组和对照组的时间趋势是否不同
回归方程(仅使用政策前数据):
原假设:(处理组和对照组有相同的时间趋势)
python
import statsmodels.formula.api as smf
# 仅使用政策前数据
df_pre = df[df['period'] < treatment_time].copy()
# 回归:检验交互项
model_pre_trend = smf.ols('y ~ treated + period + treated:period', data=df_pre).fit()
print("=" * 70)
print("前趋势检验回归(Pre-trend Test)")
print("=" * 70)
print(model_pre_trend.summary())
print("\n")
# 提取关键系数
interaction_coef = model_pre_trend.params['treated:period']
interaction_pval = model_pre_trend.pvalues['treated:period']
print("=" * 70)
print("检验结果")
print("=" * 70)
print(f"交互项系数: {interaction_coef:.4f}")
print(f"p值: {interaction_pval:.4f}")
if interaction_pval > 0.05:
print(" 结论:不能拒绝平行趋势假设(p > 0.05)")
else:
print(" 结论:拒绝平行趋势假设(p < 0.05),存在不同趋势")注意事项
- Roth (2022) 的警告:"Pretest with Caution"
- 前趋势检验的检验力(power)有限,不拒绝原假设不代表假设成立
- 显著性检验容易受样本量影响
- 建议:结合可视化和多种检验
三、事件研究图(Event Study)
什么是事件研究
定义:估计政策前后每一期的动态处理效应和平均处理效应
模型方程
其中:
- 表示个体 在时期 距离政策实施有 个时期的距离
- 作为基准期(normalization)
- 是相对时期 的处理效应
解释
- :政策前, 应该接近0(平行趋势检验)
- :政策后, 是政策的动态效应
Python实现完整事件研究
python
from linearmodels.panel import PanelOLS
# 构造相对时间变量
df['rel_time'] = df['period'] - treatment_time
# 仅对处理组构造相对时间
df['rel_time_treated'] = df['rel_time'] * df['treated']
# 生成leads和lags虚拟变量(以t=-1作为基准组)
for k in range(-n_pre_periods, n_post_periods):
if k != -1: # 基准组
df[f'lead_lag_{k}'] = ((df['rel_time_treated'] == k)).astype(int)
# 转换为面板格式
df_panel = df.set_index(['unit', 'period'])
# 构建回归公式
lead_lag_vars = [f'lead_lag_{k}' for k in range(-n_pre_periods, n_post_periods) if k != -1]
# 事件研究回归
model_es = PanelOLS(
dependent=df_panel['y'],
exog=df_panel[lead_lag_vars],
entity_effects=True,
time_effects=True
).fit(cov_type='clustered', cluster_entity=True)
print("=" * 70)
print("事件研究回归结果")
print("=" * 70)
print(model_es.summary)
print("\n")
# 提取系数并构造绘图数据
event_study_results = []
for k in range(-n_pre_periods, n_post_periods):
if k == -1:
# 基准期
event_study_results.append({
'rel_time': k,
'coef': 0,
'ci_lower': 0,
'ci_upper': 0
})
else:
var_name = f'lead_lag_{k}'
coef = model_es.params[var_name]
ci = model_es.conf_int().loc[var_name]
event_study_results.append({
'rel_time': k,
'coef': coef,
'ci_lower': ci[0],
'ci_upper': ci[1]
})
es_df = pd.DataFrame(event_study_results)
# 绘制事件研究图
fig, ax = plt.subplots(figsize=(16, 8))
ax.plot(es_df['rel_time'], es_df['coef'], 'o-',
linewidth=3, markersize=10, color='navy', label='DID 系数', zorder=3)
ax.fill_between(es_df['rel_time'], es_df['ci_lower'], es_df['ci_upper'],
alpha=0.3, color='navy', label='95% 置信区间')
# 零线
ax.axhline(y=0, color='black', linestyle='--', linewidth=1.5, alpha=0.7)
# 政策实施时间
ax.axvline(x=-0.5, color='red', linestyle='--', linewidth=2.5, alpha=0.8, label='政策实施时间')
# 背景区域
ax.axvspan(-n_pre_periods, -0.5, alpha=0.08, color='gray')
ax.axvspan(-0.5, n_post_periods, alpha=0.08, color='yellow')
ax.text(-n_pre_periods/2, ax.get_ylim()[1] * 0.9, '政策前',
fontsize=14, ha='center', bbox=dict(boxstyle='round', facecolor='gray', alpha=0.3))
ax.text(n_post_periods/2, ax.get_ylim()[1] * 0.9, '政策后',
fontsize=14, ha='center', 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('事件研究图:动态处理效应', fontsize=16, fontweight='bold')
ax.legend(fontsize=12, loc='upper left')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 检验前趋势系数
print("=" * 70)
print("前趋势检验(查看政策前系数)")
print("=" * 70)
pre_treatment_coefs = es_df[es_df['rel_time'] < 0]
print(pre_treatment_coefs)
print("\n")
# 联合检验:所有前趋势系数是否都为0
from scipy import stats
pre_coefs = [model_es.params[f'lead_lag_{k}'] for k in range(-n_pre_periods, -1)]
pre_vcov = model_es.cov[lead_lag_vars].loc[
[f'lead_lag_{k}' for k in range(-n_pre_periods, -1)],
[f'lead_lag_{k}' for k in range(-n_pre_periods, -1)]
]
# F检验(联合检验)
# F = (R * beta)' * (R * V * R')^{-1} * (R * beta) / q
# 这里简化使用Wald检验
print("说明:如果所有前趋势系数显著不为0,说明违反了平行趋势假设")
print("建议:查看上图和前趋势系数的置信区间,判断是否违反")事件研究图的解读
关键解读要点
政策前()
- 所有系数应接近0且不显著
- 如果系数显著异于0,说明违反平行趋势
政策实施期()
- 政策效应是否立即显现
- 效应大小是多少
政策后()
- 效应是否持续
- 效应是否随时间增强/减弱/保持稳定
置信区间
- 查看所有政策后时期的置信区间是否都不包含0
- 置信区间变宽说明样本量不足或异质性增加
四、处理违反平行趋势的情况
情况1:选择更相似的对照组
思路:限制对照组为与处理组最相似的单位
python
# 基于政策前的平均结果选择相似对照组
df_pre_avg = df[df['period'] < treatment_time].groupby('unit')['y'].mean().reset_index()
df_pre_avg.columns = ['unit', 'pre_avg']
df = df.merge(df_pre_avg, on='unit')
# 选择与处理组相似的对照组(例如根据pre_avg的分布)
# 这里简化为选取分位数范围内的对照组
control_units = df[(df['treated'] == 0) &
(df['pre_avg'] > df[df['treated'] == 1]['pre_avg'].quantile(0.25)) &
(df['pre_avg'] < df[df['treated'] == 1]['pre_avg'].quantile(0.75))]['unit'].unique()
df_matched = df[(df['treated'] == 1) | (df['unit'].isin(control_units))]
print(f"原始对照组个体数量: {(df['treated'] == 0).sum() // (n_pre_periods + n_post_periods)}")
print(f"筛选后对照组个体数量: {len(control_units)}")情况2:控制线性趋势交互项
模型方程
允许处理组有线性的不同趋势
python
# 生成线性趋势交互项
df['treated_time'] = df['treated'] * df['period']
model_trend = PanelOLS(
df.set_index(['unit', 'period'])['y'],
df.set_index(['unit', 'period'])[['did', 'treated_time']],
entity_effects=True,
time_effects=True
).fit(cov_type='clustered', cluster_entity=True)
print("=" * 70)
print("控制线性趋势的DID")
print("=" * 70)
print(model_trend.summary)警告
- 可能存在过度控制(over-control)问题
- 如果趋势差异是政策的长期影响,控制趋势会低估真实政策效应
情况3:使用合成控制法(Synthetic Control)
思路:使用多个对照组单位的加权组合,构造一个 "合成对照组",在政策前最大化匹配处理组
伪代码示例
python
# 需要安装相关包,例如: pySynth 或 synthdid
print("=" * 70)
print("使用合成控制法(Synthetic Control Method)")
print("=" * 70)
print("当平行趋势假设不成立时的替代方案:")
print("")
print("基本思路:")
print("1. 使用多个对照组单位的加权组合,构造 '合成对照组'")
print("2. 权重的选择使得:合成对照组在政策前最大程度匹配处理组")
print("3. 政策后:比较真实处理组和合成对照组,估计政策效应")
print("")
print("适用场景:")
print("- 处理组只有1个(或极少)个体")
print("- 难以找到完全相似的对照组")
print("- 平行趋势假设较弱")
print("")
print("Python工具包:")
print("- pip install pySynth")
print("- 或使用 R 的 Synth 包")五、完整的平行趋势分析函数
封装代码
python
def did_parallel_trends_analysis(df, treatment_time, outcome, unit_var, time_var, treated_var):
"""
完整的平行趋势分析
参数:
- df: 数据框
- treatment_time: 政策实施时间
- outcome: 结果变量名
- unit_var: 个体变量名
- time_var: 时间变量名
- treated_var: 处理组变量名
"""
import matplotlib.pyplot as plt
import seaborn as sns
from linearmodels.panel import PanelOLS
print("=" * 70)
print("DID 平行趋势完整分析")
print("=" * 70)
# 1. 可视化检验
print("\n[1/4] 绘制趋势图...")
means = df.groupby([treated_var, time_var])[outcome].mean().unstack()
fig, ax = plt.subplots(figsize=(14, 8))
ax.plot(means.columns, means.loc[0], 'o-', label='对照组', linewidth=2.5, markersize=8)
ax.plot(means.columns, means.loc[1], 's-', label='处理组', linewidth=2.5, markersize=8)
ax.axvline(treatment_time - 0.5, color='red', linestyle='--', linewidth=2, label='政策实施时间')
ax.legend(fontsize=12)
ax.set_title('平行趋势可视化检验', fontsize=16, fontweight='bold')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 2. 前趋势检验回归
print("\n[2/4] 运行前趋势检验回归...")
df_pre = df[df[time_var] < treatment_time].copy()
formula = f'{outcome} ~ {treated_var} + {time_var} + {treated_var}:{time_var}'
model_pre = smf.ols(formula, data=df_pre).fit()
interaction_pval = model_pre.pvalues[f'{treated_var}:{time_var}']
print(f"交互项 p值: {interaction_pval:.4f}")
if interaction_pval > 0.05:
print(" 前趋势检验结果: 不能拒绝平行趋势假设")
else:
print(" 前趋势检验结果: 拒绝平行趋势假设")
# 3. 事件研究
print("\n[3/4] 运行事件研究图...")
df['rel_time'] = df[time_var] - treatment_time
df['rel_time_treated'] = df['rel_time'] * df[treated_var]
# 生成leads and lags(具体实现省略,参考前面的完整代码)
# 4. 提供进一步建议
print("\n[4/4] 提供进一步建议:")
print(" - 检查是否需要匹配对照组")
print(" - 检查政策后的动态效应是否符合预期")
print(" - 控制线性趋势或其他协变量")
print(" - 考虑使用合成控制法")
print("\n" + "=" * 70)
print("分析完成")
print("=" * 70)
# 使用示例
# did_parallel_trends_analysis(df, treatment_time=8, outcome='y',
# unit_var='unit', time_var='period', treated_var='treated')六、本节总结
关键要点
平行趋势假设是DID识别的最核心、不可替代的假设
检验方法
- 可视化检验:直观观察
- 前趋势检验回归:统计检验
- 事件研究图:最全面的方法(推荐)
事件研究最佳实践
- 平行趋势检验:观察政策前系数(接近0且不显著)
- 动态效应:观察政策后系数
- 提供更丰富的信息
处理违反平行趋势的方法
- 选择更相似对照组
- 控制线性趋势
- 使用合成控制法
- 考虑其他识别策略
最佳实践
| 步骤 | 方法 |
|---|---|
| 1. 可视化检验 | 绘图观察趋势 |
| 2. 前趋势检验 | 使用统计检验(但要谨慎Roth 2022) |
| 3. 事件研究 | 如何使用完整的动态效应 |
| 4. 提供稳健性 | 结合多种验证方法 |
下一步
进入 第4节:安慰剂检验 学习进一步验证:
- 假政策检验
- 假对照组检验
- 推荐的最佳实践和稳健性检验情况
平行趋势假设是DID的灵魂!
上一节: [9.2 DID 原理](./9.2-DID 原理.md) | 下一节: 9.4 安慰剂检验