稳健性检验完整清单
Robustness Checks:基准回归跑出来之后,如何系统地证明你的结论不是"换一种做法就消失"。本节给出 18 项可勾选的稳健性检验清单、形式化框架与一套完整可运行的 Stata 代码体系。
boottest、estout、reghdfe、winsor2、ppmlhdfe、permute,按清单选用)。01 为什么必须做稳健性检验
稳健性检验(robustness check / sensitivity analysis)的核心问题只有一个:你的核心结论 $\hat{\beta}$,是不是只在"你精心构造的那一个回归设定"下才成立?如果换一个被解释变量、换一个样本窗口、换一种标准误聚类方式、加一阶固定效应、或者把处理时间随机打乱,系数就立刻变脸、符号翻转或失去显著性,那么读者有理由怀疑:这个结果可能是数据挖掘(data mining)或设定搜索(specification search)的产物,而不是一个可复制的因果关系。
基准回归一般写成双向固定效应形式:
其中 $\mu_i$ 为个体固定效应,$\lambda_t$ 为时间固定效应,$D_{it}$ 为处理变量,$X_{it}$ 为控制向量。稳健性检验本质上是在不改变核心识别故事的前提下,系统性地扰动"变量构造、样本、估计量、误差结构、函数形式、时间结构"这六个维度,观察 $\hat{\beta}$ 的符号、量级与显著性是否稳定。一个健康的结果应当表现为:符号稳定、量级在合理区间内波动、显著性大致维持,而不是每一项检验都恰好显著。
稳健性的正确理解是"结论对合理的设定扰动不敏感"。真正可信的论文,往往会主动报告一两个系数变小或变弱的检验,并解释其原因;把每一个扰动都凑到显著,反而会被审稿人怀疑你在挑结果。
02 稳健性检验全景清单(18 项)
下表是一张可勾选的工作清单(checklist)。建议把它贴在你的 do 文件里,做完一项打一个勾,并把每一步的核心系数记录到一张汇总表中。括号内是对应 Stata 关键词。
| # | 检验类别 | 具体做法 | Stata 关键词 |
|---|---|---|---|
| 1 | 替换被解释变量 | 用增长率/水平值/ alternative 口径的 Y 重跑基准 | gen 新Y |
| 2 | 替换核心解释变量 | 用连续处理强度替代 0/1 虚拟变量 | reghdfe Y Xcont |
| 3 | 剔除特殊样本 | 剔除直辖市、疫情年份、刚上市/刚退市观测 | if 条件 |
| 4 | 改变缩尾程度 | 1% 缩尾 → 5% 缩尾;winsor 与 trim 对比 | winsor2 / pctile |
| 5 | 替换估计方法 | OLS → 固定效应 FE → MLE/GLM/泊松 | xtreg, fe / ppmlhdfe |
| 6 | 剔除其他政策干扰 | 同时期其他政策纳入控制或子样本剔除 | control 政策虚拟 |
| 7 | 控制高阶固定效应 | 双向 FE → 三维 FE;加入行业×年份联合 FE | absorb(ind#year) |
| 8 | 安慰剂:随机处理组 | 随机抽取假处理组 500/1000 次 | permute / 循环 |
| 9 | 安慰剂:随机处理时间 | 把政策年份随机前移/后移 | gen fakeyear |
| 10 | 改变聚类层级 | 企业聚类 → 行业/地区聚类;单向→双向聚类 | vce(cluster ind) |
| 11 | Bootstrap 标准误 | 自助抽样 1000 次构造置信区间 | vce(bootstrap, reps(1000)) |
| 12 | 剔除时间趋势 | 加入个体线性时间趋势 | xtreg ... i.id#c.t |
| 13 | 非线性时间趋势 | 年份固定效应 / 省份×年份联合 FE | absorb(prov#year) |
| 14 | 滞后项检验 | 用滞后一期的 X / D 作解释变量 | L.D |
| 15 | 提前期(预期/平行趋势) | 加入处理前 lead,检验是否显著 | F.D / pre-trend |
| 16 | 更换样本区间 | 缩短窗口(政策前后 ±2 年) | if year 范围 |
| 17 | PSM 配对后回归 | 倾向得分匹配 + DID(PSM-DID) | psmatch2 |
| 18 | 平衡面板 | 仅保留连续存在的企业/地区 | xtbalance |
03 替换变量与替换样本
替换被解释变量(alternative dependent variable)检验的是"你的结论是否依赖于 Y 的某个特定度量"。例如你用全要素生产率 TFP 度量企业效率,可以再用劳动生产率、利润率、创新产出(专利/研发)作为替代 Y。若系数在所有合理度量下方向一致,说明效应不是由某一个变量构造的偶然特性驱动的。
替换核心解释变量检验的是"处理强度是否真的起作用"。若基准用 0/1 的处理虚拟变量,可以改用连续的处理强度(treatment intensity)重新估计,这本身也是对"剂量—反应关系"(dose-response)的检验。
剔除特殊样本针对的是样本污染:直辖市往往政策力度与经济结构特殊;2020—2022 年受疫情冲击的观测可能扭曲估计;刚上市、刚被 ST、刚退市的企业处于财务异常区间。把这些观测逐个剔除,若系数稳定,则结果不是由少数极端样本撑起的。改变缩尾程度(winsorize)则从 1% 换到 5%,并对比 winsor(缩尾,保留样本量)与 trim(截尾,删除尾部)两种做法。
* ===== 03.1 替换被解释变量 =====
* 基准 Y = lnTFP,替代 Y = 劳动生产率 lnLP、利润率 ROA
reghdfe lnTFP D $X, absorb(id year) vce(cluster id)
est store m_y1
reghdfe lnLP D $X, absorb(id year) vce(cluster id) // 替代Y:劳动生产率
est store m_y2
reghdfe ROA D $X, absorb(id year) vce(cluster id) // 替代Y:会计利润率
est store m_y3
esttab m_y1 m_y2 m_y3, keep(D) se star(* 0.1 ** 0.05 *** 0.01)
* ===== 03.2 替换核心解释变量:连续处理强度 =====
gen treat_intensity = 政策覆盖率 * 处理虚拟 // 连续处理强度
reghdfe lnTFP treat_intensity $X, absorb(id year) vce(cluster id)
* ===== 03.3 剔除特殊样本 =====
reghdfe lnTFP D $X if 直辖市==0, absorb(id year) vce(cluster id) // 剔除直辖市
reghdfe lnTFP D $X if !inlist(year,2020,2021,2022), absorb(id year) vce(cluster id) // 剔除疫情年
* ===== 03.4 改变缩尾程度:1% -> 5% =====
winsor2 lnTFP, cuts(1 99) replace suffix(_w1) // 1% 缩尾
winsor2 lnTFP, cuts(5 95) replace suffix(_w5) // 5% 缩尾
reghdfe lnTFP_w1 D $X, absorb(id year) vce(cluster id)
reghdfe lnTFP_w5 D $X, absorb(id year) vce(cluster id)
* ===== 03.5 PSM-DID:先配对再回归 =====
* 第一步:估计倾向得分
gen post = (year >= policy_year)
gen treat_post = treat * post
psmatch2 treat $X, out(lnTFP) neighbor(1) caliper(0.05)
* 第二步:仅在共同支撑(common support)样本上做 DID
reghdfe lnTFP treat_post $X if _weight!=., absorb(id year) vce(cluster id)
04 替换估计方法与高阶固定效应
当被解释变量为计数变量(如专利数)或存在大量零值时,OLS 可能因异方差或函数形式误设而产生偏差。此时应报告泊松拟极大似然估计 ppmlhdfe(Poisson PML),它对可加性假设更稳健、且系数可解释为半弹性。
控制高阶固定效应是面板论文中最常被审稿人要求的检验。双向固定效应(个体 + 年份)假设所有个体面临相同的年份冲击;若不同行业、不同地区受到不同的时间冲击(例如行业性周期、区域性政策周期),就应当加入行业×年份、省份×年份的联合固定效应。这类设定会吸收大量变异,若系数仍稳定,则识别非常干净。
* ===== 04.1 替换估计方法:OLS -> FE -> Poisson PML =====
xtset id year
xtreg lnTFP D $X i.year, fe vce(cluster id) // 固定效应
ppmlhdfe patent_count D $X, absorb(id year) vce(cluster id) // 计数Y用泊松PML
* ===== 04.2 高阶固定效应:行业×年份、省份×年份 =====
reghdfe lnTFP D $X, absorb(id industry#year) vce(cluster id) // 行业×年份
reghdfe lnTFP D $X, absorb(id prov#year) vce(cluster id) // 省份×年份
* ===== 04.3 剔除其他同期政策 =====
gen other_policy = (year>=2015 & 地区=="试点") // 构造同期另一政策虚拟
reghdfe lnTFP D other_policy $X, absorb(id year) vce(cluster id) // 同时控制
* 或:剔除受同期政策影响的子样本
reghdfe lnTFP D $X if other_policy==0, absorb(id year) vce(cluster id)
* ===== 04.4 平衡面板:仅保留连续存在的样本 =====
xtbalance, range(2008 2020) // 需 ssc install xtbalance
reghdfe lnTFP D $X, absorb(id year) vce(cluster id)
05 安慰剂检验与随机化推断
安慰剂检验(placebo test / permutation test)是可信度最高的一类稳健性检验,其逻辑是:如果你的效应真的来自处理,那么在"根本没有处理"的反事实世界里,应该估计不出显著的效应。做法有两类:
第一类:随机分配处理组。从所有个体中随机抽取与真实处理组数量相同的"假处理组",重新估计,记录假系数 $\hat{\beta}^{fake}$;重复 500—1000 次,得到一个"零分布"(null distribution)。真实系数应当落在这个分布的极端尾部,且假系数的均值应接近 0。
第二类:随机处理时间。保持处理组不变,但把政策发生年份随机前移或后移到一个"假年份",检验在假年份前后是否出现断点式变化。如果假年份也出现显著效应,说明你的识别可能捕捉到了某种一般性的时间趋势,而非政策本身。
* ===== 05.1 安慰剂检验:随机分配处理组(1000次循环)=====
* 真实处理组标识为 treat(0/1,按个体id层面)
levelsof id, local(ids)
scalar real_b = .
reghdfe lnTFP treat_post $X, absorb(id year) vce(cluster id)
scalar real_b = _b[treat_post]
* 用 simulate 跑 1000 次随机抽样
program drop _permute_treat
program define _permute_treat, rclass
use "panel_data.dta", clear
* 随机抽 n_treat 个 id 作为假处理组
preserve
keep if year == first_year
gen u = runiform()
sort u
gen byte fake_treat = (_n <= `= n_treat')
keep id fake_treat
tempfile tmp
save `tmp'
restore
merge m:1 id using `tmp', nogen
gen fake_post = (year >= fake_policy_year) // 假政策年
gen fake_tp = fake_treat * fake_post
reghdfe lnTFP fake_tp $X, absorb(id year) vce(cluster id)
return scalar b_fake = _b[fake_tp]
end
permute b_fake = r(b_fake), reps(1000) seed(20240101) saving("placebo.dta", replace): ///
_permute_treat
* 画分布图:真实系数应落在分布远端
use "placebo.dta", clear
gen pval = (abs(b_fake) >= abs(real_b))
histogram b_fake, xline(`=real_b') title("Placebo: fake coefficient distribution") ///
note("真实系数 = " %6.4f real_b)
* ===== 05.2 安慰剂检验:随机处理时间 =====
* 保持处理组,把政策年随机提前3年/延后3年
gen fake_post = (year >= policy_year - 3)
gen fake_tp = treat * fake_post
reghdfe lnTFP fake_tp $X, absorb(id year) vce(cluster id) // 预期:不显著
安慰剂检验应同时报告:假系数的均值(应≈0)、标准差、真实系数对应的伪 p 值(真实系数落在零分布尾部的比例),并附上系数分布直方图。只放一张图而不报告统计量,说服力很弱。
06 标准误、聚类与 Bootstrap
很多时候"不显著"或"过度显著"其实是标准误估计的问题。聚类层级(clustering level)决定了误差自相关被如何处理:Bertrand, Duflo & Mullainathan (2004) 著名地指出,DID 中若处理在组层面分配,标准误必须聚类到组层面,否则会严重低估标准误、虚高 t 值。你应当对比:企业层面聚类、行业/地区层面聚类、双向聚类(企业 + 年份)这几种设定下的显著性变化。
当聚类数偏少(少于 ~30—50 个集群)时,渐近聚类标准误会偏低,此时应使用 Wild Cluster Bootstrap(boottest)或小样本校正。当误差分布未知时,Bootstrap 标准误通过有放回地重复抽样 1000 次来构造经验分布,对小样本与非正态误差更稳健。
* ===== 06.1 改变聚类层级:企业 -> 行业/地区 =====
reghdfe lnTFP D $X, absorb(id year) vce(cluster id) // 企业聚类(基准)
reghdfe lnTFP D $X, absorb(id year) vce(cluster industry) // 行业聚类
reghdfe lnTFP D $X, absorb(id year) vce(cluster prov) // 地区聚类
* 双向聚类:企业 + 年份
reghdfe lnTFP D $X, absorb(id year) vce(cluster id year)
* ===== 06.2 聚类数少时:Wild Cluster Bootstrap =====
* ssc install boottest, replace
reghdfe lnTFP D $X, absorb(id year) vce(cluster prov)
boottest D, reps(999) weight(webb) // Webb 权重,集群少时更准
* ===== 06.3 一般 Bootstrap 标准误 =====
reghdfe lnTFP D $X, absorb(id year) vce(bootstrap, reps(1000) seed(20240101))
07 时间趋势与动态效应
识别策略最怕的就是"趋势混淆":处理组与对照组在政策前本身就处于不同的时间轨迹上。加入个体线性时间趋势($i \times t$ 交互)可以吸收组间不同的线性趋势;加入省份×年份联合固定效应则吸收任意形式的地区特定时间冲击。如果控制这些趋势后系数消失,说明原结果可能是趋势而非政策效应。
动态效应检验(dynamic / event-study)则同时回答两个问题:处理前是否平行趋势(提前期 lead 是否显著)、处理后效应如何演化(滞后项 lag 的形态)。标准做法是事件研究法:
其中 $t_i^*$ 为个体 $i$ 的处理时点。要求处理前各 lead 的 $\beta_k\approx0$(平行趋势),处理后 $\beta_k$ 显著且呈现合理形态。
* ===== 07.1 加入个体线性时间趋势 =====
xtset id year
reghdfe lnTFP D $X i.id#c.year, vce(cluster id) // 个体特定线性趋势
* 加入非线性/联合时间趋势
reghdfe lnTFP D $X, absorb(id prov#year) vce(cluster id) // 省份×年份
* ===== 07.2 事件研究:提前期 + 滞后项 =====
* 以政策当年为基准期(omit),生成相对时间 d_{-K} ... d_{+K}
gen rel_time = year - policy_year
forvalues k = -4/4 {
local kk = `k' + 5 // 命名为 d1..d9
gen d`kk' = (rel_time == `k')
}
* 省略处理当年(rel_time==0,即 d5)作为基准
reghdfe lnTFP d1 d2 d3 d4 d6 d7 d8 d9 $X, absorb(id year) vce(cluster id)
est store event_study
* 画图:提前期(d1-d4)应不显著贴近0;滞后项(d6-d9)显著
coefplot event_study, keep(d1 d2 d3 d4 d6 d7 d8 d9) ///
vertical yline(0) title("Event Study: pre-trend vs post-effect")
* 也可使用 eventdd / did_multiplegt 等官方推荐命令
* ssc install eventdd
eventdd lnTFP $X, timevar(rel_time) method(fe, vce(cluster id)) ///
leads(4) lags(4) baseline(-1)
08 逐步流程
08b PSM 倾向得分匹配:从 Rosenbaum-Rubin 到平衡性检验
PSM(Propensity Score Matching, Rosenbaum & Rubin 1983)解决的是可观测混淆下的自选择偏差:处理组和对照组在可观测特征 $X$ 上系统不同,直接 OLS 把"特征差异"和"处理效应"混在一起。PSM 的核心思路是:不直接匹配高维的 $X$,而是先估计每个个体被处理的概率 $p(X)=P(D=1|X)$(即倾向得分),再按 $p(X)$ 在处理组和对照组之间匹配。
08b.1 三步流程
psmatch2 的 common 选项自动完成。08b.2 完整 Stata 代码
* ===== PSM 倾向得分匹配完整流程 =====
* 场景:评估"参加培训 D=1"对工资 lnWage 的影响
set seed 20240109
* --- 1. 估计倾向得分(logit)---
* ssc install psmatch2, replace
global X age educ gender married exper // 决定是否参加培训的可观测变量
logit D $X, robust
predict pscore, pr // 倾向得分
* --- 2. 看共同支撑区间 ---
sum pscore if D==1
local pmin_treat = r(min)
local pmax_treat = r(max)
sum pscore if D==0
* 只保留 pscore 在 [max(min_treat, min_ctrl), min(max_treat, max_ctrl)] 内的观测
* --- 3. 1:1 最近邻匹配 + 卡尺 ---
psmatch2 D $X, out(lnWage) neighbor(1) caliper(0.05) common ties
* 输出:
* ATT (Average Treatment effect on Treated)
* _weight : 匹配权重(处理组=1,对照组=匹配权重)
* _support: 共同支撑标识
* --- 4. 平衡性检验 ---
pstest $X, both graph
* 输出:每个 X 匹配前后的标准化均值差
* 经验阈值:匹配后 |bias| < 10%
* 若某个 X 匹配后偏差仍 > 10%,调整匹配方法或加入高次项
* --- 5. 不同匹配方法的稳健性 ---
psmatch2 D $X, out(lnWage) neighbor(3) caliper(0.05) common // 1:3 近邻
psmatch2 D $X, out(lnWage) kernel bwidth(0.06) common // 核匹配
psmatch2 D $X, out(lnWage) strata breaks(0 .2 .4 .6 .8 1) common // 分层匹配
* --- 6. teffects 替代命令(Stata 内置)---
teffects psmatch (lnWage) (D $X, logit), atet nneighbor(3) vce(robust)
* teffects 输出更规范,自动算 ATT / ATU / ATE
PSM 假设所有决定处理的混淆变量都可观测(unconfoundedness / selection on observables)。如果存在不可观测的自选择(如"更上进的人更愿意参加培训"),PSM 完全无效。这也是为什么顶刊常做 PSM-DID(06 DID 页 s3):PSM 平衡可观测,DID 消除时不变不可观测,两者互补。
08c Heckman 两阶段:样本选择偏差与逆米尔斯比
当"能被观测到 Y"本身不是随机的——比如你想估计女性工资方程,但只有"选择进入劳动力市场"的女性有工资数据——OLS 在这个 self-selected 样本上估计是有偏的。Heckman (1979) 两阶段法用逆米尔斯比(Inverse Mills Ratio, IMR)修正这种选择偏差。
08c.1 模型结构
第一步:用全部样本跑 probit / logit 估计选择方程,得到 $\hat{\gamma}$。排除约束(exclusion restriction):选择方程里必须至少有一个变量 $Z_i$ 影响"是否参与"但不影响 $Y$——否则 IMR 与 $X$ 高度共线,识别脆弱。
第二步:用第一步的预测值构造逆米尔斯比 $\hat{\lambda}_i = \phi(\hat{Z}_i'\hat{\gamma}) / \Phi(\hat{Z}_i'\hat{\gamma})$(参与样本)或 $\hat{\lambda}_i = -\phi(\hat{Z}_i'\hat{\gamma}) / [1-\Phi(\hat{Z}_i'\hat{\gamma})]$(非参与样本),加到结果方程里:
$\hat{\lambda}_i$ 显著(t 值)说明存在样本选择偏差,Heckman 修正是必要的。
08c.2 完整 Stata 代码
* ===== Heckman 两阶段:女性工资方程 =====
* 场景:估计女性教育回报率,但只有"参与劳动力市场"的女性有工资数据
* --- 1. 数据准备 ---
* lwage: 对数工资(仅参与工作的女性观测到)
* educ: 教育年限
* exper: 工作经验
* married / children: 婚姻、子女(影响是否参与工作,但不直接影响工资)
* inlf: 是否参与劳动力市场
* --- 2. 两步法手动估计 ---
* 第一步:选择方程(probit)
probit inlf educ exper married children other_income
predict xb, xb
gen imr_normal = normalden(xb)/normal(xb) // IMR(参与样本)
* 第二步:结果方程(仅 inlf=1 样本)
reg lwage educ exper imr_normal if inlf==1, robust
* imr_normal 显著 → 存在样本选择偏差
* --- 3. Stata 内置 heckman 命令(MLE,推荐)---
heckman lwage educ exper, select(inlf = educ exper married children other_income)
* 输出:
* 结果方程:lwage 的系数
* 选择方程:inlf 的系数
* /athrho / lnsigma:rho(选择方程与结果方程误差相关)与 sigma
* lambda: IMR 的系数(=rho*sigma)
* LR test of indep. eqns. (rho=0): H0 选择与结果独立
* 若 LR 检验 p<0.05 → 必须用 Heckman
* --- 4. 二值结果变量:heckprob ---
* 若 Y 是 0/1(如"是否拥有养老保险"),用 heckprob
heckprob own_pension educ exper, select(inlf = educ exper married children)
* --- 5. 关键诊断 ---
* 排除约束:选择方程中的 married/children 不应出现在结果方程中
* 检查:结果方程若加入 married/children,应不显著(理论上)
* 若找不到排除约束,Heckman 只能靠函数形式识别,结果极不可靠
- 没有排除约束还硬跑 Heckman:选择方程和结果方程用完全相同的 $X$,只能靠正态分布的函数形式识别,结果高度敏感。规范做法:至少找一个"决定参与但不影响结果"的变量。
- 把 IMR 显著当成"一定要 Heckman":IMR 显著只说明样本选择存在,不代表你修正得对。如果排除约束弱,Heckman 反而引入新偏误。
- 把 Heckman 当成万能内生性修正:Heckman 只修正样本选择偏差,不修正遗漏变量或反向因果。
08d Oster (2019) 系数稳定性分析
当无法用工具变量、只能靠"控制可观测变量"逼近因果效应时,一个自然的追问是:如果存在不可观测的混杂 $W_2$,它需要多强,才能把你观察到的系数 $\hat{\beta}$ 推到 0?Oster (2019, JBES 37(2): 187–204) 在 Altonji–Elder–Taber (2005) 基础上,用"加入控制后系数变化幅度"和"$R^2$ 上升幅度"两条信息,对不可观测混杂的偏误上界做部分识别。
08d.1 原理与公式
记两个设定:短回归(不含控制)系数 $\ddot\beta$、$R^2_{short}$;长回归(含全部可观测控制)系数 $\tilde\beta$、$R^2_{controlled}$。Oster 引入两个参数:
- $\delta$:不可观测混杂相对可观测控制的"选择强度比"。$\delta=1$ 表示"不可观测与可观测一样重要";$\delta>1$ 表示需要不可观测比已控制变量更强才能推翻结论。
- $R_{max}$:若真实模型被完全估计能达到的最大 $R^2$(不可能超过 1)。Oster 建议取 $R_{max}=1.3\times R^2_{controlled}$。
偏误校正后的估计量(对 $\beta=0$ 的偏误上界)为:
实务报告两个量:(1) 在 $R_{max}=1.3\tilde R^2$ 下,让 $\beta^*=0$ 所需的 $\delta^*$;(2) 在 $\delta=1$(不可观测与可观测同等重要)下的识别区间 $[\beta^*, \tilde\beta]$。
若 $\delta^*>1$(推翻结论需要"不可观测比已控制变量还更重要"),或在 $\delta=1$ 时识别区间不跨 0,则结论对不可观测混杂稳健。中文顶刊常见表述:"$\delta$ 大于 1(或识别区间不含 0),说明遗漏变量造成的偏误较小。"
08d.2 完整 Stata 代码(psacalc)
*==============================================================*
* Oster (2019) 系数稳定性:psacalc
* 场景:处理变量 D 对 Y 的影响,可观测控制 X
*==============================================================*
clear all
set seed 20240701
sysuse auto.dta, clear
* --- 0. 两个基础设定:短回归 & 长回归 ---
global Y price
global D foreign
global X weight length turn
* 短回归:只放处理变量
regress $Y $D
scalar b_short = _b[$D]
scalar r2_short = e(r2)
* 长回归:放全部控制
regress $Y $D $X
scalar b_long = _b[$D]
scalar r2_long = e(r2)
* --- 1. psacalc(Oster 官方 Stata 命令)---
* ssc install psacalc, replace
regress $Y $D $X
* 报告让 beta=0 所需的 delta*(默认 Rmax = 1.3 * R2_long)
psacalc delta $D, rmax(0.5)
* 若 delta* > 1 => 稳健(需要不可观测比可观测更强才能归零)
* 报告在 delta=1 下的 beta*(识别区间下限)
psacalc beta $D, rmax(0.5) delta(1)
* 识别区间 = [beta*, b_long];若不含 0 => 稳健
* --- 2. 手算 delta* 与 beta*(理解原理)---
* delta* = (b_long - 0) * (Rmax - R2_long) / ((b_short - b_long) * (R2_long - R2_short))
scalar Rmax = 1.3 * r2_long
scalar dstar = (b_long - 0) * (Rmax - r2_long) / ((b_short - b_long) * (r2_long - r2_short))
display "delta* (让 beta=0 所需) = " dstar
* 在 delta=1 下的 beta*
scalar bstar = b_long - 1 * (b_short - b_long) * (Rmax - r2_long)/(r2_long - r2_short)
display "delta=1 时 beta* = " bstar " ; b_long = " b_long
* 区间 [bstar, b_long] 若不含 0,则稳健
08e Cinelli–Hazlett (2020) 混杂敏感性与 RV
Cinelli & Hazlett (2020, JRSS-B 82(1): 39–67) 把 Oster 的思想一般化:不假设"不可观测与可观测成比例",而是直接问——一个假设的未观测混杂 $Z$,需要分别与处理 $D$、结果 $Y$ 有多强的偏相关(partial $R^2$),才能让处理效应 $\beta$ 不显著甚至变号?这就是 部分识别(partial identification) 框架。
08e.1 概念框架与 RV
- 部分 $R^2$:未观测混杂在控制已观测变量后,能解释处理 $D$ 和结果 $Y$ 残差变异的比例(记作 $R^2_{D\sim Z|X}$、$R^2_{Y\sim Z|D,X}$)。
- Robustness Value (RV):使处理效应的置信区间恰好包含 0 所需的、混杂与 $D$、与 $Y$ 的同等强度(equal strength)下限。RV 越大越稳健。直观对照:如果某个已观测重要控制的 partial $R^2$ 是 5%,而 RV=15%,那需要一个比它强得多的未观测混杂才能推翻结论。
08e.2 与 Oster 的对比
| 维度 | Oster (2019) | Cinelli–Hazlett (2020) |
|---|---|---|
| 核心参数 | $\delta$(选择强度比)+ $R_{max}$ | partial $R^2$ + RV |
| 建模假设 | 不可观测与可观测按比例(proportional selection) | 不要求成比例,直接刻画混杂强度 |
| 输出 | 识别区间 $[\beta^*,\tilde\beta]$、$\delta^*$ | RV、敏感性曲线/等值线、校正后 CI |
| 直观解释 | "需要多强的相对选择" | "需要一个多强的混杂"(可与已观测控制对照) |
08e.3 代码:sensemakr(R 为主,Stata 可手算)
# Cinelli-Hazlett (2020) 混杂敏感性分析
# install.packages("sensemakr")
library(sensemakr)
# 1. 先用线性回归估计处理 D 对 Y 的效应(含控制 X)
data("darfur")
model <- lm(peacefactor ~ directlyharmed + age + farmer_dar +
heron_dar + voted, data = darfur)
# 2. 跑 sensemakr:处理变量 directlyharmed
s <- sensemakr(model = model, treatment = "directlyharmed",
benchmark_covariates = "farmer_dar", # 用已观测变量作强度标尺
kd = 1:3) # 混杂强度 = kd * 标尺
summary(s)
# 3. 核心输出:
# RV (robustness value):让 CI 含 0 所需的最小 partial R2
# 若 RV 远大于任一已观测控制的 partial R2 => 结论稳健
# 4. 画敏感性等值线图
# plot(s, sensitivity.of = "estimate")
* Stata 无 sensemakr 官方命令,可手算 RV 的近似量或调用 Python/R
* 思路:报告已观测控制的 partial R2,作为"混杂强度标尺"
sysuse auto.dta, clear
regress price foreign weight length turn
* 用 partial R2 作对照:某个强控制解释了多少残差变异
* 若 RV >> 任一已观测控制的 partial R2,则不可观测混杂需非常强
* 严谨的 RV/等值线建议在 R 中用 sensemakr 完成,再把数值贴回论文
08f 随机化推断(Randomization Inference / Fisher 精确 p)
当处理是随机分配(或近似随机)时,Fisher (1935) 的思路更直接:在"处理完全无效"的零假设下,处理分配本身是随机的。于是把处理标签在样本间反复随机置换(permutation),每次重新计算检验统计量,就得到了零假设下的完整经验分布——真实统计量落在该分布的多远尾部,就是随机化 p 值。这叫 Randomization Inference (RI) / Fisher Exact p。
08f.1 原理与适用场景
与传统渐近 p 值相比,RI 不依赖大样本正态近似,因此在以下场景更可靠:
- 小样本:n 很小,CLT 还没生效。
- 聚类少:处理在组层面分配、组数只有十来个,渐近聚类标准误失真(见 s6)。
- 非标准分布:残差严重偏态、存在极端值,t/F 近似不准。
- 有限总体精确性:你关心的是"这 $N$ 个单位的处理效应"而非无限总体,RI 直接在有限总体上做推断。
08f.2 完整 Stata 代码
*==============================================================*
* 随机化推断 RI:permute / ritest / 手写置换
*==============================================================*
clear all
set seed 20240702
sysuse auto.dta, clear
* --- 1. 真实统计量:foreign 对 price 的效应 ---
regress price foreign weight
scalar b_obs = _b[foreign]
* --- 2. 官方 permute:置换 foreign 标签 1000 次 ---
permute foreign b = _b[foreign], reps(1000) seed(20240702) saving("ri_perm.dta", replace): ///
regress price foreign weight
* 读结果:真实 b 在置换分布中的位置
use "ri_perm.dta", clear
gen p_ri = (abs(b) >= abs(b_obs)) + 0
quietly sum p_ri
display "随机化 p 值 = " r(mean)
histogram b, xline(`=b_obs') note("真实系数 = " %6.3f b_obs)
* --- 3. ritest(Heß 2017,更灵活,推荐)---
* ssc install ritest, replace
sysuse auto.dta, clear
ritest foreign _b[foreign], reps(1000) seed(20240702) : ///
regress price foreign weight
* ritest 直接输出 RI p 值;支持 reghdfe、聚类、固定效应
* --- 4. 手写 permutation test(理解原理)---
sysuse auto.dta, clear
gen b_perm = .
local B = 1000
forvalues b = 1/`B' {
preserve
gen u = runiform()
sort u
gen fake_foreign = mod(_n, 2) // 随机重排 foreign
regress price fake_foreign weight
scalar bb = _b[fake_foreign]
restore
replace b_perm = bb in `b'
}
quietly count if abs(b_perm) >= abs(b_obs)
display "手写 RI p 值 = " r(N)/`B'
本页 s5 的"随机处理组安慰剂检验"在统计原理上就是 RI:把置换分布画出来、报告真实系数落在尾部的比例,本质就是 Fisher 随机化 p 值。s8f 把它形式化为一种推断方法,适用于任意检验统计量与小样本/少聚类场景;ritest 是目前最规范的实现。
09 论文案例
psacalc。是当前中文顶刊(《经济研究》《管理世界》《财经研究》等)观测研究类论文"不可观测混杂稳健性"的标准做法。本页 s8d 的方法来源。sensemakr。比 Oster 更灵活、不依赖"成比例选择"假设,并给出可与已观测变量强度对照的直观判据。本页 s8e 的方法来源。ritest 命令,把随机化推断标准化、可复现。小样本、少聚类、非标准分布场景下报告 RI p 值时的首选引用。本页 s8f.2 的实现依据。psacalc:取 $R_{max}=1.3\times R^2_{controlled}$,报告使 $\beta=0$ 所需的 $\delta^*$(典型表述:$\delta$ 大于 1 或识别区间不含 0 则结论稳健);在 DID/准实验稳健性中,则用置换/ritest 报告随机化 p 值与安慰剂分布图。本页 s8d、s8f 的中文落地参照。10 常见错误与进阶资料
逐一遍历替换变量、样本、方法,只保留显著的那几个写进论文,把不显著的扔进附录甚至丢弃——这正是 Brodeur et al. (2016) 批评的 p-hacking。正确做法是事前规划检验清单并全部报告,包括那些让系数变弱的。
随机化推断的零分布需要足够多的重抽样次数(通常 ≥500,严谨的用 1000—2000)。次数太少时直方图抖动剧烈、伪 p 值估计不准,结论不可信。同时必须固定种子(seed)以保证可复现。
处理是在省/行业层面分配的,却只在企业层面聚类——这正是 Bertrand et al. (2004) 证明会严重低估标准误的情形。聚类层级必须与处理的分配层级一致;聚类数太少时改用 Wild Cluster Bootstrap。
不能只画事件研究图凭视觉判断 lead 是否贴近 0,应当报告处理前各 lead 系数的联合显著性检验(例如 test d1=d2=d3=d4=0 的 F 统计量),用统计量支撑平行趋势。
匹配后必须跑 pstest 检查每个 $X$ 的标准化均值差。如果某个变量匹配后偏差仍 > 10%,说明匹配失败,ATT 不可信。中文论文常见"跑完 psmatch2 就报 ATT"的做法,审稿人现在会要求看平衡性表。
选择方程和结果方程用完全相同的解释变量,仅靠正态分布函数形式识别 IMR。结果对分布假设极度敏感,换 logit 或半参数结果就变号。规范做法:找一个"影响参与但不影响结果"的变量(如家庭非劳动收入对女性参与工作的影响)。
$R_{max}$ 是 Oster 分析最敏感的旋钮。随意取 $R_{max}=1$(回归注定过拟合、区间失真)或取一个极小值(人为制造"稳健"假象)都会让结论不可信。规范做法:遵循 Oster 建议取 $R_{max}=1.3\times R^2_{controlled}$,并做 $R_{max}$ 敏感性(换 1.25/1.5/2.0 倍看 $\delta^*$ 是否稳定)。
Oster/Cinelli–Hazlett 的价值在于"偏误上界 / 置信区间",而不是单个 $\delta$ 或点估计。只报一个 $\delta^*>1$ 却不报告识别区间 $[\beta^*,\tilde\beta]$ 是否跨 0、不报告 RV 与已观测控制的对照,等于没做敏感性分析。
置换重抽样必须 set seed(或 seed() 选项)保证可复现,否则审稿人无法核验;重抽样次数通常 ≥1000,次数太少时伪 p 值抖动大、结论不稳。同时要在文中写明置换了什么标签(处理组?处理时间?),避免与真实实验设计混淆。
进阶资料
- Bertrand, Duflo & Mullainathan (2004), QJE — 聚类与序列相关的方法论源头。
- Oster, E. (2019), JBES 37(2): 187–204 — 系数稳定性与 $\delta$ / $R_{max}$,Stata 命令
psacalc。 - Cinelli, C. & Hazlett, C. (2020), JRSS-B 82(1): 39–67 — RV 与 partial R² 敏感性分析,R 包
sensemakr。 - Fisher, R. A. (1935), The Design of Experiments — 随机化推断 / Fisher 精确检验源头。
- Heß, S. (2017), Stata Journal 17(3): 630–651 —
ritest随机化推断指南。 - Brodeur et al. (2016), AEJ: Applied — t 统计量分布与 p-hacking 证据。
- Cameron, Gelbach & Miller (2008), ReStat — 双向聚类与 Wild Bootstrap。
- Schmidheiny & Siegloch (2019), NBER WP — 事件研究法与交错 DID 的稳健性实践。
- Stata 命令:
reghdfe、ppmlhdfe、boottest、eventdd、coefplot、winsor2、psmatch2。