做政策评估、项目复盘或者任何因果推断研究的人,这几年应该都有一个共同的感受:只要数据里出现“不同时间点才落地的处理”,关于统计检验的讨论就绕不开。我最近复核一个区域性就业扶持项目的效果评估,A类城市在早期落地政策、B类城市一年多以后才跟进、C类城市在整个观察期内始终没有落地,合作方第一版分析直接把所有处理时点都虚拟变量化,跑一个双向固定效应回归就准备下结论。这恰恰是多时间点DID里最容易出问题的地方。
现在我把自己处理多时间点DID时的一套检验流程完整梳理出来。包括事件研究图怎么画、双向固定效应为什么会在交错处理下翻车、几种稳健估计量怎么操作、结果不一致时怎么排查。适合正在做面板数据政策评估、准备实证论文,或者只是想在自己报告里把因果结论写严谨的研究者。内容偏实操,代码用R和Stata双版本展示,复制下来改改数据就能用。
1. 从两期DID到多时间点DID:关键变化解析
1.1 经典两期DID的标准逻辑
先回到最基本的场景。假设你有一个处理组和一个对照组,政策在某个明确时点生效,数据恰好只有政策前后两期。DID的思想就是做两次差分:处理组前后变化减去对照组前后变化,得到处理效应。之所以可以做这个减法,核心依赖是平行趋势假设——如果政策没有落地,处理组的变化趋势会和对照组保持一致。
这个设计的优雅之处在于,它不需要控制所有影响结果的变量,只需要两组在没有政策时的轨迹平行。所以经典DID的“统计检验”任务相对简单:验证平行趋势,报告处理组前后差异的显著性,结束。
1.2 多时间点DID的两种常见形态
多时间点DID至少有两种形态,很多人没区分清楚就开始跑代码。
形态一:政策同一时间在全国或全样本范围内全面落地,但数据采集是多期面板。这种情况下处理组和对照组的身份不随时间变化,多出的时间点主要用来验证动态效应和政策前的平行趋势。检验起来相对温和,传统事件研究法就够用。
形态二:交错处理,也叫滚动采纳。处理组在不同时间点陆续进入处理状态,有人早处理、有人晚处理、还有人不处理。本文重点讨论的就是这种形态。
我用一个贯穿全文的虚构案例来说明。一个区域性就业扶持项目X,目标是提升参与者就业收入。A类城市在2018年落地,B类城市在2020年落地,C类城市在整个观察期内都没有落地。你的数据是一份城市-年份面板,2016年到2023年,每个城市有若干个体层面的观测值。现在要回答的问题有三个:政策落地前,三类城市的收入轨迹是否平行?政策落地后,收入提升是否真实、是否随时间增强或衰减?不同城市群体的反应可能不一致,怎样得到一个总体平均效应?
1.3 为什么检验任务突然变复杂
多时间点交错设计的难点,不在于“时间点变多了”,而在于处理状态在个体和时间维度上出现了交错。A类城市2018年就处理了,但B类城市直到2020年才开始处理。在估计B类城市效果的时候,A类城市已经被处理过,它还能不能当对照组?如果把A类城市拿去当B类城市的对照,而A类城市自己的处理效应还在随时间变化,那么这个“对照”本身就携带了处理效应,反事实被污染。
这就是多时间点DID统计检验的核心矛盾:处理组之间会互相污染。后面要介绍的负权重问题、稳健估计量问题,全都源于这个结构。所以多时间点DID的检验不是一个孤立步骤,而是一整套针对识别假设、估计方法和推断策略的系统性审查。
2. 多时间点DID检验体系:需要检验的四件事
2.1 四个检验目标拆解
很多人一说多时间点DID的检验,直觉反应就是“画个事件研究图看平行趋势”。但平行趋势只是第一层。完整来说,我觉得至少要覆盖四个目标。
第一个目标:平行趋势假设是否成立。政策落地前,各处理队列与对照组的趋势是否可比。第二个目标:动态处理效应是否稳定。处理效应可能随时间增强、衰减,甚至先滞后再显现,是否捕捉到了这种时变特征。第三个目标:处理效应在不同处理队列之间是否同质。早处理组和晚处理组的效应往往不同,忽略这种异质性会让总体估计失去解释力。第四个目标:统计推断是否可靠。标准误聚类层级是否合理、处理组数量少时渐进近似是否可靠、多重检验问题是否被忽略。
| 检验目标 | 核心问题 | 常用手段 | 关键判断标准 |
|---|---|---|---|
| 平行趋势 | 处理组与对照组处理前趋势是否可比 | 事件研究图、预处理期联合检验 | 处理前各期系数置信区间包含0,且系数绝对值不大 |
| 动态效应 | 效果随时间的演进模式 | 事件时间聚合、分段平均处理效应 | 处理后期系数趋势、累积效应是否显著 |
| 处理效应异质性 | 不同处理队列效应是否一致 | 分组平均处理效应、分解诊断 | 队列间系数差异、负权重占比 |
| 推断可靠性 | 显著性结论是否稳健 | 聚类稳健标准误、bootstrap | 聚类层级合理,少量处理组时结论不翻转 |
2.2 区分设计检验与稳健性检验
我会把检验任务分成两类。第一类是设计检验,回答“这个数据能不能支撑因果识别”,典型操作是事件研究图和平行趋势检验。第二类是结果稳健性检验,回答“我的结论对不同估计方法是否敏感”,典型操作是换用多种稳健估计量并对比。
这两类检验的定位完全不同。设计检验不过关,后面无论用什么估计量都缺乏说服力。稳健性检验则是用来应对“我已经确认设计合理,但不同估计方法给出的数字不同”的情况。很多实证报告只做了第一类,跑完事件研究图就直接报告TWFE结果,这在交错处理背景下是不够的,因为没有检查估计量的内部污染问题。
2.3 检验计划要前置,而不是事后补救
我踩过的最大的坑,就是在回归结果出来之后才想“我要不要再做一个平行趋势检验”。这种做法容易变成为了通过而调整检验设定。正确的做法是在数据分析前就把检验计划写清楚:基期选在哪一期、预处理期检验哪些时点、使用哪些稳健估计量、聚类层级选哪个。这样既对自己的结论负责,也方便合作方或审阅人理解你的决策逻辑。
检验计划前置不是形式主义。它逼着你在看到结果之前想清楚数据生成过程,而不是根据结果反推一个“能过”的设定。
3. 平行趋势与动态效应:事件研究法详细操作
3.1 事件时间变量的构造逻辑
事件研究法的第一步,是构造“相对事件时间”变量。公式很简单:
rel_time = year - first_treat_year其中first_treat_year是每个城市第一次接受处理的年份,从未处理的城市记为缺失。政策落地当期记为0,政策前一期记为-1,政策后一期记为1,以此类推。
这个变量构造看起来简单,但有几个坑需要注意。从未处理的城市没有first_treat_year,它们不进入事件时间维度,只在回归中提供时点和个体固定效应。数据中如果有城市在样本期之前就已经被处理,也就是左删失,这类样本的rel_time会偏小,甚至看起来永远是处理状态,需要额外处理。
3.2 事件研究回归方程与代码实现
事件研究的回归设定是:
y_it = α_i + γ_t + Σ β_k D_it^k + ε_it其中α_i是个体固定效应,γ_t是时间固定效应,D_it^k是在相对时间等于 k 时取1的虚拟变量。基期一般选-1期,也就是政策前一期。处理前所有β_k应该不显著,这是平行趋势的直观证据;处理后所有β_k描述了动态处理效应。
R语言用fixest包,写法非常简洁:
library(fixest) # rel_time 已经按前一步构造好,ref = -1 表示基期设为 -1 mod_es <- feols(y ~ i(rel_time, ref = -1) + year | id, data = df, cluster = ~ id) # 提取事件研究系数并绘图 library(broom) es_df <- tidy(mod_es) |> filter(str_detect(term, "rel_time")) # 也可以直接用 fixest 自带的绘图函数 iplot(mod_es, main = "Event Study Estimates")Stata版本:
* 构造相对事件时间 gen rel_time = year - first_treat_year if !missing(first_treat_year) * 事件研究回归,基期设为 -1 期 reghdfe y ib(-1).rel_time i.year, absorb(id) vce(cluster id) * 绘制事件研究图 coefplot, keep(rel_time*) vertical yline(0) xline(-1)注意reghdfe估计时,rel_time会被自动当作因子变量处理,产生一系列相对时间虚拟变量。如果样本时期跨度大,可以考虑把末端时期合并,避免远端样本量过少导致系数波动。R 中可以用i(rel_time, ref = -1, bin = "bin")之类的端点合并选项,Stata 中需要手动生成合并端点变量。
3.3 看图与判定:不只是看“显著不显著”
事件研究图的判定标准,很多人理解成“处理前系数不显著即可”。这个说法不够严谨。
首先要看处理前各期系数的置信区间是否包含0。其次还要看系数本身的绝对值,如果处理前系数是0.8但置信区间很宽,不显著只是意味着“信息量不足”,并不能证明平行趋势成立。反之,处理前系数全部在0附近小幅度波动,即使个别点靠近显著边界,也可以接受。这里的关键是幅度,不是单一的p值。
还需要注意多重检验问题。事件研究通常在处理前画了四五个点,每个点单独做一次置信区间检验,整体上看,出现一个点落入显著区域的概率会被放大。严谨的做法是使用同时置信带,或者对处理前所有系数做联合F检验。R中可以用car::linearHypothesis对多个预处理系数做联合检验,Stata中可以用test命令。
3.4 动态效应如何解读
处理后的系数如果逐渐增大,说明效果随时间累积;如果先升后降,说明存在短期效应但衰减;如果处理当期不显著,滞后一两期才显著,说明政策存在传播和落地过程。这些动态信息比单纯一个平均效应更有价值。
但要注意,事件研究估计结果本身在交错处理下也可能存在偏误,原理就是第4章要讨论的坏对照组问题。所以事件研究图适合作为诊断工具,而不一定要作为最终报告的唯一依据。
4. 不能绕开的坑:双向固定效应在交错处理下的偏误
4.1 负权重的来源
双向固定效应模型是政策评估里的默认选项:
y_it = α_i + γ_t + δ D_it + ε_itD_it在处理后取1,系数δ被当作平均处理效应。在单一处理时点且处理状态恒定不变的场景下,这个解释没问题。但在交错处理下,δ本质上是一堆2×2比较的加权平均,权重可能为负。
负权重怎么来的?想象一下,C类城市从未处理,是干净的对照组;A类城市处理早,B类城市处理晚。估计B类城市效应时,模型会把A类城市在前一段时期的变化当作“对照组趋势”。可是A类城市已经处理了,它的变化既包含时间趋势,也包含自身的处理效应。如果A类城市的处理效应随时间变化,那么“A类城市的变化”就不是一个干净的对照。更麻烦的是,TWFE在组合多个比较时,会给某些比较分配负权重,导致最终估计量可能偏离真实效应,甚至出现符号反转。
我用一个生活化类比解释:你想测一款新肥料对不同批次作物的效果,第一批早就浇了,第二批刚浇。你把第一批浇过肥的田当作第二批的“对照”,可第一批田自己还在持续生长,这个对照就不干净。你还不小心给某些对照的差值打了负的权重,最后算出来的“平均促长效果”可能远低于真实值。
4.2 什么信号说明TWFE可能不可靠
实操中怎么判断自己是否中招?我会检查三个危险信号。
一是处理组覆盖率随时间快速上升,比如政策在几年内从10%覆盖到70%,那么“未处理对照组”的规模越来越小,坏比较的比例上升。二是动态效应明显,事件研究图显示处理后效应持续增强或衰减,说明处理效应本身存在时间异质性,这是负权重问题产生的重要条件。三是处理组内部的处理时机差异大,第一批和最后一批间隔多年,早期处理组长期处于“已被处理”状态,不断被用作晚期队列的对照。
如果想量化负权重问题,现在有基于2×2比较的分解诊断方法,把TWFE估计结果拆成若干组比较的加权和。如果负权重占比高,基本可以判定TWFE估计量不是一个可信的总体效应。
4.3 什么时候可以继续使用TWFE
也不能一棒子打死。如果处理时点集中,比如所有处理组都在同一两年内落地,或者处理效应相对同质,不太随时间变化,TWFE的偏误通常比较有限。
我更推荐的做法是:把TWFE当作参照系,而不是唯一结论。先跑事件研究和TWFE,再跑稳健估计量,如果几种方法结果方向一致、幅度相近,直接报告TWFE并说明稳健性检查通过;如果差异明显,以稳健估计量为准,并解释差异来源。
5. 稳健估计量实操指南:三种路径对照
5.1 组别-时期平均处理效应估计量
这是目前最主流的思路。基本逻辑是:先按首次处理时间把处理组分成不同队列,对每一个队列g在每一个时期t估计一个平均处理效应ATT(g, t),然后再按需要聚合成总体效应或事件时间动态效应。
关键细节在对照组的选择。可以选择从未处理的城市作为对照组,也可以选择“尚未处理”的城市作为对照组。后者更常用,因为样本量更大。R语言中可以用did包:
library(did) # gname 是首次处理年份,未处理组编码为0 att_gt_res <- att_gt( yname = "y", tname = "year", idname = "id", gname = "first_treat_year", data = df, control_group = "notyettreated", base_period = "universal", panel = TRUE ) summary(att_gt_res) # 聚合成事件时间动态效应 dynamic_res <- aggte(att_gt_res, type = "dynamic", min_e = -4, max_e = 5) summary(dynamic_res) # 聚合成简单总体平均处理效应 overall_res <- aggte(att_gt_res, type = "simple") summary(overall_res)Stata中对应命令是csdid:
ssc install csdid csdid y, ivar(id) time(year) gvar(first_treat_year) method(reg) * 查看总体效应、动态效应、队列效应 estat simple estat event estat group使用csdid前注意变量的编码规则,未处理组的first_treat_year通常记为0或缺失,不同命令要求不一致,需要仔细看帮助文档。
5.2 插补型估计量
插补型估计量的想法更直接:用从未处理组样本估计一个结果方程,预测处理组的“反事实结果”,然后用实际结果减去反事实结果,得到个体处理效应,再平均。
这种方法的优点是计算透明、原理清晰。R语言用didimputation包:
library(didimputation) est <- did_imputation( data = df, yname = "y", idname = "id", tname = "year", gname = "first_treat_year", first_stage = ~ 0, horizon = NULL, pretrends = NULL ) summary(est)Stata中可以用did_imputation命令:
ssc install did_imputation did_imputation y id year first_treat_year, hor(0(1)5) pretrend(1(1)5)插补法的风险在于第一阶段模型的设定。如果结果变量受很多时变协变量影响,而你漏掉了关键变量,反事实预测就会出现偏差。所以在使用插补法时,first_stage公式里要谨慎加入协变量,不能为了省事全都不加。
5.3 交互加权型估计量
交互加权型估计量保留了事件研究的外形,但调整了处理组和对照组的权重,使得每个队列只用“干净”的未处理样本做对照,避免早期处理组污染晚期队列。
R中可以用fixest包配合sun_ab()函数:
mod_sa <- feols(y ~ sun_ab(rel_time, first_treat_year) + year | id, data = df, cluster = ~ id) iplot(mod_sa, main = "Sun-Abraham Event Study")Stata中可以用eventstudyinteract命令:
ssc install eventstudyinteract eventstudyinteract y, absorb(id year) cohort(first_treat_year) event(rel_time) vce(cluster id)交互加权型估计量对数据要求稍高,如果某些队列在某段事件时间上没有样本,权重计算可能出现极端值,需要检查每个队列-事件时间单元的样本量。
5.4 三种方法怎么选、结果如何对照
三种方法不是互相排斥的,我会在报告里至少跑两种交叉验证。选择逻辑如下:
| 方法类型 | 核心思路 | 适用场景 | 主要风险 |
|---|---|---|---|
| 组别-时期平均ATT | 分队列分时期估计再聚合 | 队列多、动态效应明显 | 结果解释依赖聚合方式 |
| 插补型估计量 | 用未处理组预测反事实 | 处理组占比不太大、协变量完整 | 第一阶段模型设定偏误 |
| 交互加权型估计量 | 队列×事件时间饱和估计 | 需要保留事件研究图形态 | 队列样本稀疏时不稳定 |
如果三种方法给出的总体效应高度一致,说明结论对方法选择不敏感,这是最理想的情况。如果差异明显,不要直接平均了事,要认真查差异来源:对照组定义不同、样本范围不同、聚合权重不同,都可能导致差异。把这些来源在报告里坦诚写清楚,比强行得出一个数字更有价值。
6. 常见问题与排查技巧实录
6.1 事件研究图中处理前系数“显著”怎么办
这是最让研究者头疼的情况。我的排查顺序固定是这样:先做分解诊断,看是不是负权重造成的人为假趋势;再检查基期选择,换一个基期看看结论是否翻转;然后考虑预期效应,如果政策在正式落地前已经被公众知晓,处理组可能在政策前就开始调整行为,这时候需要把政策宣布时点纳入事件时间;最后做安慰剂检验,随机打乱处理时点,看事件研究图在随机分配下是否还会出现“显著”的先期趋势。
如果以上都没有问题,那就要承认平行趋势假设存疑。此时能做的是找额外的预处理期数据、找更细颗粒度的数据,或者换用合成控制等更灵活的方法。
6.2 多个稳健估计量结果不一致,该信谁
这个问题几乎每次评审都会被问。我的原则是:先比方向和显著性,再比点估计的大小。如果所有方法方向一致且都在同一量级,就报告一组范围值。如果方向都不一致,说明数据里可能存在强异质性或对照组选择敏感性问题。
这时回到原始数据检查:是不是某个队列贡献了过多权重?是不是从未处理组样本量太小?是不是协变量分布在组间严重不平衡?我发现很多时候“结果不一致”是因为不同方法对“对照”的定义不同,有的用从未处理组,有的用尚未处理组,两组样本构成差异很大。
6.3 聚类层级怎么选
多时间点DID的推断问题容易被忽略。个体层面有重复观测,处理状态在城市层面变化,所以聚类至少应该到城市层面。如果只聚类到个体,会严重低估标准误。
处理组数量较少时,常规聚类稳健标准误可能偏大或偏小,更稳妥的做法是使用野聚类bootstrap。一个简单的经验法则是:如果处理组数量小于30,建议做wild cluster bootstrap检验,并把它作为主结果推断依据。
6.4 没有“从未处理”组怎么办
不是所有数据都有从未处理组。比如一项政策最终覆盖了所有城市,只是时间有先后。这时可以把最后一批处理的城市当作“尚未处理”的对照组,利用它们处理之前的时间段做识别。
这类设计的解释范围要收窄:估计出来的效应是“相对于最后处理组的相对效应”,而不是“相对于完全不受政策影响的绝对效应”。如果你的政策理论上对所有城市都有长期影响,那么最后处理组自身也受到了影响,效应会存在衰减偏误,解读时需要谨慎。
6.5 时期太少或样本量不足怎么处理
多时间点DID的很多方法依赖事件时间的丰富变化。如果样本期只有五年,第一批和最后一批处理间隔又近,事件时间能覆盖的点很少,稳健估计量的标准误会很大。
我的经验是:把事件时间端点做合并,不要追求画出完整的事件研究图;预处理期的检验集中在政策前一期或前两期;更多依赖插补法这类对数据要求稍低的方法;在报告里明确标注小样本警示,不要过度解读不显著的结果。
另外提醒一个容易被忽略的问题:如果面板数据不平衡,有些城市在样本期内消失,事件研究图的端点系数可能只由留存样本估计,存在选择性进出样本的偏误。建议先做一个平衡面板版本的敏感性分析,看看结论是否一致。
我个人的体会是,多时间点DID的统计检验没有一劳永逸的程序,每一步取舍都取决于你对数据生成过程的理解。跑代码只占三成,剩下七成是在想清楚“处理状态”是怎么进入样本的、对照组的反事实究竟由谁承担。如果你正在做类似评估项目,建议从事件研究和分解诊断入手把病变查清,再选估计量,这个流程会帮你少踩很多坑。