多期双重差分模型Stata实战:从平行趋势检验到异质性处理效应全流程

发布时间:2026/11/2 4:20:24
多期双重差分模型Stata实战:从平行趋势检验到异质性处理效应全流程

多期双重差分模型在政策评估类研究里出现的频率越来越高,但凡涉及“某项政策分批落地、不同个体在不同时间点受到冲击”的场景,几乎都绕不开它。我这些年帮不少同学和朋友看过实证代码,发现一个特别普遍的现象:很多人把DID理解成“跑一个交互项就完事”,结果平行趋势没检验、处理时点没对齐、标准误没聚类,最后系数看着显著,审稿人一问就露馅。这篇内容就围绕多期DID的Stata操作展开,把从数据整理、模型设定、平行趋势检验到异质性处理效应的完整链路讲透,适合刚接触政策评估的实证新手,也适合已经跑过两期DID、想升级到多期场景的进阶读者。核心关键词多期双重差分、DID、Stata操作、平行趋势、事件研究法、异质性处理效应,会自然穿插在各个环节里,不堆砌,只讲能直接抄作业的东西。

1. 多期DID到底在解决什么问题

1.1 从两期DID到多期DID的思维跃迁

两期DID的逻辑很干净:一个处理组、一个对照组,政策在某一年统一实施,比较前后变化之差。但现实中的政策很少这么整齐。比如某项改革在不同地区分批试点,A地2015年落地,B地2017年落地,C地一直没实施;或者某平台功能对不同用户群体灰度上线,时间点各不相同。这时候如果硬套两期DID,把所有处理组揉成一个“处理组”虚拟变量,就会把“何时受处理”这个关键信息丢掉,估计出来的系数其实是多个时点效应的加权平均,解释起来非常别扭。

多期DID(也叫交错DID、渐进DID)的核心思路,是给每个个体定义各自的处理时点,用“个体是否已经进入处理状态”来构造处理变量。数学上它仍然是“处理组前后变化减去对照组前后变化”,只不过这个“前后”是相对于每个个体自己的处理时点而言的。理解这一点特别重要,因为后面所有的数据整理、模型设定、平行趋势检验,都是围绕“处理时点”这个变量展开的。我见过太多人卡在第一步,就是因为脑子里还停留在“政策年”这个单一时间点上。

这里要强调一个概念:处理时点(treatment timing)。它不是一个年份变量,而是每个个体首次受到政策冲击的时间。对于从未受处理的个体,通常设为一个缺失值或者一个永远不会出现的大数值(比如9999或.year)。这个设定方式直接影响后续模型能否正确识别,后面会详细讲。

1.2 多期DID的适用边界与常见误用

多期DID不是什么场景都能用。它有几个隐含前提:第一,处理时点的分配不能与结果变量的潜在趋势相关,也就是常说的“处理时点外生”;第二,处理效应最好在同质或者至少可加的意义上稳定;第三,样本期内要有足够的从未处理个体或尚未处理个体作为对照。如果所有个体最终都会被处理,只是时间早晚不同,那么晚期处理组在早期其实可以充当对照组,但这种“尚未处理组作对照”的设计对平行趋势的要求更苛刻,需要额外检验。

常见的误用有这么几类。一是把多期DID当成普通固定效应回归,随便加个体和时间固定效应就交差,完全不做平行趋势检验。二是处理时点定义混乱,比如用政策发文年份而不是实际执行年份,导致处理效应被稀释。三是在存在异质性处理效应时,仍然用传统的双向固定效应(TWFE)估计量,结果出现“负权重”问题,系数甚至可能符号相反。这几点在后面章节会逐一展开,尤其是负权重问题,是近几年方法论讨论的热点,做多期DID的人必须心里有数。

1.3 为什么Stata是主流选择

做多期DID,Stata的生态确实成熟。基础的双向固定效应用reghdfe就能跑,平行趋势和事件研究法有eventdd、coefplot这类工具,异质性处理效应有csdid、did_multiplegt、bacondecomp等命令,基本覆盖了从入门到前沿的全流程。R语言虽然也有对应包,但Stata在实证经济学和政策评估圈子的用户基数大,命令文档和社区讨论更密集,遇到问题更容易找到答案。

不过工具多也意味着选择困难。我的建议是:先把TWFE跑通、把平行趋势检验做扎实,再考虑上异质性稳健估计量。不要一上来就堆砌新命令,结果连基本设定都没搞清楚。下面从数据准备开始,一步步往下走。

2. 数据准备与处理时点构造

2.1 面板数据的结构要求

多期DID对数据结构的第一个要求是个体-时间层面的平衡或非平衡面板。每个个体在每个时间点最好都有一条观测,如果某些个体某些年份缺失,reghdfe和csdid一般能处理非平衡面板,但事件研究法的时间窗口对齐会麻烦一些。所以我的习惯是,先xtset id year确认面板设定无误,再用xtdescribe看一眼缺失模式。如果缺失是随机的、比例不大,可以保留;如果系统性缺失,比如某些地区早期数据完全没有,那就要考虑缩短样本期或者剔除这些个体。

第二个要求是个体标识和时间标识唯一确定一条观测。跑之前务必duplicates report id year检查重复,重复观测会导致固定效应估计出错,而且报错信息往往不直观。我踩过一次坑,数据合并时没注意一对多,结果每个个体每年有两条记录,系数直接翻倍,排查了大半天才发现是重复行的问题。

2.2 处理时点变量的三种构造方式

处理时点变量(通常命名为first_treat或treat_year)的构造,直接决定模型识别。常见有三种写法。

第一种,从未处理组设为缺失值。这是csdid等命令的推荐做法,因为缺失值在计算组别时会被自动识别为“从未处理”。写法上可以用gen first_treat = .然后对处理组赋值。

第二种,从未处理组设为一个极大值,比如9999。这种写法在手动构造处理虚拟变量时比较方便,因为year >= first_treat对9999永远为假,自然就是未处理状态。但要注意,如果时间变量本身有超过9999的取值(几乎不可能),会出问题。

第三种,从未处理组设为0。这种写法要小心,因为0容易被误认为“第0年处理”,在事件研究法里如果直接拿它做基准,会出错。我一般不建议用0,除非后续代码里明确做了区分。

具体操作上,假设原始数据里有一个政策实施年份变量policy_year,从未实施的个体该变量为空。可以这样构造:

gen first_treat = policy_year replace first_treat = 9999 if missing(policy_year)

或者用缺失值版本:

gen first_treat = policy_year * 保持缺失,不替换

两种写法对应不同的后续命令,用csdid时推荐缺失值版本,用TWFE手动构造时推荐9999版本。这个细节看似小,但选错了后面会反复报错。

2.3 处理状态虚拟变量的生成

有了处理时点,就可以生成核心解释变量——个体在当年是否处于处理状态。标准写法是:

gen treat = (year >= first_treat) & (first_treat != 9999)

这一行代码的含义是:当年份大于等于该个体的处理时点,且该个体不是从未处理组时,treat取1,否则取0。注意这里用&而不是if,是为了让从未处理组的treat正确取0而不是缺失。如果写成gen treat = (year >= first_treat),那么first_treat为9999的个体,year >= 9999为假,treat取0,其实也能用,但逻辑上不够清晰,而且如果first_treat是缺失值,结果会是缺失,容易埋雷。

生成之后,建议用tab year treat交叉看一下,确认处理组在政策前后的分布符合预期。比如2015年处理的组,2015年之前treat应该全是0,2015年及之后全是1。如果发现某个体在政策前就出现treat=1,说明处理时点赋值有误,要回去查原始数据。

2.4 样本期与处理组的匹配检查

一个容易被忽略的步骤,是检查处理时点是否落在样本期内。如果某个体的处理时点是2010年,但样本从2012年开始,那么这个个体在整个样本期内都处于处理状态,对识别没有贡献,反而可能干扰估计。同理,如果处理时点在样本期结束后,那这个个体实际上等同于从未处理组。

可以用下面的代码快速排查:

sum first_treat if first_treat != 9999 * 对比样本期 sum year

如果发现大量处理时点落在样本期外,要么调整样本期,要么把这些个体剔除或重新归类。我一般会在论文的数据说明部分明确写出“剔除处理时点不在样本期内的观测”,这是审稿人常问的点。

3. 基准TWFE模型与Stata实现

3.1 双向固定效应模型的设定逻辑

多期DID最基础的估计量是双向固定效应(TWFE)模型,形式上是:

Y_it = α + β·Treat_it + γ·X_it + μ_i + λ_t + ε_it

其中Treat_it就是上一节生成的处理状态虚拟变量,μ_i是个体固定效应,λ_t是时间固定效应,X_it是控制变量。β就是我们关心的平均处理效应。个体固定效应吸收了个体不随时间变化的特征,时间固定效应吸收了所有个体共同面临的冲击,剩下的变异来自“个体是否已进入处理状态”的差异。

用Stata实现,首选reghdfe,因为它处理高维固定效应快,而且支持多维聚类。基本写法:

reghdfe y treat x1 x2, absorb(id year) vce(cluster id)

这里absorb(id year)同时吸收个体和时间固定效应,vce(cluster id)在个体层面聚类标准误。聚类层级的选择要看处理变异的层级:如果处理是在个体层面分配的,聚类到个体;如果处理是在更高层级(比如地区、行业)分配的,应该聚类到那个层级。这一点很多人搞错,聚类层级太低会低估标准误,导致过度拒绝原假设。

3.2 控制变量的取舍原则

控制变量加不加、加哪些,是多期DID里争议比较大的点。我的原则是:只加那些在处理时点之前就已确定、且可能同时影响处理分配和结果变量的变量。如果控制变量本身是处理后的结果(比如政策影响了某个中介变量,你又把它当控制变量),会造成“坏控制”,引入偏误。

具体操作上,我通常先跑一个不加控制变量的基准回归,再逐步加入控制变量,观察系数稳定性。如果加入某个控制变量后系数大幅变化,要警惕这个变量是否被处理效应污染。另外,控制变量最好做标准化或者取对数,避免量纲差异导致系数解读困难。

3.3 系数解读与经济学含义

TWFE估计出的β,在满足平行趋势和同质处理效应的前提下,可以解释为“处理组在政策后的平均变化减去对照组同期变化”。但要注意,多期DID下这个β其实是所有“已处理vs未处理”比较的加权平均,权重可能为负(这就是后面要讲的负权重问题)。所以解读时不能简单说“政策使Y提高了β”,更稳妥的表述是“在TWFE框架下,估计得到的平均处理效应为β”。

如果Y取了对数,β可以近似解读为百分比变化;如果Y是水平值,β就是绝对变化量。我建议在论文里同时报告经济显著性和统计显著性,比如“系数为0.05,相当于样本均值的8%”,这样审稿人更容易判断实际意义。

3.4 一个完整的基准回归示例

假设研究某项试点政策对企业创新产出的影响,数据包含企业id、年份year、专利申请量patent、研发投入rd、企业规模size,处理时点变量first_treat。完整代码如下:

* 设定面板 xtset id year * 生成处理变量 gen treat = (year >= first_treat) & (first_treat != 9999) * 基准回归 reghdfe patent treat rd size, absorb(id year) vce(cluster id) * 输出结果 est store baseline

跑完之后重点看三件事:treat的系数符号是否符合理论预期、标准误是否合理、样本量是否与预期一致。如果样本量突然少了很多,多半是控制变量缺失值导致的,可以用misstable summarize查一下缺失模式。

4. 平行趋势检验与事件研究法

4.1 平行趋势为什么是多期DID的命门

平行趋势假设说的是:如果没有政策,处理组和对照组的結果变量应该沿着相同的趋势变化。这个假设无法直接检验(因为反事实不可观测),但可以通过政策前的趋势对比来间接验证。多期DID下,平行趋势的检验比两期DID更复杂,因为处理时点不同,需要相对于每个个体自己的处理时点来对齐时间。

如果平行趋势不成立,TWFE估计的β就混杂了趋势差异,不能解释为因果效应。我见过不少论文只报告回归结果,平行趋势检验放在附录里草草带过,这种在严谨的审稿中很容易被质疑。稳妥的做法是把事件研究图放在正文,直观展示政策前系数不显著、政策后系数逐渐显现。

4.2 事件研究法的模型设定

事件研究法(event study)是检验平行趋势的标准工具。思路是把处理时点前后的时间拆成一系列虚拟变量,看政策前各期的系数是否接近0。模型形式:

Y_it = α + Σ β_k·D_it^k + γ·X_it + μ_i + λ_t + ε_it

其中D_it^k表示个体i在处理时点前后第k期的虚拟变量。k取负值表示政策前,取正值表示政策后。通常以政策前一期(k=-1)作为基准组省略,避免完全共线性。

Stata里可以用eventdd命令快速实现,也可以手动生成虚拟变量。手动版本更灵活,适合需要精细控制的场景:

* 生成相对时间 gen rel_time = year - first_treat replace rel_time = . if first_treat == 9999 * 生成各期虚拟变量,以-1期为基准 forvalues k = 5(-1)2 { gen pre_`k' = (rel_time == -`k') } forvalues k = 0/5 { gen post_`k' = (rel_time == `k') }

注意这里只取了前后各5期,实际取多少期要看样本期长度和处理时点分布。如果某些期的观测太少,可以合并相邻期,避免估计不稳定。

4.3 事件研究图的绘制与解读

跑完回归后用coefplot画图:

reghdfe y pre_5 pre_4 pre_3 pre_2 post_0 post_1 post_2 post_3 post_4 post_5 x1 x2, absorb(id year) vce(cluster id) coefplot, keep(pre_* post_*) vertical yline(0) xline(5.5, lpattern(dash)) ci(95)

解读时重点看两点:政策前各期系数是否在0附近且置信区间包含0,政策后系数是否呈现预期的方向变化。如果政策前某一期系数显著偏离0,说明平行趋势存疑,需要进一步排查原因,比如是否有预期效应、是否有其他同期政策干扰。

这里有个实操细节:rel_time的基准期选择。通常用-1期,但如果-1期观测特别少,可以改用-2期或其他期。基准期不同,各期系数的绝对值会变,但相对 pattern 不变。论文里要明确说明基准期是哪一期。

4.4 平行趋势不通过时的补救思路

如果平行趋势检验没通过,不要急着放弃。可以先排查几个方向:一是控制变量是否遗漏了随时间变化的混杂因素,加入个体-specific的时间趋势项试试;二是样本是否存在异常值,做一下缩尾或者剔除极端观测;三是处理时点是否存在预期效应,比如政策公布早于实施,个体提前反应,这时候可以把处理时点前移一期再检验。

如果这些都不奏效,可以考虑用合成控制法、匹配DID等替代方案,或者诚实地在论文里讨论平行趋势的局限性,用敏感性分析展示结果对趋势假设的稳健性。硬凹一个通过的图,反而风险更大。

5. 异质性处理效应与负权重问题

5.1 负权重从哪来

前面提过,多期DID下TWFE的β是多个2x2比较的加权平均,而权重可能为负。负权重出现的根源是:当处理效应随时间或组别变化时,已经处理的组会被当作后来处理组的对照,如果早期处理组的效应持续存在,这种“对照”就污染了估计。更直白地说,TWFE隐含假设处理效应在同组内不随时间变化、在不同组间相同,一旦违背,权重就可能为负,导致β甚至符号相反。

这个问题在2018年之后被大量讨论,代表性的诊断工具是bacondecomp。它把TWFE的β分解成所有可能的2x2比较,展示每个比较的权重和效应大小。如果发现负权重占比很高,说明TWFE结果不可靠,需要换用异质性稳健的估计量。

5.2 Goodman-Bacon分解的Stata操作

bacondecomp y treat, ddetail

输出会列出各类比较(处理组vs从未处理组、早期处理组vs晚期处理组等)的权重和平均效应。重点看“Earlier T vs Later C”和“Later T vs Earlier C”这两类的权重,如果它们为负或者占比很大,就要警惕。这个命令还能画出权重分布图,直观展示哪些比较在主导估计结果。

5.3 异质性稳健估计量的选择

目前主流的异质性稳健估计量有几类。csdid(Callaway-Sant'Anna)基于组别-时间平均处理效应,允许处理效应随组别和时间变化,是比较常用的选择。did_multiplegt适合处理效应动态变化且存在多个处理时点的场景。sunab(Sun-Abraham)通过交互加权的方式修正事件研究法的负权重问题。

以csdid为例,基本用法:

csdid y x1 x2, ivar(id) time(year) gvar(first_treat) method(dripw) estat event csdid_plot

这里gvar(first_treat)指定处理时点变量,从未处理组用缺失值或0表示(具体看版本要求)。method(dripw)是双重稳健估计,也可以用method(reg)。estat event输出事件研究形式的动态效应,csdid_plot画图。

选择哪个估计量,要看研究问题和数据结构。如果处理效应预期随时间累积,csdid的动态效应很合适;如果更关心整体平均效应,可以用simple选项。我的建议是至少报告TWFE和一种异质性稳健估计量的结果,做交叉验证,如果两者结论一致,说服力更强。

5.4 结果对比与稳健性呈现

把TWFE和csdid的结果放在一张表里对比,是现在比较规范的做法。如果两者系数接近,说明负权重问题不严重;如果差异大,要以异质性稳健估计量为准,并在论文里说明原因。稳健性部分还可以加入:改变样本期、剔除特殊个体、更换聚类层级、加入时间趋势项等,展示核心结论不随设定变化而动摇。

6. 实操避坑与常见问题速查

6.1 命令报错与数据问题的排查顺序

跑多期DID时遇到的报错,八成出在数据层面。我总结了一个排查顺序:先xtdescribe看面板结构,再duplicates report查重复,然后sum first_treat看处理时点分布,最后tab year treat看处理状态是否合理。这四步走完,大部分低级错误都能定位。

常见报错里,reghdfe提示“absorb variable has missing values”通常是固定效应变量有缺失;csdid提示“gvar must be positive”多半是处理时点变量用了0表示从未处理组,改成缺失值或检查赋值逻辑即可。

6.2 标准误聚类的层级选择

聚类层级选错是审稿意见的高频来源。原则是:处理在哪一层分配,就聚类到哪一层。如果政策是省级实施,个体是企业,应该聚类到省而不是企业。如果处理在个体层面,聚类到个体。如果同时存在多个层级的相关性,可以用双向聚类vce(cluster id province),但要注意双向聚类在小样本下可能不稳定。

6.3 处理时点定义的常见错误

处理时点用政策发文年还是执行年,要看政策实际生效时间。如果发文当年就执行,用发文年;如果有过渡期,用执行年。另外,如果个体在样本期内多次受到同类政策冲击,要明确是取首次处理时点还是每次分别处理,这会影响模型设定。我一般建议取首次处理时点,并在论文里说明理由。

6.4 样本量与功效的平衡

多期DID对样本量要求不低,尤其是事件研究法拆出多期虚拟变量后,每期的观测可能很少。如果发现某些期的置信区间特别宽,可以考虑合并相邻期,或者缩短事件窗口。不要为了追求“完整”而保留观测极少的期次,估计不稳定反而影响可信度。

常见问题可能原因排查与解决
treat系数全为0或缺失处理时点变量赋值错误检查first_treat的缺失值和9999设定
平行趋势图政策前系数显著遗漏时变混杂因素或存在预期效应加入个体趋势项,或前移处理时点重检
csdid报错gvar相关从未处理组编码不符合要求改为缺失值或按文档要求编码
标准误异常小聚类层级过低提升聚类层级至处理分配层级
样本量骤减控制变量缺失misstable summarize排查缺失模式

6.5 论文写作中的表述规范

最后提一下写作。多期DID的论文里,方法部分要明确写出:处理时点的定义、样本期的选择理由、平行趋势的检验方式、是否处理了异质性效应问题。结果部分除了系数和标准误,建议报告经济显著性。稳健性部分至少包含一种异质性稳健估计量的对比。这些细节看似繁琐,但都是审稿人重点检查的地方,提前做好能省很多返修时间。

我个人在实际操作中的体会是,多期DID的难点不在命令本身,而在对“处理时点”和“对照逻辑”的理解。命令跑不通可以查文档,但设定错了,结果再漂亮也是空中楼阁。所以每次动手前,我都会先把数据结构、处理时点分布、对照组的构成在纸上画一遍,确认逻辑自洽再写代码。这个习惯帮我避开了不少返工的坑,也希望对你有用。