11.2 RDD 原理与识别策略
"Regression discontinuity is one of the most credible quasi-experimental strategies.""断点回归是最可信的准实验策略之一。"— Guido Imbens & Thomas Lemieux, RDD Review Authors (RDD综述作者)
从潜在结果框架到局部随机化:RDD 的理论基础
本节概要
在本节中,我们将深入探讨:
- Sharp RDD 的严格数学推导
- Fuzzy RDD 与工具变量(IV)的关系
- 局部平均处理效应(LATE)的含义
- 参数 vs 非参数估计方法
- 带宽选择的权衡
Sharp RDD 的识别理论
潜在结果框架的回顾
Rubin 因果模型中,每个个体 都有两个潜在结果:
- :如果接受处理
- :如果不接受处理
个体处理效应:
根本性问题:我们永远无法同时观察到 和 !
观察到的结果:
其中 是处理指示变量。
RDD 的识别逻辑
核心想法:在断点 附近,处理分配"准随机"。
Sharp RDD 的分配规则:
关键假设:连续性假设(Continuity Assumption)
对于 ,假设:
直觉:
- 在断点 处,潜在结果函数 是连续的
- 换句话说,驱动变量 的微小变化不会导致潜在结果的跳跃
识别策略的推导
观察到的条件期望:
在断点右侧(,所有人都接受处理):
在断点左侧(,所有人都不接受处理):
RDD 估计量:
为什么这是因果效应?
根据连续性假设:
关键点:
- RDD 估计的是断点处的处理效应,而非总体ATE
- 这是一个局部效应(Local Average Treatment Effect, LATE)
- 外推到其他 值需要额外的假设(如效应同质性)
Fuzzy RDD:不完美的断点规则
Fuzzy RDD 的动机
在现实中,处理分配规则可能不完美:
例子 1:大学录取
- 规则:高考分数 ≥ 600 → 录取
- 现实:有特殊录取(体育特长、艺术生、校长推荐等)
- 结果:,
例子 2:医疗保险(Medicare)
- 规则:年龄 ≥ 65 → 自动获得Medicare
- 现实:有些人提前购买,有些人选择不参加
- 结果:处理在断点处跳跃,但不是从0到1
Fuzzy RDD 的定义
Sharp RDD:
Fuzzy RDD:
但不一定是完美的0和1。
关键条件:
即:处理概率在断点处有跳跃。
Fuzzy RDD 的识别:工具变量方法
核心思想:将断点指示变量作为工具变量(IV)!
定义工具变量:
IV 的三个关键条件:
相关性(Relevance): 与处理 相关
排他性(Exclusion): 只通过 影响 (控制 后)
单调性(Monotonicity):跨过断点不会使任何人从处理变为不处理
Fuzzy RDD 估计量:
简化形式(Reduced Form):
第一阶段(First Stage):
Fuzzy RDD 效应(Wald 估计量):
Fuzzy RDD 的因果解释
局部平均处理效应(LATE):
Fuzzy RDD 估计的是顺从者(Compliers)的处理效应。
四类个体(基于潜在处理状态):
- Always-takers:(无论是否跨断点都接受处理)
- Never-takers:(无论是否跨断点都不接受处理)
- Compliers:(只有跨过断点才接受处理)
- Defiers:(跨过断点反而不接受处理)
单调性假设排除了 Defiers 的存在。
LATE 定理(Imbens & Angrist 1994):
解释:
- Fuzzy RDD 估计的是顺从者(那些因为跨过断点而改变处理状态的人)的平均处理效应
- 对于 Always-takers 和 Never-takers,我们无法识别他们的处理效应
- 如果 Compliers 与总体不同,LATE ≠ ATE
参数 vs 非参数估计方法
方法 1:全局多项式回归
模型( 阶多项式):
优点:
- 简单易懂
- 使用所有数据,效率高
缺点:
- 对函数形式假设敏感(选择 很关键)
- 远离断点的数据会影响估计(可能引入偏差)
- Gelman & Imbens (2019) 警告:不要使用 的多项式!
Python 实现:
import statsmodels.formula.api as smf
# 二阶多项式
model = smf.ols('Y ~ D + X_c + I(X_c**2) + D:X_c + D:I(X_c**2)', data=df).fit()
print(model.summary())方法 2:局部线性回归(推荐)
思路:只使用断点附近 的数据,拟合线性模型。
模型:
带宽(Bandwidth):
- 太小:方差大(数据少)
- 太大:偏差大(函数形式假设可能错误)
优点:
- 对函数形式假设最弱(只需局部线性)
- 理论上最优(Fan & Gijbels 1996)
- 现代软件(如
rdrobust)自动选择最优带宽
Python 实现:
# 手动选择带宽
h = 10
df_local = df[np.abs(df['X_c']) <= h]
model_local = smf.ols('Y ~ D + X_c + D:X_c', data=df_local).fit()
# 使用 rdrobust(自动带宽)
from rdrobust import rdrobust
result = rdrobust(y=df['Y'], x=df['X'], c=cutoff)
print(result)方法 3:核加权局部线性回归
进一步改进:给离断点更近的观测更大的权重。
三角核函数(Triangular Kernel):
权重:
加权回归:
Python 实现:
from statsmodels.regression.linear_model import WLS
# 计算权重
df['weight'] = np.maximum(0, 1 - np.abs(df['X_c']) / h)
# 加权最小二乘
model_wls = smf.wls('Y ~ D + X_c + D:X_c', data=df, weights=df['weight']).fit()
print(model_wls.summary())️ 带宽选择的权衡
偏差-方差权衡(Bias-Variance Tradeoff)
小带宽 :
- 优点:偏差小(函数近似更准确)
- 缺点:方差大(样本量少,估计不稳定)
大带宽 :
- 优点:方差小(样本量大,估计稳定)
- 缺点:偏差大(可能违反局部线性假设)
均方误差(MSE):
最优带宽 :最小化 MSE。
最优带宽选择方法
1. Imbens-Kalyanaraman (IK) 方法(2012)
思路:基于均方误差(MSE)的渐近展开,推导最优带宽。
公式(简化版):
其中:
- :残差方差
- :驱动变量在断点处的密度
- :左侧潜在结果函数的二阶导数
Python 实现:
from rdrobust import rdbwselect
# 自动选择 IK 带宽
bw = rdbwselect(y=df['Y'], x=df['X'], c=cutoff, bwselect='mserd')
print(f"IK Bandwidth: {bw.bws[0]}")2. Calonico-Cattaneo-Titiunik (CCT) 方法(2014)
改进:
- 考虑有限样本的偏差校正
- 提供稳健的置信区间
两种带宽:
- 主带宽(Main Bandwidth):用于估计点估计
- 偏差带宽(Bias Bandwidth):用于估计和校正偏差
Python 实现(rdrobust 默认使用 CCT):
from rdrobust import rdrobust
# CCT 方法(默认)
result_cct = rdrobust(y=df['Y'], x=df['X'], c=cutoff)
print(result_cct)交叉验证(Cross-Validation)
留一法交叉验证:
- 对每个观测 ,移除它
- 用剩余数据拟合模型(使用带宽 )
- 预测
- 计算预测误差:
- 重复所有观测,选择最小化 的
注意:只使用断点一侧的数据进行交叉验证(避免使用跳跃本身)。
Python 完整示例:Sharp vs Fuzzy RDD
示例 1:Sharp RDD
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.formula.api as smf
from rdrobust import rdrobust, rdplot
# 设置
np.random.seed(42)
n = 2000
c = 0
# 生成驱动变量
X = np.random.normal(0, 10, n)
# Sharp RDD:处理完全由断点决定
D = (X >= c).astype(int)
# 生成结果变量
# DGP: Y = 50 + 0.5*X + 0.01*X^2 + 10*D + noise
true_effect = 10
Y = 50 + 0.5 * X + 0.01 * X**2 + true_effect * D + np.random.normal(0, 5, n)
df = pd.DataFrame({'X': X, 'D': D, 'Y': Y, 'X_c': X - c})
# 估计(使用 rdrobust)
result_sharp = rdrobust(y=df['Y'], x=df['X'], c=c)
print("=" * 70)
print("Sharp RDD 结果")
print("=" * 70)
print(result_sharp)
# 可视化
rdplot(y=df['Y'], x=df['X'], c=c,
title='Sharp RDD',
x_label='Running Variable',
y_label='Outcome')
plt.show()示例 2:Fuzzy RDD
# Fuzzy RDD:处理不完美
# 跨过断点,处理概率从0.2跳到0.8
np.random.seed(42)
# 潜在处理状态
prob_treat = 0.2 + 0.6 * (X >= c) # 左侧20%,右侧80%
D_fuzzy = np.random.binomial(1, prob_treat)
# 生成结果变量(真实效应还是10)
Y_fuzzy = 50 + 0.5 * X + 0.01 * X**2 + true_effect * D_fuzzy + np.random.normal(0, 5, n)
df_fuzzy = pd.DataFrame({'X': X, 'D': D_fuzzy, 'Y': Y_fuzzy, 'X_c': X - c})
# Fuzzy RDD 估计(自动检测并使用IV)
result_fuzzy = rdrobust(y=df_fuzzy['Y'], x=df_fuzzy['X'], c=c, fuzzy=df_fuzzy['D'])
print("\n" + "=" * 70)
print("Fuzzy RDD 结果")
print("=" * 70)
print(result_fuzzy)
# 检查第一阶段(First Stage)
print("\n第一阶段检验:")
first_stage = rdrobust(y=df_fuzzy['D'], x=df_fuzzy['X'], c=c)
print(f"处理概率的跳跃: {first_stage.coef[0]:.3f}")
print(f"F-统计量: {first_stage.z[0]**2:.2f}")示例 3:不同带宽的敏感性分析
# 尝试不同带宽
bandwidths = [5, 10, 15, 20, 25]
results = []
for h in bandwidths:
df_local = df[np.abs(df['X_c']) <= h]
model = smf.ols('Y ~ D + X_c + D:X_c', data=df_local).fit()
results.append({
'bandwidth': h,
'effect': model.params['D'],
'se': model.bse['D'],
'n': len(df_local)
})
results_df = pd.DataFrame(results)
print("\n" + "=" * 70)
print("带宽敏感性分析")
print("=" * 70)
print(results_df.to_string(index=False))
# 可视化
fig, ax = plt.subplots(figsize=(12, 6))
ax.errorbar(results_df['bandwidth'], results_df['effect'],
yerr=1.96 * results_df['se'],
fmt='o-', capsize=5, capthick=2, linewidth=2, markersize=8)
ax.axhline(y=true_effect, color='red', linestyle='--', linewidth=2,
label=f'True Effect = {true_effect}')
ax.set_xlabel('Bandwidth', fontsize=13, fontweight='bold')
ax.set_ylabel('Estimated RDD Effect', fontsize=13, fontweight='bold')
ax.set_title('Sensitivity to Bandwidth Choice', fontsize=15, fontweight='bold')
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()RDD 的统计推断
标准误的计算
异方差稳健标准误(HC1/HC2):
model = smf.ols('Y ~ D + X_c + D:X_c', data=df_local).fit(cov_type='HC2')
print(model.summary())聚类标准误(如果有聚类结构):
# 假设数据在学校层面聚类
model_cluster = smf.ols('Y ~ D + X_c + D:X_c', data=df_local).fit(
cov_type='cluster', cov_kwds={'groups': df_local['school_id']}
)置信区间的构建
常规置信区间(基于渐近正态性):
稳健置信区间(CCT 方法,考虑有限样本偏差):
from rdrobust import rdrobust
# CCT 稳健置信区间
result = rdrobust(y=df['Y'], x=df['X'], c=c)
print(f"点估计: {result.coef[0]:.3f}")
print(f"稳健95% CI: [{result.ci[0][0]:.3f}, {result.ci[0][1]:.3f}]")Bootstrap 置信区间:
from scipy.stats import bootstrap
def rdd_estimator(data, indices):
"""RDD 估计量(用于 bootstrap)"""
df_boot = data.iloc[indices]
df_boot_local = df_boot[np.abs(df_boot['X_c']) <= h]
model = smf.ols('Y ~ D + X_c + D:X_c', data=df_boot_local).fit()
return model.params['D']
# Bootstrap(1000次重抽样)
n_boot = 1000
boot_estimates = []
for _ in range(n_boot):
indices = np.random.choice(len(df), len(df), replace=True)
boot_estimates.append(rdd_estimator(df, indices))
boot_estimates = np.array(boot_estimates)
ci_lower = np.percentile(boot_estimates, 2.5)
ci_upper = np.percentile(boot_estimates, 97.5)
print(f"Bootstrap 95% CI: [{ci_lower:.3f}, {ci_upper:.3f}]")关键要点
Sharp RDD
- 识别条件:连续性假设(潜在结果在断点处连续)
- 估计量:断点处观察结果的跳跃
- 因果解释:断点处的局部平均处理效应(LATE)
- 最佳实践:使用局部线性回归 + 自动带宽选择(CCT)
Fuzzy RDD
- 本质:工具变量估计,断点作为IV
- 识别条件:连续性 + IV假设(相关性、排他性、单调性)
- 估计量:Wald 估计量(结果跳跃 / 处理跳跃)
- 因果解释:顺从者(Compliers)的LATE
- 检验:第一阶段要足够强(F > 10)
带宽选择
- 权衡:偏差(小带宽)vs 方差(大带宽)
- 最优方法:IK 或 CCT(自动数据驱动)
- 稳健性:报告多个带宽下的结果
本节总结
在本节中,我们学习了:
- Sharp RDD 的严格数学推导(从潜在结果框架出发)
- Fuzzy RDD 与工具变量的深刻联系
- 局部平均处理效应(LATE)的含义和局限性
- 参数(多项式)vs 非参数(局部线性)方法的权衡
- 带宽选择的理论和实践(IK, CCT)
- 完整的Python实现和稳健推断
下一步:在 第3节 中,我们将学习如何检验RDD的核心假设,包括连续性假设、密度检验(McCrary Test)和协变量平衡。
理论扎实,实证才能可信!