11.4 带宽选择与稳健性检验
"Bandwidth choice involves a fundamental bias-variance tradeoff.""带宽选择涉及基本的偏差-方差权衡。"— Guido Imbens & Karthik Kalyanaraman, Optimal Bandwidth Authors (最优带宽作者)
让你的RDD估计经得起审稿人的考验
本节概要
带宽选择是RDD实证分析中最关键的决策之一。本节将介绍:
- 最优带宽选择的理论与方法(IK, CCT)
- 敏感性分析(Sensitivity Analysis)
- 多项式阶数的选择
- Donut-hole RDD(排除断点附近的观测)
- 完整的稳健性检验流程
️ 带宽选择的核心权衡
偏差-方差权衡(Bias-Variance Tradeoff)
均方误差(MSE):
小带宽 :
- 偏差小:局部线性近似更准确
- 方差大:样本量少,估计不稳定
大带宽 :
- 方差小:样本量大,估计稳定
- 偏差大:可能违反局部线性假设
最优带宽 :
渐近理论(Asymptotic Theory)
Fan & Gijbels (1996) 推导出最优带宽的渐近形式:
直觉:
- 样本量越大,最优带宽越小(可以用更局部的数据)
- 带宽以 的速度收缩(比参数估计的 慢)
自动带宽选择方法
方法 1:Imbens-Kalyanaraman (IK) 带宽(2012)
思路:基于MSE的渐近展开,推导数据驱动的最优带宽。
简化公式:
其中:
- :常数(取决于核函数)
- :断点左右两侧的残差方差
- :驱动变量在断点处的密度
- :潜在结果函数的二阶导数
Python 实现:
python
from rdrobust import rdbwselect
# IK 带宽选择
bw_ik = rdbwselect(y=df['Y'], x=df['X'], c=0, bwselect='mserd')
print(f"IK Bandwidth: {bw_ik.bws[0]:.2f}")方法 2:Calonico-Cattaneo-Titiunik (CCT) 带宽(2014)
改进:
- 偏差校正:考虑有限样本的偏差
- 两个带宽:
- 主带宽 :用于点估计
- 偏差带宽 :用于估计和校正偏差
覆盖误差最优(Coverage Error Optimal, CEO):
Python 实现(rdrobust 默认):
python
from rdrobust import rdrobust
# CCT 方法(默认)
result_cct = rdrobust(y=df['Y'], x=df['X'], c=0)
print(f"CCT Main Bandwidth: {result_cct.bws[0]:.2f}")
print(f"CCT Bias Bandwidth: {result_cct.bws[1]:.2f}")方法 3:交叉验证(Cross-Validation, CV)
留一法交叉验证:
- 对每个观测 ,移除它
- 用剩余数据拟合模型(带宽 )
- 预测
- 计算预测误差:
- 选择最小化 的
注意:只使用断点一侧的数据(避免利用跳跃本身)。
python
def cross_validation_bandwidth(df, X_col, Y_col, cutoff, h_candidates):
"""
交叉验证选择带宽(简化版)
参数:
- df: 数据框
- X_col, Y_col: 变量名
- cutoff: 断点
- h_candidates: 候选带宽列表
"""
cv_scores = []
for h in h_candidates:
# 限制样本
df_local = df[np.abs(df[X_col] - cutoff) <= h].copy()
df_local['X_c'] = df_local[X_col] - cutoff
df_local['D'] = (df_local[X_col] >= cutoff).astype(int)
# 留一法CV(简化:用K-fold代替)
from sklearn.model_selection import KFold
kf = KFold(n_splits=5, shuffle=True, random_state=42)
fold_mse = []
for train_idx, test_idx in kf.split(df_local):
df_train = df_local.iloc[train_idx]
df_test = df_local.iloc[test_idx]
# 拟合
model = smf.ols(f'{Y_col} ~ D + X_c + D:X_c', data=df_train).fit()
# 预测
y_pred = model.predict(df_test)
mse = np.mean((df_test[Y_col] - y_pred) ** 2)
fold_mse.append(mse)
cv_scores.append(np.mean(fold_mse))
optimal_h = h_candidates[np.argmin(cv_scores)]
return optimal_h, cv_scores
# 示例
h_candidates = np.arange(5, 30, 2)
optimal_h, cv_scores = cross_validation_bandwidth(df, 'X', 'Y', 0, h_candidates)
print(f"CV Optimal Bandwidth: {optimal_h:.2f}")稳健性检验:敏感性分析
1. 多个带宽的比较
最佳实践:报告多个带宽下的估计结果。
python
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
import matplotlib.pyplot as plt
# 尝试一系列带宽
bandwidths = [5, 10, 15, 20, 25, 30]
results = []
for h in bandwidths:
df_local = df[np.abs(df['X']) <= h].copy()
df_local['X_c'] = df_local['X']
df_local['D'] = (df_local['X'] >= 0).astype(int)
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'],
'ci_lower': model.conf_int().loc['D', 0],
'ci_upper': model.conf_int().loc['D', 1],
'n_obs': len(df_local)
})
results_df = pd.DataFrame(results)
# 可视化
fig, ax = plt.subplots(figsize=(12, 7))
ax.plot(results_df['bandwidth'], results_df['effect'],
'o-', linewidth=2, markersize=8, label='Point Estimate')
ax.fill_between(results_df['bandwidth'],
results_df['ci_lower'],
results_df['ci_upper'],
alpha=0.3, label='95% CI')
ax.axhline(y=0, color='black', linestyle='--', linewidth=1)
ax.set_xlabel('Bandwidth', fontsize=13, fontweight='bold')
ax.set_ylabel('RDD Effect Estimate', 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()
# 打印表格
print("=" * 80)
print("带宽敏感性分析")
print("=" * 80)
print(results_df.to_string(index=False))解释:
- 稳健:如果估计在不同带宽下大致稳定 → 结果可信
- 不稳健:如果估计随带宽剧烈波动 → 需要进一步调查
2. 多项式阶数的敏感性
警告(Gelman & Imbens 2019):不要使用高阶多项式()!
原因:
- 高阶多项式容易过拟合
- 远离断点的数据会过度影响断点处的估计
- 置信区间可能过于乐观
建议:
- 使用局部线性(,推荐)
- 如果需要,最多使用局部二次()
- 避免
python
# 比较不同多项式阶数
poly_orders = [1, 2, 3]
poly_results = []
for p in poly_orders:
df_local = df[np.abs(df['X']) <= 20].copy()
df_local['X_c'] = df_local['X']
df_local['D'] = (df_local['X'] >= 0).astype(int)
# 构建公式
formula_terms = ['D', 'X_c']
for k in range(2, p + 1):
formula_terms.append(f'I(X_c**{k})')
# 添加交互项
for term in formula_terms[1:]:
formula_terms.append(f'D:{term}')
formula = 'Y ~ ' + ' + '.join(formula_terms)
model = smf.ols(formula, data=df_local).fit()
poly_results.append({
'polynomial_order': p,
'effect': model.params['D'],
'se': model.bse['D'],
'ci_lower': model.conf_int().loc['D', 0],
'ci_upper': model.conf_int().loc['D', 1]
})
poly_df = pd.DataFrame(poly_results)
print("\n" + "=" * 80)
print("多项式阶数敏感性分析")
print("=" * 80)
print(poly_df.to_string(index=False))
print("\n建议:使用线性(p=1)或二次(p=2),避免高阶")3. Donut-hole RDD
动机:排除极接近断点的观测,检验结果是否稳健。
原因:
- 如果存在精确操控,最可能发生在断点附近
- 如果排除这些观测后结果依然稳健,增强可信度
实施:
- 定义"洞"的大小 (如 )
- 排除 的观测
- 重新估计RDD
python
def donut_rdd(df, X_col, Y_col, cutoff, donut_size, bandwidth):
"""
Donut-hole RDD
参数:
- df: 数据框
- X_col, Y_col: 变量名
- cutoff: 断点
- donut_size: 洞的大小(排除 |X - c| < donut_size 的观测)
- bandwidth: 带宽
"""
df_donut = df[
(np.abs(df[X_col] - cutoff) >= donut_size) &
(np.abs(df[X_col] - cutoff) <= bandwidth)
].copy()
df_donut['X_c'] = df_donut[X_col] - cutoff
df_donut['D'] = (df_donut[X_col] >= cutoff).astype(int)
model = smf.ols(f'{Y_col} ~ D + X_c + D:X_c', data=df_donut).fit()
return {
'donut_size': donut_size,
'effect': model.params['D'],
'se': model.bse['D'],
'n_dropped': len(df[(np.abs(df[X_col] - cutoff) < donut_size)]),
'n_used': len(df_donut)
}
# 尝试不同洞的大小
donut_sizes = [0, 1, 2, 3, 5]
donut_results = []
for ds in donut_sizes:
result = donut_rdd(df, 'X', 'Y', 0, ds, 20)
donut_results.append(result)
donut_df = pd.DataFrame(donut_results)
print("\n" + "=" * 80)
print("Donut-hole RDD 检验")
print("=" * 80)
print(donut_df.to_string(index=False))
print("\n如果估计稳健,说明结果不受极近断点观测的影响")完整稳健性检验流程
综合报告
python
def rdd_robustness_report(df, X_col, Y_col, cutoff):
"""
生成完整的 RDD 稳健性检验报告
参数:
- df: 数据框
- X_col: 驱动变量
- Y_col: 结果变量
- cutoff: 断点
"""
from rdrobust import rdrobust
print("=" * 80)
print(" " * 25 + "RDD 稳健性检验报告")
print("=" * 80)
df = df.copy()
df['X_c'] = df[X_col] - cutoff
df['D'] = (df[X_col] >= cutoff).astype(int)
# 1. 基准估计(CCT 最优带宽)
print("\n【1】基准估计(CCT 最优带宽)")
print("-" * 80)
baseline = rdrobust(y=df[Y_col], x=df[X_col], c=cutoff)
print(f"最优带宽: {baseline.bws[0]:.2f}")
print(f"RDD 效应: {baseline.coef[0]:.4f}")
print(f"稳健 SE: {baseline.se[0]:.4f}")
print(f"稳健 p-value: {baseline.pval[0]:.4f}")
print(f"95% CI: [{baseline.ci[0][0]:.4f}, {baseline.ci[0][1]:.4f}]")
# 2. 带宽敏感性
print("\n【2】带宽敏感性分析")
print("-" * 80)
h_baseline = baseline.bws[0]
bandwidths = [0.5 * h_baseline, 0.75 * h_baseline, h_baseline,
1.25 * h_baseline, 1.5 * h_baseline]
bw_results = []
for h in bandwidths:
df_local = df[np.abs(df['X_c']) <= h]
if len(df_local) > 50:
model = smf.ols(f'{Y_col} ~ D + X_c + D:X_c', data=df_local).fit()
bw_results.append({
'Bandwidth': f'{h:.2f}',
'Effect': f"{model.params['D']:.4f}",
'SE': f"{model.bse['D']:.4f}",
'N': len(df_local)
})
bw_df = pd.DataFrame(bw_results)
print(bw_df.to_string(index=False))
# 3. 多项式阶数
print("\n【3】多项式阶数敏感性")
print("-" * 80)
poly_results = []
for p in [1, 2]:
df_local = df[np.abs(df['X_c']) <= h_baseline]
formula_parts = ['D', 'X_c']
for k in range(2, p + 1):
formula_parts.append(f'I(X_c**{k})')
for part in formula_parts[1:]:
formula_parts.append(f'D:{part}')
formula = f'{Y_col} ~ ' + ' + '.join(formula_parts)
model = smf.ols(formula, data=df_local).fit()
poly_results.append({
'Polynomial': f'p={p}',
'Effect': f"{model.params['D']:.4f}",
'SE': f"{model.bse['D']:.4f}"
})
poly_df = pd.DataFrame(poly_results)
print(poly_df.to_string(index=False))
# 4. Donut-hole
print("\n【4】Donut-hole RDD")
print("-" * 80)
donut_results = []
for ds in [0, 1, 2, 5]:
df_donut = df[
(np.abs(df['X_c']) >= ds) &
(np.abs(df['X_c']) <= h_baseline)
]
if len(df_donut) > 50:
model = smf.ols(f'{Y_col} ~ D + X_c + D:X_c', data=df_donut).fit()
donut_results.append({
'Donut': ds,
'Effect': f"{model.params['D']:.4f}",
'N_dropped': len(df[np.abs(df['X_c']) < ds]),
'N_used': len(df_donut)
})
donut_df = pd.DataFrame(donut_results)
print(donut_df.to_string(index=False))
print("\n" + "=" * 80)
print(" " * 30 + "报告结束")
print("=" * 80)
# 生成报告
rdd_robustness_report(df, 'X', 'Y', 0)实践中的最佳实践
带宽选择的建议
- 默认使用 CCT 最优带宽(
rdrobust默认) - 报告多个带宽下的估计(如 0.5h, 0.75h, h, 1.25h, 1.5h)
- 检查敏感性:如果结果在合理范围内的带宽下稳定 → 可信
多项式阶数的建议
- 主结果:使用局部线性()
- 稳健性:检查局部二次()
- 避免:不要使用
报告清单
论文中应报告的内容:
- 主效应估计(CCT 最优带宽 + 稳健SE)
- 带宽敏感性(表格或图形)
- 有效性检验:
- 协变量平衡
- McCrary 密度检验
- 安慰剂检验
- 多项式阶数敏感性(可选)
- Donut-hole 检验(如果担心操控)
关键要点
带宽选择
- MSE 最优:IK 和 CCT 方法(推荐 CCT)
- 自动化:使用
rdrobust包的默认设置 - 稳健性:报告多个带宽下的结果
稳健性检验
- 带宽敏感性:估计应在合理范围内稳定
- 多项式阶数:避免高阶()
- Donut-hole:排除极近断点观测,检验稳健性
报告规范
- 主结果:CCT + 稳健置信区间
- 稳健性表格:带宽、多项式、donut-hole
- 有效性检验:协变量、密度、安慰剂
本节总结
在本节中,我们学习了:
- 带宽选择的理论(偏差-方差权衡)
- 自动带宽选择方法(IK, CCT)
- 敏感性分析(带宽、多项式阶数)
- Donut-hole RDD
- 完整的稳健性检验流程
关键教训:
"RDD 的可信度不仅取决于点估计,更取决于结果在各种规格下的稳健性。审稿人会仔细检查这些!"
下一步:在 第5节 中,我们将复现经典RDD研究,包括 Angrist & Lavy (1999) 和 Lee (2008)。
严格稳健性检验,让你的研究经得起考验!