Stata平行趋势检验绘图实战:coefplot三大坑与解决
做DID的人十有八九都要过平行趋势检验这一关。最近我在处理一组政策评估数据时把这套流程完整走了一遍——从事件时间变量生成到reghdfe跑完交互项再到coefplot出图每一步都踩过坑。尤其是最后用coefplot画图这一步一张图调了两小时中间一度想放弃直接用Excel手画。这篇文章把3个最典型的坑和解决办法整理出来希望能让你少走点弯路。无论你是刚接触Stata的实证小白还是已经被平行趋势检验折磨过几轮的论文党这篇都值得收藏备用。1. 平行趋势检验到底在检验什么——先搞清楚原理再动手1.1 为什么政策评估前必须先过这一关DID双重差分Difference-in-Differences的核心逻辑是拿“处理组的前后变化”减去“对照组的前后变化”剩下的就是政策净效应。但这个做法的前提是如果政策从来没有发生处理组和对照组的趋势应该是平行的。换句话说两组在政策实施前必须走在同一条时间轨迹上否则你算出来的“政策效应”其实混入了两组本来就存在的差异。举个生活化的例子你想评估一家奶茶店搞“第二杯半价”活动对销量的影响于是把这家店当成处理组隔壁一条街没搞活动的奶茶店当对照组。如果活动前处理组销量每个月涨10%对照组每个月只涨2%那么政策后处理组涨了15%你能说其中13个百分点都是活动贡献的吗不能因为按照原有趋势处理组本来就会比对照组涨得多。平行趋势检验就是用来排除这种“运气好”的可能性的。1.2 事件研究法与平行趋势检验的关系平行趋势检验最主流的做法是事件研究法event study也就是把政策实施当年当作事件点0往前推若干期-1、-2、-3……往后推若干期1、2、3……然后构造“处理组×相对时间”的交互项放进回归里逐个估计每个相对时期的系数。回归设定大概是这样的[ Y_{it} \alpha \sum_{k \neq -1} \beta_k (Treat_i \times D_{it}^k) \gamma X_{it} \mu_i \lambda_t \varepsilon_{it} ]其中 (D_{it}^k) 是相对时间为 (k) 的虚拟变量(\beta_k) 就是第 (k) 期的处理组与对照组差异。理论上政策前各期k -4, -3, -2的系数应该不显著且在0附近波动政策后各期k 0, 1, 2, 3……的系数如果显著偏离0说明政策有效果。画图时把每个 (\beta_k) 和它的95%置信区间画出来就是大家常说的平行趋势图。注意一个细节基准期通常取政策前一期k -1这个期别的系数会被虚拟变量陷阱吃掉所以图形上会缺一个点。如果你看到别人论文里的平行趋势图在“-1期”位置没有点、其他期都在那是正常的不是画错了。2. 用Stata做平行趋势检验的标准流程2.1 数据准备与事件时间变量生成Stata做平行趋势检验第一步不是跑回归而是把事件时间变量event time生成好。这一步看似简单实际上埋着最大的雷。通常我们手头的数据是面板结构每个个体id有多年的观测year处理组有一个政策实施年份policy_year。事件时间变量表示“距离政策实施还有几年”公式是gen rel_time year - policy_year if treat 1注意后面这个if treat 1非常关键否则对照组没有政策年份policy_year是缺失值year - .也会变成缺失值倒不会报错但如果你事先把对照组的policy_year填成了某个统一值那就会出大问题——这个咱们在坑一里细说。生成之后我强烈建议先做一个交叉表检查数据tab rel_time treat, missing看看处理组在各个相对时期有多少观测对照组是不是全是缺失。如果这里分布不对后面画出来的图再漂亮也是废的。2.2 基准回归与系数提取事件时间变量准备好之后就可以跑平行趋势检验的主回归了。常见的写法是用高维固定效应回归命令reghdfereghdfe y i.treat##ib(-1).rel_time, absorb(id year) vce(cluster id)这里ib(-1).rel_time表示把相对时间-1期作为基准i.treat##ib(-1).rel_time表示把处理组虚拟变量和各期相对时间虚拟变量做全交互。reghdfe会自动吸收个体固定效应和时间固定效应聚类稳健标准误放在个体层面。跑完之后用est store把结果存下来est store event然后就可以进入画图环节了。coefplot的基本思路是从刚刚估计的结果中提取出一系列系数把它们画成带置信区间的散点或折线。核心命令是coefplot event, keep(1.treat#*) vertical yline(0) ...问题恰恰就出在这个keep(1.treat#*)上很多人在这里翻车。3. coefplot画图遇到的3个坑及解决办法3.1 坑一事件时间变量生成错误导致整条曲线“镜像扭曲”这个坑是最隐蔽的因为它不报错也不显示红色警告而是让你在不知不觉中得到一条完全错误的曲线。我见过的最典型错误是这样操作的数据里只有处理组有policy_year对照组没有但有人在生成rel_time之前先用某个统一值把对照组的policy_year补齐了比如写了replace policy_year 2018 if treat 0。这样一来对照组每个个体也被强行贴上了“2018年政策实施”的标签对照组的事件时间变量就不是缺失值而是从-5到5的一堆假数字。结果是什么回归里treat#rel_time的交互项会额外把“对照组在2018年前后的变化”也吸收进去。画出来的图就会表现成处理组和对照组在政策前已经出现明显的背离甚至出现政策前系数显著、政策反而不显著的怪象。更麻烦的是这种错误在数值上不会给你任何提示只有你把图画出来、发现趋势完全不合理时才会起疑。解决办法其实很简单生成rel_time时就严格限定处理组再在回归前用replace rel_time . if treat 0做一次兜底检查。更稳妥的做法是画图之前先跑一个描述性统计tabstat rel_time, by(treat) stats(min max n)如果处理组的最小值是-4、最大值是4对照组全是缺失或没有有效值说明数据基本没问题。这里我再多说一句经验事件时间变量的起点和终点的观测数量通常很少尤其是政策实施很早或很晚才进入样本的个体会导致两端系数估计得很不精确。如果你发现曲线的首尾置信区间宽到离谱不要慌这是样本量不足的正常表现可以考虑把两端的期别合并比如把“-10到-5期”合并成一个“事前第5期及以上”。3.2 坑二reghdfe跑完后coefplot找不到系数图形缺一块这个坑是coefplot使用中最常见的报错来源具体表现为命令跑完后coefplot要么提示no coefficients found要么画出来的图在基准期位置多出一个难以解释的“缺口”或“空位”。先说“报错找不到系数”的情况。当你用i.treat##ib(-1).rel_time做交互时Stata生成的系数名不是简单的“treat1_reltime_m2”这种格式而是类似1.treat#-2.rel_time、1.treat#0.rel_time、1.treat#3.rel_time。如果我们在coefplot里写keep(1.treat#*)但星号位置写错了比如写成keep(1.treat#*)变成keep(treat#rel_time)或者漏掉了前面的1.那就一个系数都匹配不上。再说“图形缺一块”的情况。基准期-1期因为共线性被omitted它的系数本来就不存在这是正常的。但coefplot默认会按照系数向量的顺序排列一旦某个位置没有系数它可能把这个位置空出来导致图形看起来在-1期处断裂。很多初学者会以为是自己代码错了拼命加选项其实不是。解决办法有三个层面第一先看系数名到底长什么样可以在回归后输入matrix list e(b)这一步能清楚看到每个系数对应的变量名。以我跑过的数据为例系数名一定会长成1.treat#-4.rel_time、1.treat#-3.rel_time……这样带数字前缀的样子。第二coefplot的keep不要写得花里胡哨直接用最简单的通配符coefplot event, keep(1.treat#*) vertical yline(0) xline(6.5)这里xline(6.5)是为了在基准期位置画一条竖线方便读者看到政策时点。6.5这个数字取决于你图形的横轴排列基准期在横轴的第几个位置。如果相对时期是从-4到4共9个点基准期-1在第4个点竖线可以画在4和5之间也就是4.5但我习惯直接用相对时间的数值索引这个具体数值需要试一下或者干脆不画xline。第三如果你仍然担心keep会漏掉系数可以显式列出所有需要的系数coefplot event, keep(1.treat#-4.rel_time 1.treat#-3.rel_time 1.treat#-2.rel_time /// 1.treat#0.rel_time 1.treat#1.rel_time 1.treat#2.rel_time 1.treat#3.rel_time /// 1.treat#4.rel_time) vertical yline(0)这种写法虽然啰嗦但好处是不会匹配错也方便在coeflabels里自定义标签。另外还有一个容易忽略的点如果你在reghdfe之后用了est store之后又跑了别的回归想重新画图必须先est restore event把估计结果调回来否则coefplot默认是用当前内存里的结果画图很容易画成其他模型的内容。这个错误我犯过不止一次。3.3 坑三图是画出来了但丑到不能放进论文第三个坑不是“画不出来”而是“画出来的图没法见人”。默认的coefplot输出非常朴素没有零线、点之间没有连线、置信区间是一条垂直虚线、横轴标签直接显示成1.treat#-4.rel_time这种变量名。直接放到论文里审稿人大概率会质疑你的专业性。先说横轴标签问题。默认显示系数名正确做法是用coeflabels手动指定coefplot event, keep(1.treat#*) vertical yline(0) /// coeflabels(1.treat#-4.rel_time -4 1.treat#-3.rel_time -3 /// 1.treat#-2.rel_time -2 1.treat#0.rel_time 0 /// 1.treat#1.rel_time 1 1.treat#2.rel_time 2 /// 1.treat#3.rel_time 3 1.treat#4.rel_time 4)这样横轴就变成了-4、-3、-2、0、1、2、3、4便于读者对照政策时点。再说图形样式。平行趋势图的标准形态是“带置信区间的点线图”所以必须加上recast(connected)让散点之间用连线连接起来。同时用yline(0, lpattern(dash) lcolor(black))画一条零线方便读者判断系数是否落在0附近。置信区间的样式建议用ciopts(recast(rcap) lwidth(thin) color(navy))这样看起来比较纤细、学术。我常用的完整画图代码是这样coefplot event, keep(1.treat#*) vertical yline(0, lpattern(dash) lcolor(black)) /// xline(4.5, lpattern(dash) lcolor(gs8)) /// coeflabels(1.treat#-4.rel_time -4 1.treat#-3.rel_time -3 /// 1.treat#-2.rel_time -2 1.treat#0.rel_time 0 /// 1.treat#1.rel_time 1 1.treat#2.rel_time 2 /// 1.treat#3.rel_time 3 1.treat#4.rel_time 4) /// recast(connected) ciopts(recast(rcap) lwidth(thin) color(navy)) /// ytitle(估计系数) xtitle(相对政策实施时间) /// legend(off) graphregion(color(white))这套图基本可以直接放进论文。如果想更加美观还可以提前设置Stata的绘图主题set scheme s1color另外导出图片时建议输出高分辨率PNG或矢量格式graph export 平行趋势检验.png, width(2400) replace如果期刊要求矢量图可以导出为.eps或.pdf。注意Stata在Windows上导出eps有时字体会出问题可以直接用.pdf再转成LaTeX需要的格式。还有一个细节是有些Stata版本尤其是旧版默认没有安装coefplot命令需要先安装ssc install coefplot如果网络不好也可以考虑用net install st0366, from(http://www.stata-journal.com/software/sj14-2)这种镜像方式安装。装好之后再确认which coefplot能看到路径才算真正装成功。4. 常见问题与排查技巧实录这一节把平时被问到最多的几个问题集中起来做成一个速查清单方便你排错。现象常见原因解决办法运行coefplot提示 command coefplot is unrecognized没有安装coefplot包ssc install coefplot或用net install方式安装提示 no coefficients foundkeep中的系数名匹配错误先matrix list e(b)看系数名再调整keep写法图形在基准期处断裂基准期系数被omitted属于正常现象不用处理或者用xline()画出政策时点竖线反而更能说明问题横轴标签是一大串变量名没有用coeflabels自定义标签用coeflabels将变量名替换为“-4、-3……”这样的相对时期置信区间特别宽图形上下跨度大样本量不足尤其是事件时间两端观测少考虑合并两端期别或检查是不是单位根/异常值干扰图例显示一堆数字编号legend没有关闭或设置加legend(off)图例信息在图题中说明即可导出PNG后图片模糊分辨率不够用graph export xx.png, width(2400) replace提高输出分辨率重复画图时图形没有更新内存中的估计结果被替换画图前先est restore event恢复到正确的估计结果除了这张表我再分享两个我自己踩过的特殊坑。第一个是聚类标准误导致的置信区间异常。如果聚类层级选择错误比如个体层级clustering和年份层级搞混了置信区间可能整体偏窄或偏宽平行趋势图看起来就会“过分显著”或“过分不显著”。我的建议是主回归用vce(cluster id)稳健性检验再用vce(cluster province)这类更高层级的聚类画图时至少报告一种不要混用。第二个是时间趋势项与事件时间项的共线性问题。有些模型里同时放了线性时间趋势和全部事件时间虚拟变量这会导致部分相对时期系数无法识别coefplot就会画出一些缺位点。如果出现这种情况先检查模型设定是否已经包含了i.year固定效应再判断事件时间虚拟变量是否编码正确。5. 从零复现一套完整的平行趋势检验流程为了让你能直接跑通这一节我给一套完整的模拟数据代码。数据是自己造的结构很简单100个个体、10年数据、一半是处理组、政策发生在2018年。你可以在Stata里逐行粘贴运行。clear all set seed 2024 set obs 100 gen id _n gen treat 0 replace treat 1 in 1/50 gen policy_year 2018 if treat 1 expand 10 sort id by id: gen year 2014 _n gen rel_time year - policy_year if treat 1 * 生成一个带个体效应和时间效应的y真实验的政策效应是政策后每期增加1 gen y 2 0.5 * treat 0.1 * (year - 2014) rnormal(0, 1) replace y y 1.0 if treat 1 year 2018 * 平行趋势检验 reghdfe y i.treat##ib(-1).rel_time, absorb(id year) vce(cluster id) est store event * 画图 coefplot event, keep(1.treat#*) vertical yline(0, lpattern(dash) lcolor(black)) /// xline(4.5, lpattern(dash) lcolor(gs8)) /// coeflabels(1.treat#-4.rel_time -4 1.treat#-3.rel_time -3 /// 1.treat#-2.rel_time -2 1.treat#0.rel_time 0 /// 1.treat#1.rel_time 1 1.treat#2.rel_time 2 /// 1.treat#3.rel_time 3 1.treat#4.rel_time 4) /// recast(connected) ciopts(recast(rcap) lwidth(thin) color(navy)) /// ytitle(估计系数) xtitle(相对政策实施时间) /// legend(off) graphregion(color(white)) graph export 平行趋势检验_复现.png, width(2400) replace这段代码跑完之后你会看到政策前-4到-2期的系数都不显著图形在0附近波动而政策后0到4期的系数开始明显向上偏离这就成功复现了“满足平行趋势假定且政策有效”的典型图表。如果你跑完发现图形和政策时点对不上优先检查rel_time的编码因为模拟数据里year从2014到2023处理组政策年是2018所以相对时期应该是-4到5但2018年当期是02023年是5注意基准期-1对应2017年。6. 关于coefplot画图的一些补充经验最后再分享几个散装经验都是实际操练中一点点试出来的。第一coefplot的keep()和drop()可以搭配使用。如果只想画政策前的几期和政策后的前几期可以用keep(1.treat#-4.rel_time 1.treat#-3.rel_time 1.treat#-2.rel_time 1.treat#0.rel_time 1.treat#1.rel_time 1.treat#2.rel_time 1.treat#3.rel_time)把窗口限制住如果只想剔除个别异常期别用drop(1.treat#-5.rel_time)更省事。第二平行趋势图的美化不必追求复杂。有些同学喜欢把每个点的置信区间用不同颜色标注显著或不显著这当然可以但核心信息是“政策前系数是否联合不显著”。所以审稿人最看重的是曲线前段是否平稳。图上只要点线清楚、零线明确、政策时点标注清楚就已经达到学术发表标准。第三如果期刊要求报告联合显著性检验可以这样做先估计完整模型再使用test命令检验所有政策前交互项的联合显著性。比如检验-4期到-2期的系数是否同时为0test 1.treat#-4.rel_time 1.treat#-3.rel_time 1.treat#-2.rel_time如果p值大于0.1说明不能拒绝这些系数联合为零的原假设平行趋势假设得到支持。这个结果可以补充在论文脚注里比单看图更有说服力。第四如果你的数据时间跨度很长或者事件窗口有20多期建议优先合并两端期数再画图否则图形会变得很扁平趋势特征被稀释。比如相对时期小于等于-6的合并为一组“-6及以前”大于等于6的合并为一组“6及以后”这样图形更聚焦于政策前后五期内的变化。我在实际使用中还有一个习惯画完图之后一定会手动盯着“政策前系数绝对值”看一遍任何一个超过0.1的系数都要回原数据定位问题。因为coefplot只负责画图不负责告诉你数据质量行不行。平行趋势检验最怕的不是图丑而是图好看但背后的设定是错的。总的来说coefplot是一个足够成熟的画图命令坑虽然多但几乎每个坑都有据可查、有解可依。把上面这3个坑避过去你的平行趋势图基本就能稳稳当当地出现在论文里了。