读这篇文章的人,多半是正在被实证论文折磨的学生,或者刚接触政策评估的年轻研究者。打开Stata,装好命令,敲下一行did,弹出一堆看不懂的输出,然后陷入迷茫:系数是哪个?显著性怎么看?平行趋势怎么检验?审稿人问起来怎么答?
双重差分DID是政策评估里最常用、也最容易出错的方法之一。这篇文章不绕弯子,直接说清楚从原理到落地操作的完整链路。数据应该长什么样、交互项怎么生成、基准回归用哪个命令、平行趋势如何画图证明、安慰剂检验怎么写代码、顶级期刊现在还在追问什么问题——按这个顺序走一遍,你就能把DID真正跑通,而不是停留在"只知道命令"的阶段。
1. DID的核心逻辑:为什么前后对比不够用
先说原理,但只说人话版。很多人第一次接触DID时,会忘记一个基础问题:我们到底在估计什么?
1.1 政策评估的困境:一个反事实难题
假设有一天,某市突然实施了限行政策,你要评估它是否降低了空气污染。最朴素的想法:拿政策实施前的平均空气质量,和实施后的平均空气质量比一比,差多少就是政策效果。
问题在于,空气污染受季节、气象、产业结构调整、周边城市传输等多种因素影响。即便没有限行政策,下个月的污染水平也可能自然下降。你看到的"前后差异",是政策效应和一系列时间趋势的混合体。这个混杂效应不剥离掉,结论就不干净。
所以研究者需要找一组"对照组"。这个对照组最好和限行城市高度相似——地理相邻、产业结构相近——但它没实施限行。这样,对照组在同时期内的变化,就可以代理"如果限行城市没有政策,会出现什么变化"。用处理组的前后差异,减去对照组的前后差异,剔除了共同的时间趋势,剩下的才是政策净效应。这就是"双重差分"名字的由来:第一次差分消除个体异质性,第二次差分消除时间趋势。
1.2 一张2x2表看懂DID的本质
把DID塞进一张四格表:
| 组别 | 政策前 | 政策后 | 前后差异 |
|---|---|---|---|
| 处理组 | A | B | B - A |
| 对照组 | C | D | D - C |
| 组间差异 | A - C | B - D | (B-A)-(D-C) |
右下角那个差分值,就是DID估计量。它的数学表达式很简单:
[ DID = ( \bar{Y}{T,after} - \bar{Y}{T,before} ) - ( \bar{Y}{C,after} - \bar{Y}{C,before} ) ]
放在回归框架里,变成大家最熟悉的形式:
[ Y_{it} = \alpha + \beta_1 Treat_i + \beta_2 Post_t + \beta_3 (Treat_i \times Post_t) + \gamma X_{it} + \epsilon_{it} ]
其中(\beta_3)就是政策净效应。理解这个回归式的关键在于:(Treat_i)吸收组别差异,(Post_t)吸收时间趋势,交互项才是政策效应。这在后面的Stata操作中反复出现。
1.3 一个必须理解的前提:平行趋势假定
DID能成立,核心不是数据有多好,而是满足平行趋势假定:如果没有政策干预,处理组和对照组的结果变量应该沿着平行路径演变。
这个假定绕不过去。没有它,DID就只是两个差分的机械减法。
理解平行趋势最好的方式,是找一个生活类比。想象两棵相邻的树,一棵施肥(处理组),一棵不施(对照组)。两棵树生长速度本来就差不多,施肥后长得更快。你观察到的长势差异,可以归因于肥料。但如果两棵树的初始土壤条件完全不同,一棵在肥沃地,一棵在贫瘠地,即便不施肥,生长速度也会不一样——这时候用对照树去模拟"处理树如果没有肥料会怎样",就不成立了。
2. 数据准备:先确认你的数据长什么样
跑DID之前,90%的问题出在数据形态上。见过太多人拿着一份只有几十行的截面数据,就试图跑DID。先搞清楚DID需要什么数据。
2.1 面板数据与重复截面数据
DID最常见的载体是面板数据:每个个体(城市、企业、个人)被追踪多个时期。面板数据里,你能控制个体固定效应,把不随时间变化的个体特征全部吸收掉,这是DID最理想的形态。
没有真实面板怎么办?重复截面数据也可以做DID。比如每年调查对象不同,但调查设计稳定,你可以从不同年份中抽取"政策实施前"和"政策实施后"两波样本。此时没有个体固定效应可用,但依然可以控制年份和组别效应。核心要求是:每个细胞(处理组-政策前、处理组-政策后、对照组-政策前、对照组-政策后)都得有足够样本量。
无论哪种数据,关键变量有三类:
- 标识变量:个体ID和年份/时期变量。
- 分组变量:该个体是否属于处理组((Treat))。
- 时间变量:该时期是否处于政策实施之后((Post))。
2.2 构造交互项:小心这种常见错误
基准回归里的核心解释变量是(Treat \times Post)。在Stata中,你可以用gen命令直接生成新变量:
gen did = treat * post这是最简单直接的做法。但如果你在回归里写成下面这样:
reg y treat post treat#post, vce(cluster id)注意了:treat#post和treat*post不完全等价。前者会生成全交互(包括同一个变量的虚拟变量)的展开形式,在某些情况下会多出一堆变量。更稳妥的方式,是显式定义交互项,或者使用c.treat#c.post告诉Stata这是两个连续变量的乘积。实际操作中,我更推荐先gen did = treat * post,然后在回归里直接放did变量,简单明了,不易出错。
焦虑点提醒:生成交互项之前,先确认treat和post都是0/1二值变量。如果treat是连续变量(比如政策强度),那DID就变成了"强度DID"(即连续型DID),模型解释要相应调整。
2.3 数据形态检查清单
不管你是从原始数据库(如中国工业企业数据库、CNRDS、CSMAR)下载的数据,还是自己构建的平衡面板,建议在跑回归前先做这几步检查:
xtset id year确认面板声明成功,没有报"repeated time values"错误。tab treat post看看四类样本量是否充足,有没有出现某类细胞为0的情况。summarize y treat post did观察变量的量纲和取值范围,确认没有异常值。
这一步花十分钟,回报巨大。很多人跑出来的DID结果极其诡异,回查数据发现处理组和政策后时期的交叉项全是0——那还回归什么。
3. Stata基准回归:从命令到结果解读的完整梳理
数据搞定,进入核心环节:运行DID回归。这里介绍三种命令,本质估计量相同,但使用场景和输出细节有差异。
3.1 基础OLS回归写法
最经典的写法,直接对前文的回归方程对应:
reg y treat post did ctrl1 ctrl2, vce(cluster id)几个关键点逐一说明。
第一,标准误。DID回归的扰动项往往在个体内自相关,所以一定要聚类到个体层面(即政策评估的单元),写成vce(cluster id)。如果你的处理变量在更高级别变化(比如要研究省级政策,处理组按省划分),聚类层级至少要省级。Cameron等人的经验法则是:聚类数量太少(少于30或40个)时,聚类稳健标准误可能失效,此时可考虑bootstrap或野生聚类自助法。
第二,控制变量。控制变量加入原则在DID里被经常误解。大家以为控制变量加得越多越好,其实不然。加控制变量的目的,是缓解释义时段变化的混杂因素,而不是"让R²更高"。要警惕坏控制变量:政策实施后受到影响的结果变量(中介变量、事后变量)不能进回归,否则会引入偏差。这类问题在实证文献中被称为"坏控制"(bad control)问题。我的经验是,基准回归先不加控制变量跑一遍,再加时变控制变量跑一遍,两个结果一起报,既展示稳健性,也避免"控制变量选择争议"。
第三,个体固定效应和时间固定效应。细心的读者会发现,reg y treat post did里没有显式添加个体和年份固定效应。实际上,广义DID通常用双向固定效应(TWFE)模型:
xtreg y did ctrl1, fe i(id) i(year) vce(cluster id)或者等价地:
reghdfe y did ctrl1, absorb(id year) vce(cluster id)reghdfe是目前最推荐的工具,速度快,内存占用低,还便于吸收高维固定效应。记得先安装:
ssc install reghdfe这里注意:在双向固定效应模型中,treat和post的主效应已经被个体固定效应和时间固定效应吸收了,所以只需放交互项did。这是初学者最容易困惑的点——为什么reg写法里有treat和post,而xtreg写法里不需要了。
3.2 diff命令:傻瓜式但有用
Stata社区还有一个外部命令diff,专门为DID优化:
ssc install diff diff y, t(treat) p(post) cov(ctrl1 ctrl2) robustt()里面放处理组变量,p()里面放政策前后变量,cov()是控制变量。运行后输出会自动给出处理组前后均值、对照组前后均值、双重差分估计量和t统计量,还会给出多种标准误版本(常规、稳健、聚类等)。
说实话,我论文初稿有时候也用diff快速扫一眼结果,因为它输出格式很像教材,方便检查估计量是否稳健。但正式写作时,我更倾向reghdfe——控制更细致,形式更清晰,便于加固定效应和高维固定效应。
3.3 结果解读:β₃到底说明什么
无论用哪种命令,核心关注点都是交互项did的系数。这个系数为正,表示政策导致结果变量显著上升;为负,则意味着政策降低结果变量。判断标准:看系数大小、显著性水平(p<0.05或p<0.01),以及经济显著性(系数量级是否有现实意义)。
以前阵子做一个最低工资调整评估为例,交互项系数是-0.08,p=0.03。说明最低工资调整使得就业概率平均降低8个百分点。如果只报系数和p值,不解释经济意义,审稿人一定会挑刺。8个百分点在这种研究背景里是不小的效应,但我也会告知读者,这一效应主要集中在低技能劳动者群体,平均效应掩盖了异质性。
4. 平行趋势检验:DID成立的生命线
基准回归跑完,系数好看还不够。同行评审看到DID结果时,第一反应几乎都是:"你如何保证平行趋势?"这个关口过不去,前面都是白搭。
4.1 事件研究法:把时间动态完全展开
平行趋势检验使用最普遍的方式,是事件研究法(event study),也被称为动态DID。
基本思想:不把政策效果压缩成一个单一系数,而是把政策实施前后每一期的处理效应都估计出来。如果政策前的各期系数统计上不显著异于0,说明处理组和对照组在政策前没有系统性差异,平行趋势成立。
实现方式:把政策时间作为事件时间(event time),生成一系列虚拟变量,比如pre4表示政策前第4期,post3表示政策后第3期,然后跑如下回归:
reghdfe y pre4 pre3 pre2 post1 post2 post3, absorb(id year) vce(cluster id)通常以政策前一期(或政策当期)作为基准组,省略不加入模型,否则会遇到完全共线性。注意,我这里用公式表示"某期效应",实际操作中用gen pre2 = (event_time == -2)等命令逐期生成虚拟变量。
还有一种更快的方式,是用coefplot直接看系数图:
coefplot, keep(pre4 pre3 pre2 post1 post2 post3) vertical yline(0) xline(2.5)如果政策前各期系数置信区间跨越0线,说明平行趋势得到直观支持。政策后的系数若是渐进上升或下降,还能读出动态效应——政策起效的时滞、持续性如何。
4.2 怎么做事件时间变量:一个实操细节
构造事件时间变量听起来简单,但有个常见的坑:政策时间不是同一个时点怎么办。比如你要评估的是各省陆续实施的某项试点,不同省份的"政策后第1期"对应不同日历年份。标准做法是使用相对时间:
gen event_time = year - policy_year if treat == 1 replace event_time = 0 if treat == 0重点:对于从未受政策影响的对照组,event_time统一设为0或一个固定值,并且不能参与事件研究虚拟变量的生成(否则对照组被错误地当作"政策当期")。通常建议对照组单独处理,保持treat=0的样本在所有pre/post虚拟变量中都是0。
4.3 平行趋势检验没通过怎么办
先说结论:如果平行趋势检验明显不通过,别硬着头皮隐瞒或伪造结果。有几种正路可走。
第一,尝试调整样本期。政策实施前的长窗口中可能存在特定年份的冲击,比如某一年突然有个全国性政策影响了所有地区。适当缩短事件窗口,或添加省份特定线性时间趋势(i.id#c.year),有时能缓解。
第二,使用匹配方法预处理数据(PSM-DID)。通过倾向得分匹配,把处理组和对照组的可观测特征拉近,再在匹配样本上做DID。这在一定程度上缓解了可观测变量不平衡造成的平行趋势偏离,但注意它不能解决不可观测的遗漏变量问题。
第三,使用合成控制法。如果对照组和处理组存在严重不可比性,合成控制法用多个对照地区加权构造一个"合成处理组",它的平行趋势表现通常更好。
5. 安慰剂检验:证明政策效果不是窗外的运气
平行趋势检验回答的是"政策前是否可比",安慰剂检验则回答"政策效果是否可信"——你是不是捡了一个统计上偶然出现的显著结果。
5.1 常规思路一:随机分配处理组
原理:把"处理组"标签在所有样本中随机打乱,这样理论上就不存在真实的政策效应。重复几百次,看看随机分配下的t值分布。如果真实的政策效应不是靠运气,那么真实的t值应该处在随机分布的极端尾部(偏大或偏小)。
具体操作步骤:
* 假设真实的t值为 real_t forvalues i = 1/500 { gen random_treat = runiform(0,1) sort random_treat gen pseudo_treat = _n <= N_treat // 保证处理组样本量与原数据一致 * 在pseudo_treat和post构成的交互项下跑回归,记录系数和t值 local t_pseudo = _b[did]/_se[did] }手动写循环效率低,更推荐用permute:
permute did = _b[did], reps(1000) seed(12345) saving(perm_results.dta) nodots: reghdfe y did, absorb(id year) vce(cluster id)跑完后,把真实系数和安慰剂系数的分布对比,画出系数分布图。如果真实系数在安慰剂分布的边缘,说明你的结果不是随机噪声。
5.2 常规思路二:提前政策时间
另一种安慰剂做法是"虚构政策时点"。比如真实政策发生在2018年,你假设政策发生在2015年(并删除2018年以后的样本,以免真实政策污染),然后跑DID。如果虚构政策产生了显著的"假效果",说明原结果可能捕捉的是假趋势,而不是真实政策效应。
这个做法的直觉是:如果在政策还没发生之前,处理组和对照组的差异已经显著,那说明两组本身趋势不同,真实政策效应可能是虚假的。
5.3 画图展示安慰剂结果
我在论文里最常用的展示方式是系数分布图。横轴为安慰剂估计系数,纵轴为密度或核密度,真实系数用一个竖直线标记。如果竖直线落在分布主体之外,读者一眼就能看出政策效果是真实存在的。用twoway kdensity或者histogram都能快速绘制。
不建议只报"做了500次安慰剂,平均系数接近0,少数显著"这种模糊说法。审稿人想看到的是分布、具体排除随机性的证据、设定的充分性。
6. 从基准回归到发表级结果:高阶问题和踩坑经验
写完基准回归、平行趋势检验、安慰剂检验的代码,理论上文章的实证部分已经能看了。但从"能看"到"发表",中间还隔着几座大山。这是我这几年被拒稿和审稿别人论文时攒下的体会。
6.1 多期DID的时代:不能再假装所有政策同一时点发生
过去的DID教学常假设政策在某一时点统一实施,处理组和对照组清晰分明。但现在,越来越多的政策是逐步试点、分批推开的,不同的处理组在不同的时点开始受政策影响。这种情形下,传统双向固定效应模型面临着异质性处理效应的困扰——不同组别在不同时间接受处理时,TWFE估计量可能不是个体处理效应的加权平均,甚至可能出现"负权重"问题。
如果是多期DID,建议:
- 首先做Bacon分解(Goodman-Bacon分解),看看TWFE估计量如何由各种2x2比较构成。
- 如果存在大量"早处理组作为后处理组的对照组"的情形,考虑使用Callaway和Sant'Anna提出的
csdid命令,或者Sun和Abraham的eventstudyinteract命令。 - 报告时同时给出TWFE和异质性稳健估计量,作为对照。
ssc install csdid csdid Y, ivar(id) time(year) gvar(first_treat_year) method(dripw) estat event这个命令输出的动态效应图比我手动写的event study图要规范得多,推荐直接用。
6.2 控制变量控制到什么程度
前文说了坏控制问题。这里再补充一条经验:控制变量一旦进入回归,就要在论文里交代选取理由。常见的组合是:地区层面的经济特征(人均GDP、产业结构、人口密度)、企业层面的基本特征(规模、年龄、所有制)。不要过度控制——控制变量过多会造成"过度调整偏差",尤其当控制变量与处理变量高度相关时,标准误会被夸大,估计量不再有效。
如果审稿人质疑"你们是否控制了XX变量",最稳妥的策略是展示一个表格:第一列不含控制变量,第二列加一组,第三列加另一组,第四列全加。系数如果稳定,你的结论就坚固;系数如果翻来覆去变号,问题就出在样本或模型上。
6.3 聚类稳健标准误、Bootstrap法和"幸存者偏差"杂谈
聚类层级选择是个高频争议点。处理变量在省级变化,但聚类到省级时只有不到10个省,聚类稳健标准误会过于激进;聚类到更细的县级,又可能低估标准误。实践中,我倾向于至少提供两个层级的标准误(比如省级和县级),或者采用多层聚类。Stata 9.2及以上版本支持vce(cluster id1 id2)的双重聚类。
另一个被低估的问题是"进行DID的面板数据是否平衡"。如果不平衡,某些个体可能在政策前后都出现退出,造成样本选择性。一个简单的处理是先检查各时期样本量差异,如果是企业数据还要考虑企业进入退出带来的存活偏差,必要时使用xtbalance或者估计面板attrition模型。
6.4 结果呈现的小规范
最后说论文写作中那些"小事":
- 表格里要标出标准误的聚类层级,不要写"括号内为标准误"就完事。
- 显著星星标到10%、5%、1%三级,并说明是双尾检验。
- 表格注释里写清楚样本量、R²、固定效应范围。
- 如果用了
reghdfe,报告里要说明被吸收的控制变量数(No. of absorbed FE)。
一个规范的结果表格,能让审稿人在30秒内抓住你的识别策略,而不是追问一堆细节。把这个做好了,比任何花哨的计量方法都能提升论文的接受概率。
工具命令汇总一下,方便之后按图索骥:
| 目的 | Stata命令 | 说明 |
|---|---|---|
| 面板声明 | xtset id year | 回归前必做 |
| 基准回归 | reghdfe Y did, absorb(id year) vce(cluster id) | 首选,吸收高维固定效应 |
| 快速诊断 | diff Y, t(treat) p(post) robust | 快速查看DID结果 |
| 平行趋势 | reghdfe+ 事件时间虚拟变量 | 政策前系数应不显著 |
| 系数图 | coefplot | 画事件研究系数图 |
| 随机安慰剂 | permute | 随机分配处理组做安慰剂 |
| 多期DID | csdid | 异质性稳健DID估计量 |
| Bacon分解 | bacondecomp | 诊断TWFE估计量构成 |
我在实际做项目时,最常提醒自己的就是那句老话:DID只有两条路能走通——要么你的平行趋势足够漂亮,要么你的政策冲击足够外生。跑一百遍回归,不如把这两件事想透。这篇文章给了你从数据准备到最终表格的全部操作路径,剩下的,就是对着你的真实数据动手跑一遍,遇到报错再回头查,跑完再想解释。这个循环,才是真正学会DID的路。