ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Stata DID平行趋势检验:coefplot画事件研究图的三个高频坑与解法

Stata DID平行趋势检验:coefplot画事件研究图的三个高频坑与解法 做DID研究的几乎没有人能绕开平行趋势检验。无论你是做政策评估、因果推断还是写论文审稿人大概率第一个问题就是——你的平行趋势图呢我自己用Stata做DID项目做了将近八年各种画平行趋势图的方法都试过最终固定下来的组合是reghdfe跑事件研究法回归再用coefplot把动态效应画出来。这个组合灵活、出图快、能直接放论文但正因为coefplot太灵活坑也格外多。这篇文章把我在实际项目中踩过、也帮别人排查过的三个高频坑——基期设置不当导致图形整体失真、keep与order配置失误导致系数乱序、模型结果被意外覆盖导致置信区间对不上逐一拆解开并给出可以直接抄作业的解决办法。1. 平行趋势检验与coefplot出图的底层逻辑1.1 平行趋势假设到底在说什么平行趋势假设翻译成大白话就是如果处理组根本没有接受政策干预它的结果变量应该沿着和控制组相同的轨迹变动。这个假设没法直接验证因为处理组“没受干预”的反事实永远观测不到。所以我们能做的是看政策实施之前两组的结果变量走势是否大致一致——如果事前轨迹已经很接近那就有理由相信事后的差异主要来自政策本身。很多人把平行趋势检验和“看两组均值线是否平行”混为一谈画两条趋势线目测一下就算完事。这在简单场景下能应付但只要数据里有协变量、固定效应或聚类误差均值线图就不够用了。更规范的姿势是用事件研究法event study回归把每一期相对于基期的处理效应估计出来再画成带置信区间的系数图。这个图就是现在各类期刊里最常见的平行趋势检验图——横轴是相对政策实施的时间纵轴是系数估计值每个点带一根置信区间棒。1.2 事件研究法模型从回归到图形事件研究法的标准模型可以写成[ Y_{it} \alpha \sum_{k-m, k\neq -1}^{n} \beta_k \cdot \mathbb{1}(t - treat_year_i k) X_{it}\gamma \mu_i \lambda_t \varepsilon_{it} ]其中 (\mathbb{1}(\cdot)) 是示性函数表示“第 (i) 个个体在 (t) 年相对政策实施正好过了 (k) 期”。基期reference period通常选政策前一期也就是 (k-1)把它从回归中隐去其他所有 (\beta_k) 解读为“相对于政策前一期第 (k) 期的处理组与控制组结果差异”。如果平行趋势成立模型估计出来的 (\beta_{-m}, \dots, \beta_{-2}) 应该都不显著且点估计值在0附近波动。而政策实施后的 (\beta_0, \beta_1, \dots, \beta_n) 如果显著偏离0就说明政策产生了动态影响。这个“事前不显著、事后有反应”的图形语言就是事件研究法图的灵魂。1.3 coefplot在DID工作流中的位置coefplot是瑞士伯尔尼大学Ben Jann开发的Stata外部命令专门用来把回归系数画成点图、条形图或置信区间图。它在DID工作流里几乎是不可替代的工具因为它既可以画单个模型的系数也可以同时画多个模型的结果做对比还能灵活控制横轴标签、纵轴标题、参考线、连接线等细节。但coefplot的强大也意味着它的选项很多很多人在画事件研究图时栽跟头不是因为命令本身有多难而是没有理解它背后“取什么、排什么、标什么”的三步逻辑。下面这三个坑基本都出在这三步里。2. 坑一基期设置不当画出来的平行趋势图整体失真2.1 症状描述这是我在给一位师弟调试DID代码时遇到的典型问题。他跑了事件研究法回归pre期的系数画出来一个比一个显著pre_4是-0.25pre_3是-0.18pre_2是-0.09呈现出一种“越靠近政策时点差距越小”的单调趋势。他以为平行趋势检验没通过正准备放弃这个选题。我让他把回归结果里的dropped variables列出来他傻眼了——被Stata自动删掉的不是-1期的变量而是-4期。换句话说他画的“pre_3显著为负”实际含义是“第-3期相对于第-4期的差异”完全偏离了标准的平行趋势检验设定。类似的症状还有几种所有pre期系数集体显著或者current期系数异常巨大又或者图上明明应该看不到基期却出现了一个值为0的点。这些基本都是基期设置错误导致的。2.2 原因解剖共线性、drop规则与参照期错位事件研究法里如果把相对政策的每一期虚拟变量全部放进回归这些变量之间会存在完全共线性——因为对每个处理组个体它每一年的相对期数加起来是个固定关系。Stata检测到共线性时会按自己的算法自动删除一个变量问题在于它删的不一定是你心里默认的-1期。Stata通常按变量在变量列表中的顺序来判断把第一个导致共线性的变量剔除。很多人用循环forvalues k -4(1)4顺次生成虚拟变量生成顺序是-4, -3, -2, -1, 0, 1, 2, 3, 4Stata很可能删掉排在最前面的-4期。一旦基期变成-4所有系数都变成“相对于第-4期的差异”如果政策前处理组和控制组本来就有缓慢分化趋势那pre期系数就会呈现单调显著的假象直接影响你对平行趋势的判断。另一种常见错误用treat * rel_time这种连续交互项代替虚拟变量。这种做法在简单DID里能用但在事件研究法里会产生一个问题——rel_time对控制组来说是缺失或无效值如果没处理好缺失回归会自动删除缺失样本导致样本量缩水画出来的图与你预期的完全不匹配。2.3 解决办法手动构造交互项并显式跳过基期我推荐的稳妥做法从来都是手动生成事件虚拟变量生成时就把基期-1期排除掉不给Stata任何自由发挥的空间。* 安装coefplot首次使用需要 * ssc install coefplot, replace * 生成相对政策时间处理组为年份差控制组为缺失 gen rel_time year - policy_year if treat 1 * 手动生成事件虚拟变量政策前至-2政策当期0政策后至4 forvalues i 4(-1)2 { gen pre_i (rel_time -i treat 1) } gen cur_0 (rel_time 0 treat 1) forvalues i 1(1)4 { gen post_i (rel_time i treat 1) } * 注意这里故意没有生成 rel_time -1 的虚拟变量它就是基期这样生成之后回归里就没有-1期这个变量基期被稳稳地固定住。跑回归时再配合reghdfe代码非常干净reghdfe y pre_* cur_0 post_* 控制变量, absorb(id year) cluster(id) est store event_main如果你确实需要用别的期做基期比如-2期只需在生成时跳过pre_2其他保持不变。这种手动控制的方式比依赖ib(-1).rel_time因子变量写法更直观也更不容易在画图时出低级错误。提示还有一种做法是回归时用因子变量的ib(-1).rel_time画图时配合baselevels显示基期系数为0。这个技巧可以用但我建议你在完全理解基期机制后再用否则一旦Stata自动drop掉了别的期你很难在图上发现问题。2.4 代码示例与效果完整的最小示例xtset id year * 模拟/使用真实面板数据后 gen rel_time year - policy_year if treat 1 forvalues i 4(-1)2 { gen pre_i (rel_time -i treat 1) } gen cur_0 (rel_time 0 treat 1) forvalues i 1(1)4 { gen post_i (rel_time i treat 1) } reghdfe y pre_* cur_0 post_* x1 x2, absorb(id year) cluster(id) est store event_main这样跑出来的回归pre_4到pre_2的系数如果都不显著你才算真正通过了平行趋势检验。我之前同时跑过“自动drop版”和“手动跳过基期版”两个结果的pre期图形差异非常大甚至能影响政策效应是否显著的结论——所以这个坑一定不能踩。3. 坑二keep与order没配对系数乱序、部分系数神秘消失3.1 症状描述当回归跑完你用coefplot画图结果出来一个“惊悚”的画面横轴上的系数顺序完全错乱比如-2、-3、-4这样排或者post_10的位置跑到post_2前面又或者某个政策前期的点直接在图上消失了但回归结果里明明有这个变量。还有一个高频现象你把keep()写成了keep(pre_* cur_* post_*)以为通配符能匹配所有变量结果图上多出了截距项、控制变量图变得又乱又长。通配符匹配的是变量名不是变量角色_cons和x1、x2都在*的匹配范围里自然会被画出来。3.2 原因解剖通配符、字典序与order的过滤逻辑coefplot的默认行为是把当前模型的所有系数都画出来所以必须用keep()筛选。但如果只写keep()而不写order()coefplot会按照“变量在回归结果中的存储顺序”来排列系数。这个顺序通常是模型估计时变量出现的顺序不一定是你心里预期的“从-4到4”。如果你生成变量的循环顺序是2(1)4也就是先生成pre_2再生成pre_3再生成pre_4那画的顺序就是-2、-3、-4。更隐蔽的是含多位数的变量名post_10和post_2如果依赖通配符和默认排序Stata会按字典序排列post_10会排在post_2前面因为字符串比较时1排在2前面——这就导致横轴出现-10、-2、-3这种匪夷所思的顺序严重时整张图根本无法解读。另一个关键机制是order()在coefplot里不只是“排序”它同时会过滤——order列表里没有出现的变量不会被画出来。很多人不知道这一点以为order只是排个序结果漏写了一个变量图上就少了一个点而且它不会报错。这种“静默失败”最坑人因为你不容易意识到是哪里写错了。3.3 解决办法统一命名、显式order、coeflabels映射标签我给自己的项目定了一套规则分享给你第一变量命名要统一且有序。用pre_4、pre_3、pre_2、cur_0、post_1、post_2这种命名方式让字母前缀表达语义数字表达期数。如果期数可能超过9建议补零例如post_01、post_02、post_10这样字典序等于数值序默认排序也不会错。第二永远显式写order()不要依赖默认顺序。order列表里的变量要写全从最左边到最右边逐一列出并确保和你在图中预期的位置一致。第三用coeflabels把变量名映射成图中显示的标签。变量名是给机器看的图中显示的是-4、-3、-2这种纯数字更干净专业。coefplot event_main, /// keep(pre_4 pre_3 pre_2 cur_0 post_1 post_2 post_3 post_4) /// order(pre_4 pre_3 pre_2 cur_0 post_1 post_2 post_3 post_4) /// coeflabels(pre_4 -4 pre_3 -3 pre_2 -2 /// cur_0 0 post_1 1 post_2 2 /// post_3 3 post_4 4) /// vertical yline(0) xline(4.5, lpattern(dash)) /// ytitle(Estimated coefficient) /// xtitle(Event time relative to policy) /// graphregion(color(white))这里vertical指定纵向点图yline(0)在y轴0处加参考线xline(4.5, lpattern(dash))在政策实施时点位置加虚线竖线——注意这里的4.5怎么算的因为order里pre_4到post_4一共8个点横轴位置分别是1到8cur_0在第4个位置post_1在第5个位置政策时点应该在两者之间所以是4.5。如果只画到post_3那就是总共7个点xline仍取4.5cur_0在第4位post_1在第5位。3.4 连接线技巧与图形微调事件研究法图通常还会把各个系数点用线连起来帮助读者看趋势。coefplot支持在图上叠加连接线用addplot(line b at)就能实现——b是系数估计值at是横轴位置coefplot会自动传给addplot。coefplot event_main, /// keep(pre_4 pre_3 pre_2 cur_0 post_1 post_2 post_3 post_4) /// order(pre_4 pre_3 pre_2 cur_0 post_1 post_2 post_3 post_4) /// coeflabels(pre_4 -4 pre_3 -3 pre_2 -2 /// cur_0 0 post_1 1 post_2 2 /// post_3 3 post_4 4) /// vertical yline(0) xline(4.5, lpattern(dash)) /// addplot(line b at, lcolor(gs6) lpattern(solid)) /// ytitle(Estimated coefficient) /// xtitle(Event time relative to policy) /// graphregion(color(white)) /// legend(off)注意legend(off)如果图里只有一组系数通常不需要图例关掉更干净。如果画了多个模型的对比才需要保留图例说明。提示如果某个系数是缺失值比如该期没有任何处理组观测coefplot会自动跳过该点连接线也会在那里断开。这种缺口通常意味着数据在某个相对期没有样本支撑建议检查事件窗口是否过大或者考虑端点归并见坑三。4. 坑三模型结果被覆盖或置信区间异常图和回归结果对不上4.1 症状描述这个坑我在线下培训和论坛答疑里见过很多次典型场景是这样的你跑完了主回归系数结果非常好然后顺手跑了几个稳健性检验——换个控制变量、换个样本、跑个安慰剂检验——最后想画主回归的平行趋势图直接敲coefplot出来的图跟你记忆中的主回归结果完全对不上。要么系数值差很多要么置信区间全变了甚至图上的变量和当前数据里的变量都对不上号。另一种症状是置信区间“不翼而飞”或者“粗壮得离谱”。前者可能是某个回归命令没有正确保存VCE导致coefplot读不到方差协方差矩阵后者更多是事件窗口两端观测太少比如政策前第8期只有极少量处理组样本系数方差被撑得特别大CI棒又长又粗把整张图都带偏了。4.2 原因解剖e(b)/e(V)被覆盖、est store/restore误区、窗口端点观测稀疏coefplot默认读取内存中最近一次估计留下的e(b)和e(V)——也就是“当前活跃的估计结果”。Stata里只要执行了一次新的估计命令比如跑了一个回归、做了一个ttest、甚至跑了一次summarize部分命令会覆盖e()内存里的估计结果都可能被替换。如果你之前的主回归结果没有用est store保存再跑别的回归后直接coefplot画的就是最近一次回归的系数——这就是图和预期对不上的根本原因。很多人有保存模型的意识会用est store但坑在于est restore是一个独立的命令如果你只是store了没有restore当前活跃的模型并不会自动切回你store的那个。它的工作机制是你需要显式执行est restore event_main才能把内存中的e(b)和e(V)切换到你保存的模型上去。否则你restore之前画图用的还是最近一次运行的回归结果。至于置信区间粗壮更本质的原因往往是数据本身。事件研究法的窗口两端的相对期数只有极少数个体能覆盖到。比如政策发生在2015年样本数据从2010年开始那rel_time-5只有2010年一年的处理组观测。如果处理组本身只有几个城市或几家企业pre_5的系数估计就会非常不稳定CI自然大得吓人。这不是coefplot画错了而是模型设定在端点处“没有足够的信息”。4.3 解决办法est store coefplot直接引用模型、限定事件窗口或端点归并最稳妥的方式是全程不依赖“当前活跃模型”而是把模型保存下来画图时直接引用模型名* 主回归 reghdfe y pre_* cur_0 post_* 控制变量, absorb(id year) cluster(id) est store event_main * 中间跑其他模型不影响event_main reghdfe y pre_* cur_0 post_* 控制变量, absorb(id year) cluster(id) if placebo 1 est store event_placebo * 画主回归的事件研究图 coefplot event_main, /// keep(pre_4 pre_3 pre_2 cur_0 post_1 post_2 post_3 post_4) /// order(pre_4 pre_3 pre_2 cur_0 post_1 post_2 post_3 post_4) /// coeflabels(pre_4 -4 pre_3 -3 pre_2 -2 /// cur_0 0 post_1 1 post_2 2 /// post_3 3 post_4 4) /// vertical yline(0) xline(4.5, lpattern(dash)) /// ytitle(Estimated coefficient) /// xtitle(Event time relative to policy) /// graphregion(color(white))这样无论你中间跑了多少个模型只要模型名没被覆盖coefplot就能精确画出你想要的结果。如果你在代码过程中想快速确认某个系数值也可以est restore event_main后再用estat或lincom但画图时更推荐直接引用模型名。如果CI粗壮的问题来自端点观测不足我建议先看一下每个相对期数的样本量tab rel_time if treat 1如果-4期只有几个观测那要么缩短事件窗口比如只画-3到3要么做端点归并把-5及更早的都并成-4把5及更晚的都并成4学术界把这种做法称为binned event study。* 端点归并示例-4之前归并到-44之后归并到4 gen rel_time_bin rel_time replace rel_time_bin -4 if rel_time -4 rel_time ! . replace rel_time_bin 4 if rel_time 4 rel_time ! . * 基于rel_time_bin重新生成虚拟变量并回归 forvalues i 4(-1)2 { gen pre_i (rel_time_bin -i treat 1) } gen cur_0 (rel_time_bin 0 treat 1) forvalues i 1(1)4 { gen post_i (rel_time_bin i treat 1) } reghdfe y pre_* cur_0 post_* 控制变量, absorb(id year) cluster(id) est store event_binned归并后端点系数反映的是“该端点及更早或更晚所有期加总”的平均效应虽然会损失一部分动态信息但能显著改善端点CI过宽的问题在很多实际数据里是更务实的选择。4.4 置信区间相关的补充说明coefplot默认会画出置信区间常见默认是95%。如果你想明确写出来避免版本差异或遗漏可以在命令中显式加上coefplot event_main, level(95) ...另一种情况是图上有置信区间但你希望把它画成条形或者改成90%的区间可以用level(90)或者ciopts(lcolor(red) lpattern(dash))来微调样式。如果你需要对比多个模型的系数比如事件研究法与静态DID的结果对比可以用coefplot同时画多个模型模型名依次列出再用byopts控制排布。但需要注意多模型图里每个模型的keep/order最好保持一致否则对比起来会很混乱。5. 完整实操流程从数据到一张能直接放论文的平行趋势图5.1 数据准备与事件时间变量的生成下面给出一套可以直接套用的完整流程假设你的数据是面板结构包含个体标识id、年份year、处理组标识treat、政策实施年份policy_year。* 第一步安装coefplot ssc install coefplot, replace * 第二步设定面板 xtset id year * 第三步生成相对政策时间 * 注意处理组有政策年控制组没有政策年所以只用处理组计算 gen rel_time year - policy_year if treat 1 * 第四步检查相对期数的分布 tab rel_time if treat 1这里有一个容易忽略的细节控制组的rel_time是缺失值后续生成虚拟变量时必须用 treat 1这个条件这样控制组所有虚拟变量都是0不会干扰估计。如果你把控制组的rel_time也填成0或者某个数值条件表达式会误把控制组当成处理组的某一期系数全部出错。5.2 事件研究法回归与模型存储* 第五步生成事件虚拟变量 forvalues i 4(-1)2 { gen pre_i (rel_time -i treat 1) } gen cur_0 (rel_time 0 treat 1) forvalues i 1(1)4 { gen post_i (rel_time i treat 1) } * 第六步事件研究法回归个体与时间双向固定效应 reghdfe y pre_* cur_0 post_* 控制变量, absorb(id year) cluster(id) est store event_main如果你没有安装reghdfe可能需要先ssc install reghdfe, replace。主回归用双向固定效应加聚类稳健标准误这是目前DID实操中比较标准的配置审稿人也基本认可。5.3 coefplot出图与样式微调* 第七步画平行趋势图 coefplot event_main, /// keep(pre_4 pre_3 pre_2 cur_0 post_1 post_2 post_3 post_4) /// order(pre_4 pre_3 pre_2 cur_0 post_1 post_2 post_3 post_4) /// coeflabels(pre_4 -4 pre_3 -3 pre_2 -2 /// cur_0 0 post_1 1 post_2 2 /// post_3 3 post_4 4) /// vertical yline(0) xline(4.5, lpattern(dash)) /// addplot(line b at, lcolor(gs6) lpattern(solid)) /// ytitle(Estimated coefficient) /// xtitle(Event time relative to policy) /// graphregion(color(white)) /// legend(off)如果图上系数点太多比如从-8到8那么coeflabels可以写成循环生成避免重复劳动* 用一个简单的循环构造标签字符串 local coeflab forvalues i 8(-1)2 { local coeflab coeflab pre_i -i } local coeflab coeflab cur_0 0 forvalues i 1(1)8 { local coeflab coeflab post_i i } coefplot event_main, keep(pre_* cur_0 post_*) /// order(pre_* cur_0 post_*) /// coeflabels(coeflab) /// vertical yline(0) xline(9.5, lpattern(dash)) /// ytitle(Estimated coefficient) /// xtitle(Event time relative to policy) /// graphregion(color(white))注意这里xline(9.5)是因为总共有8个pre期、1个cur_0期cur_0排在第9位post_1在第10位政策时点在9和10之间。导出图片用graph exportgraph export 平行趋势图.png, width(800) height(600) replace如果你想输出成矢量图可以改成.pdf或.eps投稿时更清晰。5.4 常见问题速查表下面把前面几个坑里涉及的问题整理成一张速查表方便你排查代码时对照问题现象可能原因解决办法图中出现截距项或控制变量系数keep()未写或通配符过宽在keep()中列出目标变量不要只写*x轴顺序错乱-10排在-2前面依赖默认排序未显式写order()用order()逐项列出或用前导零变量名图上有值为0的“基期”点基期变量被Stata自动drop但没意识到手动跳过基期生成虚拟变量不用baselevels某个post期系数在图上消失order()里漏写该变量或该期无观测补全order()检查tab rel_time回归中所有pre期系数都显著基期错选常见是-4期被drop手动排除-1期重新回归CI不出现在图上模型VCE未被正确读取est restore后重试显式level(95)端点CI极宽事件窗口端点样本太少缩短窗口或做端点归并图与刚跑的回归对不上当前活跃模型已被其他命令覆盖用est store保存coefplot直接引用模型名中文字体或标签乱码图形方案默认字体不支持中文使用英文标签或设置graph scheme的字体5.5 多模型对比与安慰剂检验图的注意事项平行趋势检验之外审稿人还经常要求你画安慰剂检验图。常见的做法是随机分配处理组或政策时点重复几百次记录每次的估计系数画出系数分布图。coefplot在这种场景里也能派上用场但要注意安慰剂检验的“模型”往往来自permute或自编循环你需要把每次的系数和标准误整理出来再用dpplot或histogram画分布。coefplot更适合画回归模型的系数图随机化检验的结果图建议用专门的分布图命令两者配合使用效果最好。如果你需要在同一张图里对比多个模型——比如分别用不同控制变量组合跑出来的事件研究系数——可以这样写coefplot event_main event_robust, /// keep(pre_4 pre_3 pre_2 cur_0 post_1 post_2 post_3 post_4) /// order(pre_4 pre_3 pre_2 cur_0 post_1 post_2 post_3 post_4) /// coeflabels(pre_4 -4 pre_3 -3 pre_2 -2 /// cur_0 0 post_1 1 post_2 2 /// post_3 3 post_4 4) /// vertical yline(0) xline(4.5, lpattern(dash)) /// ytitle(Estimated coefficient) /// xtitle(Event time relative to policy) /// graphregion(color(white)) /// legend(position(6) rows(1) label(1 主回归) label(2 稳健性))这里legend(position(6) rows(1))把图例放在图下方并排适合两张图并排对比的场景。6. 这些坑背后的共性思维踩过这几类坑之后我最大的体会是事件研究法出图的过程本质上是在一个“模型设定—变量管理—结果管理”的三层链条上操作任何一层掉链子最后反映出来的都是图形异常。基期问题出在模型设定层keep/order问题出在变量管理层模型覆盖问题出在结果管理层。排查图形问题时不需要在coefplot命令本身反复折腾先问自己三个问题基期是不是我想要的变量顺序是不是我控制的模型引用是不是我指定的按这个思路排查大多数问题几分钟就能定位。coefplot的官方help文档很详细但恰恰因为详细新手容易被各种高级选项带偏忽略这几个基础但致命的问题。我建议你在正式项目里先跑通一版最小可复现的流程确认图形没问题后再往里面添加样式、多模型对比、动画式分图等复杂需求。先求对再求好看这是做所有数据可视化都适用的原则。另外如果你在写学术论文或工作论文记得在正文或附录里写清楚基期选的是哪一期标准误聚类到哪个层级事件窗口如何确定端点是否做了归并。这些细节对审稿人来说非常重要很多时候审稿人挑刺不是因为你图画得不好看而是因为你没说清楚这些选择背后的依据。最后分享一个小技巧在跑完事件研究法回归后不要把est store的模型名取得太乱建议按event_main、event_placebo、event_binned这种带语意的名字来命名。这样不仅画图时不容易搞混后续做敏感性分析、生成稳健性表格时也能直接esttab event_main event_placebo event_binned, ...一次性输出多个模型的对比表省去大量重复劳动。我自己后来几乎把所有DID项目都改成了这套命名规范出错率明显下降希望对你也有用。
返回列表