11.1 本章介绍(断点回归设计)
局部随机化:当大自然为我们创造准实验
本章目标
完成本章后,你将能够:
- 理解RDD的核心思想和局部随机化原理
- 掌握Sharp RDD和Fuzzy RDD的区别与应用
- 实施RDD的有效性检验(连续性假设、密度检验、协变量平衡)
- 进行带宽选择和稳健性分析
- 使用Python实现RDD分析(rdrobust, statsmodels)
- 复现经典RDD研究(Angrist & Lavy 1999, Lee 2008等)
为什么 RDD 是"最可信"的准实验方法?
从反事实思想说起
Josh Angrist的观点:
"RDD is the most credible quasi-experimental design, because it mimics a randomized experiment in a local neighborhood of the cutoff."
在因果推断中,我们最关心的是反事实问题:
- 观察到的结果:某个学生获得奖学金后的成绩
- 反事实:如果这个学生没有获得奖学金,他的成绩会是多少?
问题:我们永远无法同时观察到两种状态!(根本性因果推断问题)
RCT的解决方案:
- 随机分配处理,确保处理组和对照组完全可比
- 处理组的平均结果 - 对照组的平均结果 = 平均处理效应(ATE)
RDD的巧妙之处: 当我们无法进行随机实验时,如果存在一个断点规则(cutoff rule),在断点附近的个体几乎是"随机"的!
RDD 的核心直觉
场景:大学奖学金与学生成绩
假设一所大学的规则:
- 高考分数 ≥ 600 分 → 获得奖学金
- 高考分数 < 600 分 → 没有奖学金
研究问题:奖学金是否提高学生的大学成绩(GPA)?
直觉:
- 得分599分的学生 vs 得分600分的学生
- 这两个学生几乎完全一样(能力、家庭背景、学习习惯等)
- 唯一的差别:一个刚好越过断点,获得了奖学金
- 因此,他们GPA的差异可以归因于奖学金的因果效应!
图示:理想的 RDD
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
# 设置随机种子
np.random.seed(42)
# 生成驱动变量(Running Variable)
x = np.linspace(-50, 50, 1000)
cutoff = 0
# 生成结果变量
# 断点左侧(未处理)
y_left = 60 + 0.5 * x[x < cutoff] + np.random.normal(0, 3, sum(x < cutoff))
# 断点右侧(处理):跳升10个点
y_right = 70 + 0.5 * x[x >= cutoff] + np.random.normal(0, 3, sum(x >= cutoff))
# 拟合多项式(用于绘制平滑曲线)
from numpy.polynomial import Polynomial
p_left = Polynomial.fit(x[x < cutoff], y_left, deg=2)
p_right = Polynomial.fit(x[x >= cutoff], y_right, deg=2)
# 绘图
fig, ax = plt.subplots(figsize=(14, 8))
# 散点图
ax.scatter(x[x < cutoff], y_left, alpha=0.4, s=20, color='blue', label='未获得奖学金')
ax.scatter(x[x >= cutoff], y_right, alpha=0.4, s=20, color='red', label='获得奖学金')
# 拟合曲线
x_left_smooth = np.linspace(x.min(), cutoff, 100)
x_right_smooth = np.linspace(cutoff, x.max(), 100)
ax.plot(x_left_smooth, p_left(x_left_smooth), color='blue', linewidth=3, label='左侧拟合线')
ax.plot(x_right_smooth, p_right(x_right_smooth), color='red', linewidth=3, label='右侧拟合线')
# 标注断点
ax.axvline(x=cutoff, color='green', linestyle='--', linewidth=2.5, alpha=0.8)
ax.text(cutoff + 2, 45, '断点\n(Cutoff)', fontsize=14, color='green',
fontweight='bold', ha='left')
# 标注 RDD 效应
y_left_at_cutoff = p_left(cutoff)
y_right_at_cutoff = p_right(cutoff)
rdd_effect = y_right_at_cutoff - y_left_at_cutoff
ax.annotate('', xy=(cutoff + 0.5, y_right_at_cutoff),
xytext=(cutoff + 0.5, y_left_at_cutoff),
arrowprops=dict(arrowstyle='<->', color='purple', lw=3.5))
ax.text(cutoff + 3, (y_left_at_cutoff + y_right_at_cutoff) / 2,
f'RDD 效应\nτ = {rdd_effect:.1f}',
fontsize=13, color='purple', fontweight='bold',
bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.4))
# 图例和标签
ax.set_xlabel('驱动变量(高考分数 - 600)', fontsize=14, fontweight='bold')
ax.set_ylabel('结果变量(大学GPA)', fontsize=14, fontweight='bold')
ax.set_title('断点回归设计(RDD)的核心逻辑', fontsize=16, fontweight='bold', pad=20)
ax.legend(loc='upper left', fontsize=12)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('rdd_illustration.png', dpi=300, bbox_inches='tight')
plt.show()关键观察:
- 断点左侧:结果变量沿着一条平滑曲线
- 断点右侧:结果变量沿着另一条平滑曲线
- 断点处:出现明显的跳跃(discontinuity)
- RDD效应:跳跃的大小就是处理效应!
RDD 的数学表达
潜在结果框架(Potential Outcomes Framework)
符号定义:
- :驱动变量(Running Variable),如高考分数
- :断点(Cutoff),如600分
- :处理状态(Treatment Status)
- :潜在结果(如果未处理)
- :潜在结果(如果处理)
- :观察到的结果
Sharp RDD:处理完全由断点决定
定义:如果驱动变量 跨过断点 ,处理状态 确定性地改变,我们称之为 Sharp RDD。
关键假设:连续性假设(Continuity Assumption)
假设在断点处,潜在结果函数是连续的:
白话:如果没有处理,结果变量在断点处应该是平滑的(没有跳跃)。
识别策略:
观察到的结果:
RDD 估计量:
为什么这是因果效应?
根据连续性假设:
重要:RDD 识别的是断点处的平均处理效应,而非整体ATE!
RDD vs RCT:局部随机化的视角
RDD 是"局部的 RCT"
Josh Angrist 的观点:
"RDD can be thought of as a local randomized experiment. Near the cutoff, treatment assignment is 'as-if random'."
直觉:
- 远离断点:高分学生和低分学生差异很大(能力、家庭背景等)
- 接近断点:599分和600分的学生几乎完全一样
- 断点处:处理分配几乎是随机的(谁能刚好得600分有运气成分)
形式化表达:
在断点的小邻域 内,假设:
这类似于RCT中的平衡性:处理组和对照组在所有协变量上都相似。
RDD vs DID:何时使用哪种方法?
| 特征 | RDD | DID |
|---|---|---|
| 数据要求 | 横截面或单期面板 | 多期面板(至少2期) |
| 识别来源 | 断点处的跳跃 | 时间和组别的双重差分 |
| 核心假设 | 连续性假设 | 平行趋势假设 |
| 外部有效性 | 局部效应(断点处) | 可能更广泛 |
| 内部有效性 | 非常高(接近RCT) | 取决于平行趋势 |
| 经典案例 | 奖学金、选举 | 最低工资、环境政策 |
经验法则:
- 如果有清晰的断点规则 → 使用 RDD
- 如果有政策的时空变异 → 使用 DID
- 如果能进行随机分配 → 直接做 RCT!
️ Sharp RDD 的实证实现
线性回归方法
最简单的RDD估计:在断点附近拟合两条线性回归线。
模型:
参数解释:
- :RDD 效应(断点处的跳跃)⭐
- :断点左侧的斜率
- :断点右侧的额外斜率(总斜率 = )
关键:将驱动变量中心化(),这样 就是断点处的效应。
多项式方法
允许结果变量与驱动变量的关系是非线性的:
警告:高阶多项式()容易过拟合!(Gelman & Imbens 2019)
局部线性回归(Local Linear Regression)
现代最佳实践(Calonico, Cattaneo, Titiunik 2014):
- 选择带宽 :只使用 的观测
- 核函数加权:离断点越近,权重越大
- 拟合局部线性回归:
优点:
- 偏差-方差权衡最优
- 对函数形式的假设最少
- 现代软件包(如
rdrobust)自动实现
Python 实现:简单示例
模拟 Sharp RDD 数据
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import statsmodels.formula.api as smf
from scipy import stats
# 设置
np.random.seed(123)
n = 1000
cutoff = 0
# 生成驱动变量(Running Variable)
X = np.random.uniform(-50, 50, n)
# 生成处理状态
D = (X >= cutoff).astype(int)
# 生成结果变量
# 真实的DGP: Y = 50 + 0.5*X + 10*D + noise
# 这意味着处理效应 = 10
true_effect = 10
Y = 50 + 0.5 * X + true_effect * D + np.random.normal(0, 5, n)
# 创建数据框
df = pd.DataFrame({
'X': X,
'D': D,
'Y': Y,
'X_centered': X - cutoff
})
print("=" * 70)
print("Sharp RDD 模拟数据")
print("=" * 70)
print(f"样本量: {n}")
print(f"断点: {cutoff}")
print(f"真实处理效应: {true_effect}")
print(f"处理组人数: {D.sum()} ({D.sum()/n*100:.1f}%)")
print("\n数据预览:")
print(df.head(10))可视化:散点图 + 拟合线
# 分组拟合
df_left = df[df['D'] == 0]
df_right = df[df['D'] == 1]
# OLS拟合
from sklearn.linear_model import LinearRegression
lr_left = LinearRegression().fit(df_left[['X_centered']], df_left['Y'])
lr_right = LinearRegression().fit(df_right[['X_centered']], df_right['Y'])
# 预测
X_left_range = np.linspace(df_left['X_centered'].min(), 0, 100).reshape(-1, 1)
X_right_range = np.linspace(0, df_right['X_centered'].max(), 100).reshape(-1, 1)
Y_left_pred = lr_left.predict(X_left_range)
Y_right_pred = lr_right.predict(X_right_range)
# 绘图
fig, ax = plt.subplots(figsize=(14, 8))
# 散点图(使用binning减少视觉混乱)
bins = 20
df['X_bin'] = pd.cut(df['X_centered'], bins=bins)
df_binned = df.groupby(['X_bin', 'D']).agg({'Y': 'mean', 'X_centered': 'mean'}).reset_index()
df_binned_left = df_binned[df_binned['D'] == 0]
df_binned_right = df_binned[df_binned['D'] == 1]
ax.scatter(df_binned_left['X_centered'], df_binned_left['Y'],
s=100, alpha=0.6, color='blue', edgecolors='black', linewidths=1.5,
label='未处理(binned means)')
ax.scatter(df_binned_right['X_centered'], df_binned_right['Y'],
s=100, alpha=0.6, color='red', edgecolors='black', linewidths=1.5,
label='已处理(binned means)')
# 拟合线
ax.plot(X_left_range, Y_left_pred, color='blue', linewidth=3, label='左侧拟合线')
ax.plot(X_right_range, Y_right_pred, color='red', linewidth=3, label='右侧拟合线')
# 断点
ax.axvline(x=0, color='green', linestyle='--', linewidth=2.5, alpha=0.7)
# 标注效应
y_left_at_cutoff = lr_left.predict([[0]])[0]
y_right_at_cutoff = lr_right.predict([[0]])[0]
estimated_effect = y_right_at_cutoff - y_left_at_cutoff
ax.annotate('', xy=(0.5, y_right_at_cutoff), xytext=(0.5, y_left_at_cutoff),
arrowprops=dict(arrowstyle='<->', color='purple', lw=3))
ax.text(1, (y_left_at_cutoff + y_right_at_cutoff) / 2,
f'估计效应\n= {estimated_effect:.2f}',
fontsize=12, color='purple', fontweight='bold',
bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.3))
ax.set_xlabel('X - Cutoff', fontsize=13, fontweight='bold')
ax.set_ylabel('Y', fontsize=13, fontweight='bold')
ax.set_title(f'Sharp RDD 示例(真实效应 = {true_effect})',
fontsize=15, fontweight='bold')
ax.legend(loc='upper left', fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()回归估计
# 方法1:全样本线性RDD
model1 = smf.ols('Y ~ D + X_centered + D:X_centered', data=df).fit()
print("\n" + "=" * 70)
print("方法1:全样本线性RDD")
print("=" * 70)
print(model1.summary().tables[1])
print(f"\n估计的RDD效应: {model1.params['D']:.3f}")
print(f"标准误: {model1.bse['D']:.3f}")
print(f"95% 置信区间: [{model1.conf_int().loc['D', 0]:.3f}, {model1.conf_int().loc['D', 1]:.3f}]")
# 方法2:带宽限制(只使用断点附近的观测)
bandwidth = 20
df_local = df[np.abs(df['X_centered']) <= bandwidth].copy()
model2 = smf.ols('Y ~ D + X_centered + D:X_centered', data=df_local).fit()
print("\n" + "=" * 70)
print(f"方法2:局部线性RDD(带宽 = {bandwidth})")
print("=" * 70)
print(f"使用观测数: {len(df_local)} / {len(df)} ({len(df_local)/len(df)*100:.1f}%)")
print(model2.summary().tables[1])
print(f"\n估计的RDD效应: {model2.params['D']:.3f}")
print(f"标准误: {model2.bse['D']:.3f}")
# 比较
print("\n" + "=" * 70)
print("效应估计比较")
print("=" * 70)
print(f"真实效应: {true_effect:.3f}")
print(f"全样本估计: {model1.params['D']:.3f} (SE = {model1.bse['D']:.3f})")
print(f"局部估计 (h={bandwidth}): {model2.params['D']:.3f} (SE = {model2.bse['D']:.3f})")输出解读:
- 两种方法都应该接近真实效应10
- 局部估计的标准误通常更大(样本量更小)
- 但局部估计的偏差更小(函数形式假设更弱)
Fuzzy RDD:处理不完美的断点
什么是 Fuzzy RDD?
在现实中,断点规则可能不完美:
- Sharp RDD:,
- Fuzzy RDD:,但不是0或1
例子:
- 大学录取:分数线600分,但有特殊情况(体育特长、少数民族加分等)
- 医疗保险:年龄65岁自动获得Medicare,但有些人提前购买
Fuzzy RDD 的识别
思路:使用断点作为工具变量(IV)!
两阶段回归:
第一阶段:用断点预测处理状态
第二阶段:用预测的处理状态估计效应
Fuzzy RDD 估计量:
解释:
- 分子:结果变量在断点处的跳跃(Reduced Form)
- 分母:处理状态在断点处的跳跃(First Stage)
- 比值:局部平均处理效应(LATE)
与IV的联系: Fuzzy RDD 本质上是IV估计,其中断点()作为工具变量!
RDD 的经典应用预览
案例 1:Thistlethwaite & Campbell (1960) - RDD的诞生
研究问题:获得国家优秀奖学金(National Merit Award)是否影响学生未来获得其他奖学金?
设计:
- 断点:全国考试分数的某个阈值
- 处理:获得优秀奖学金
- 结果:后续获得的奖学金数量
发现:RDD 效应显著为正
历史意义:这是RDD方法的首次应用(1960年)!
案例 2:Angrist & Lavy (1999) - 班级规模与学生成绩
研究问题:减少班级规模是否提高学生成绩?
背景:
- 以色列有一个规则(Maimonides' Rule):班级人数不得超过40人
- 如果学校有41名学生 → 必须分成2个班(每班≈20人)
- 如果学校有40名学生 → 1个班(40人)
设计:
- 驱动变量:学校的学生总数
- 断点:40, 80, 120, ... (40的倍数)
- 处理:班级规模(由规则决定)
- 结果:标准化考试成绩
发现:班级规模减少1人 → 成绩提高0.1-0.2个标准差
创新:这是 Fuzzy RDD 的经典应用(因为规则不是完美执行)
案例 3:Lee (2008) - 选举优势与连任
研究问题:现任议员的身份是否带来连任优势?
设计:
- 断点:选举得票率 = 50%
- 处理:成为现任议员
- 结果:下次选举的得票率
关键直觉:
- 得票率49.9%的候选人 vs 得票率50.1%的候选人
- 这两人几乎一样(政治实力、资金、选民支持等)
- 唯一差别:一个当选,一个落选
发现:现任优势巨大(约40个百分点)!
RDD 的核心假设
假设 1:连续性假设(Continuity Assumption)⭐
假设:在断点处,除了处理状态,所有其他因素都是连续的。
数学表达:
白话:如果没有处理,结果变量在断点处不会跳跃。
如何检验?(第3节详细讨论)
- 协变量平衡检验:检查断点两侧的协变量(年龄、性别等)是否平衡
- 密度检验(McCrary Test):检查驱动变量的密度在断点处是否平滑
- 安慰剂检验:使用假断点进行检验
假设 2:无精确操控(No Precise Manipulation)
假设:个体不能精确地操控驱动变量,使自己刚好越过断点。
威胁:
- 考试作弊:学生知道600分是断点,想办法作弊到刚好600分
- 选举舞弊:候选人操纵选票,使得票率刚好超过50%
- 政策游说:企业游说政府,使规模刚好低于监管阈值
如何检验?
- McCrary 密度检验:检查驱动变量在断点处是否有异常的堆积
假设 3:局部排他性(Local Exclusion)
假设:驱动变量只通过处理影响结果(在断点附近)。
威胁:
- 如果高考分数本身(除了奖学金)也直接影响GPA(如自信心),RDD会有偏差
经验法则:选择"外生"的驱动变量(如出生日期、抽签号码)
本章结构
第 1 节:本章介绍(当前)
- RDD的核心思想和反事实框架
- Sharp RDD vs Fuzzy RDD
- 与RCT和DID的比较
- Python基础实现
第 2 节:RDD原理与识别策略
- Sharp RDD的数学推导
- Fuzzy RDD和工具变量
- 局部平均处理效应(LATE)
- 线性 vs 非参数方法
第 3 节:连续性假设与有效性检验
- 连续性假设的检验
- 协变量平衡检验
- 密度检验(McCrary Test)
- 安慰剂检验
第 4 节:带宽选择与稳健性检验
- 最优带宽选择(IK, CCT)
- 敏感性分析
- 多项式阶数的选择
- Donut-hole RDD
第 5 节:经典案例和Python实现
- Angrist & Lavy (1999) 班级规模
- Lee (2008) 选举优势
- Carpenter & Dobkin (2009) 最低饮酒年龄
- 使用 rdrobust 包的最佳实践
第 6 节:本章小结
- RDD方法总结
- 常见陷阱和最佳实践
- 练习题
- 文献推荐
️ Python 工具包
核心库
| 库 | 主要功能 | 安装 |
|---|---|---|
| pandas | 数据处理 | pip install pandas |
| numpy | 数值计算 | pip install numpy |
| statsmodels | OLS回归 | pip install statsmodels |
| rdrobust | RDD最优带宽和稳健推断 | pip install rdrobust |
| rddtools | RDD工具集 | (需要从源安装) |
| 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 scipy import stats
# 中文字体设置(根据操作系统选择)
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# 设置样式
sns.set_style("whitegrid")
plt.rcParams['figure.figsize'] = (12, 7)
pd.set_option('display.float_format', '{:.4f}'.format)rdrobust 包的安装
# Python 版本
pip install rdrobust
# 或者使用 conda
conda install -c conda-forge rdrobust使用示例:
from rdrobust import rdrobust, rdbwselect, rdplot
# 自动带宽选择和稳健推断
result = rdrobust(y=Y, x=X, c=cutoff)
print(result)
# 绘制RDD图
rdplot(y=Y, x=X, c=cutoff, nbins=20)必读文献
奠基性论文
Thistlethwaite, D. L., & Campbell, D. T. (1960). "Regression-discontinuity analysis: An alternative to the ex post facto experiment." Journal of Educational Psychology, 51(6), 309.
- RDD方法的诞生
Hahn, J., Todd, P., & Van der Klaauw, W. (2001). "Identification and Estimation of Treatment Effects with a Regression-Discontinuity Design." Econometrica, 69(1), 201-209.
- RDD的现代识别理论
Lee, D. S., & Lemieux, T. (2010). "Regression Discontinuity Designs in Economics." Journal of Economic Literature, 48(2), 281-355.
- 必读综述,RDD的圣经
方法论突破
Imbens, G., & Kalyanaraman, K. (2012). "Optimal Bandwidth Choice for the Regression Discontinuity Estimator." Review of Economic Studies, 79(3), 933-959.
- 最优带宽选择(IK方法)
Calonico, S., Cattaneo, M. D., & Titiunik, R. (2014). "Robust Nonparametric Confidence Intervals for Regression-Discontinuity Designs." Econometrica, 82(6), 2295-2326.
- 稳健推断(CCT方法)
Gelman, A., & Imbens, G. (2019). "Why High-Order Polynomials Should Not Be Used in Regression Discontinuity Designs." Journal of Business & Economic Statistics, 37(3), 447-456.
- 警告:不要用高阶多项式!
经典应用
Angrist, J. D., & Lavy, V. (1999). "Using Maimonides' Rule to Estimate the Effect of Class Size on Scholastic Achievement." Quarterly Journal of Economics, 114(2), 533-575.
Lee, D. S. (2008). "Randomized Experiments from Non-random Selection in U.S. House Elections." Journal of Econometrics, 142(2), 675-697.
推荐教材
- Angrist & Pischke (2009). Mostly Harmless Econometrics, Chapter 6
- Cunningham (2021). Causal Inference: The Mixtape, Chapter 6
- Huntington-Klein (2022). The Effect, Chapter 20
准备好了吗?
RDD 是准实验设计中最接近随机实验的方法。掌握它,你将能够:
- 在缺乏随机实验的情况下识别因果效应
- 利用政策规则和自然断点进行研究
- 发表高质量的因果推断研究
记住核心思想:
"In the neighborhood of the cutoff, RDD is as good as a randomized experiment. The discontinuity is your friend." — Joshua Angrist
让我们开始深入学习 第2节:RDD原理与识别策略!
局部随机化,因果推断的利器!