Skip to content

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)?

直觉

  1. 得分599分的学生 vs 得分600分的学生
  2. 这两个学生几乎完全一样(能力、家庭背景、学习习惯等)
  3. 唯一的差别:一个刚好越过断点,获得了奖学金
  4. 因此,他们GPA的差异可以归因于奖学金的因果效应

图示:理想的 RDD

python
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()

关键观察

  1. 断点左侧:结果变量沿着一条平滑曲线
  2. 断点右侧:结果变量沿着另一条平滑曲线
  3. 断点处:出现明显的跳跃(discontinuity)
  4. 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'."

直觉

  1. 远离断点:高分学生和低分学生差异很大(能力、家庭背景等)
  2. 接近断点:599分和600分的学生几乎完全一样
  3. 断点处:处理分配几乎是随机的(谁能刚好得600分有运气成分)

形式化表达

在断点的小邻域 内,假设:

这类似于RCT中的平衡性:处理组和对照组在所有协变量上都相似。

RDD vs DID:何时使用哪种方法?

特征RDDDID
数据要求横截面或单期面板多期面板(至少2期)
识别来源断点处的跳跃时间和组别的双重差分
核心假设连续性假设平行趋势假设
外部有效性局部效应(断点处)可能更广泛
内部有效性非常高(接近RCT)取决于平行趋势
经典案例奖学金、选举最低工资、环境政策

经验法则

  • 如果有清晰的断点规则 → 使用 RDD
  • 如果有政策的时空变异 → 使用 DID
  • 如果能进行随机分配 → 直接做 RCT!

️ Sharp RDD 的实证实现

线性回归方法

最简单的RDD估计:在断点附近拟合两条线性回归线。

模型

参数解释

  • RDD 效应(断点处的跳跃)⭐
  • :断点左侧的斜率
  • :断点右侧的额外斜率(总斜率 =

关键:将驱动变量中心化),这样 就是断点处的效应。

多项式方法

允许结果变量与驱动变量的关系是非线性的:

警告:高阶多项式()容易过拟合!(Gelman & Imbens 2019)

局部线性回归(Local Linear Regression)

现代最佳实践(Calonico, Cattaneo, Titiunik 2014):

  1. 选择带宽 :只使用 的观测
  2. 核函数加权:离断点越近,权重越大
  3. 拟合局部线性回归

优点

  • 偏差-方差权衡最优
  • 对函数形式的假设最少
  • 现代软件包(如 rdrobust)自动实现

Python 实现:简单示例

模拟 Sharp RDD 数据

python
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))

可视化:散点图 + 拟合线

python
# 分组拟合
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()

回归估计

python
# 方法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

例子

  1. 大学录取:分数线600分,但有特殊情况(体育特长、少数民族加分等)
  2. 医疗保险:年龄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节详细讨论)

  1. 协变量平衡检验:检查断点两侧的协变量(年龄、性别等)是否平衡
  2. 密度检验(McCrary Test):检查驱动变量的密度在断点处是否平滑
  3. 安慰剂检验:使用假断点进行检验

假设 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
statsmodelsOLS回归pip install statsmodels
rdrobustRDD最优带宽和稳健推断pip install rdrobust
rddtoolsRDD工具集(需要从源安装)
matplotlib可视化pip install matplotlib
seaborn高级可视化pip install seaborn

基础设置

python
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 包的安装

bash
# Python 版本
pip install rdrobust

# 或者使用 conda
conda install -c conda-forge rdrobust

使用示例

python
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)

必读文献

奠基性论文

  1. 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方法的诞生
  2. 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的现代识别理论
  3. Lee, D. S., & Lemieux, T. (2010). "Regression Discontinuity Designs in Economics." Journal of Economic Literature, 48(2), 281-355.

    • 必读综述,RDD的圣经

方法论突破

  1. Imbens, G., & Kalyanaraman, K. (2012). "Optimal Bandwidth Choice for the Regression Discontinuity Estimator." Review of Economic Studies, 79(3), 933-959.

    • 最优带宽选择(IK方法)
  2. Calonico, S., Cattaneo, M. D., & Titiunik, R. (2014). "Robust Nonparametric Confidence Intervals for Regression-Discontinuity Designs." Econometrica, 82(6), 2295-2326.

    • 稳健推断(CCT方法)
  3. 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.

    • 警告:不要用高阶多项式!

经典应用

  1. 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.

  2. Lee, D. S. (2008). "Randomized Experiments from Non-random Selection in U.S. House Elections." Journal of Econometrics, 142(2), 675-697.

推荐教材

  1. Angrist & Pischke (2009). Mostly Harmless Econometrics, Chapter 6
  2. Cunningham (2021). Causal Inference: The Mixtape, Chapter 6
  3. 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原理与识别策略


局部随机化,因果推断的利器!

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