Skip to content

9.3 平行趋势假设(Parallel Trends Assumption)

DID成立的核心前提条件


本节目标

  • 深入理解平行趋势假设的定义和直觉含义
  • 掌握前趋势检验(Pre-trend Test)的方法
  • 学会绘制事件研究图(Event Study)
  • 掌握多种检验和标准误的聚类调整
  • 学会处理违反平行趋势的情况
  • 了解稳健性检验和替代方法

一、平行趋势假设的定义

为什么平行趋势是DID成立的核心

数学表达:DID的识别假设

含义:在没有政策干预的反事实情形下,处理组和对照组的结果变量趋势是平行的。

关键理解

  1. 我们不需要处理组和对照组的水平相同——允许存在固定差异
  2. 重要的是在政策前,两组的变化趋势必须相同
  3. 平行趋势假设是关于反事实的假设——不可直接观测

图示示例

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. 场景1(绿色):完全满足DID,估计无偏
  2. 场景2(红色):政策前趋势已不同,DID估计量有偏
  3. 场景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),存在不同趋势")

注意事项

  1. Roth (2022) 的警告:"Pretest with Caution"
    • 前趋势检验的检验力(power)有限,不拒绝原假设不代表假设成立
    • 显著性检验容易受样本量影响
  2. 建议:结合可视化和多种检验

三、事件研究图(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("建议:查看上图和前趋势系数的置信区间,判断是否违反")

事件研究图的解读

关键解读要点

  1. 政策前(

    • 所有系数应接近0且不显著
    • 如果系数显著异于0,说明违反平行趋势
  2. 政策实施期(

    • 政策效应是否立即显现
    • 效应大小是多少
  3. 政策后(

    • 效应是否持续
    • 效应是否随时间增强/减弱/保持稳定
  4. 置信区间

    • 查看所有政策后时期的置信区间是否都不包含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')

六、本节总结

关键要点

  1. 平行趋势假设是DID识别的最核心、不可替代的假设

  2. 检验方法

    • 可视化检验:直观观察
    • 前趋势检验回归:统计检验
    • 事件研究图:最全面的方法(推荐)
  3. 事件研究最佳实践

    • 平行趋势检验:观察政策前系数(接近0且不显著)
    • 动态效应:观察政策后系数
    • 提供更丰富的信息
  4. 处理违反平行趋势的方法

    • 选择更相似对照组
    • 控制线性趋势
    • 使用合成控制法
    • 考虑其他识别策略

最佳实践

步骤方法
1. 可视化检验绘图观察趋势
2. 前趋势检验使用统计检验(但要谨慎Roth 2022)
3. 事件研究如何使用完整的动态效应
4. 提供稳健性结合多种验证方法

下一步

进入 第4节:安慰剂检验 学习进一步验证:

  • 假政策检验
  • 假对照组检验
  • 推荐的最佳实践和稳健性检验情况

平行趋势假设是DID的灵魂!


上一节: [9.2 DID 原理](./9.2-DID 原理.md) | 下一节: 9.4 安慰剂检验

基于 MIT 许可证发布。内容版权归作者所有。