做增长分析、策略评估或者运营复盘的同学大概率都被同一个问题卡过一个活动上线了数据涨了但这个涨到底是活动带来的还是本来就会涨我以前做用户留存分析时最怕的就是老板问这个数你自己信吗。后来我逼着自己把因果推断这一套东西系统学了一遍其中双重差分Difference-in-Differences简称DID是我用得最多、也最容易上手的一把刀。这一篇是因果推断笔记系列的第四篇专门把DID的理论、假设和实践揉在一起讲透顺便聊一个现在讨论度很高的方向——因果推断CATEConditional Average Treatment Effect条件平均处理效应英文里常写作CATE。不管你是刚接触因果推断的新手还是已经跑过几次DID但总觉得结果不稳的老手这篇应该都能捞到点东西。1. 双重差分到底在解决什么问题1.1 从一次真实的评估困境说起先讲个我自己的例子。早些年我负责过一个新用户引导流程的改版把原本五步的注册流程砍成了三步。上线两周后新用户次留从38%涨到了42%团队一片欢腾。但冷静下来想想这两周正好赶上了一个行业流量高峰大盘整体都在涨那这4个点的提升里有多少是改版的功劳这就是评估里的经典难题我们永远只能看到做了这件事的世界看不到同一批人、同一时间、没做这件事的世界。后者在因果推断里叫反事实counterfactual。理想情况下做个A/B实验随机分流两组人除了是否接受处理之外完全一样那差异就是干净的因果效应。但现实里大量场景做不了实验比如全量上线的功能、区域性的投放、只有一批人能参与的政策或活动。这时候DID就派上用场了。它的思路特别朴素却极其巧妙找一组没被处理的人作为对照看两组人在处理前后的变化然后比较这两组变化量的差异。一个差分是处理组的前后变化另一个差分是对照组的前后变化两个差分再相减得到的就叫双重差分估计量。为什么这么设计有效因为处理组前后变化里混着两块东西——真实处理效应以及那些和事件无关的时间趋势季节性、大盘波动等。而对照组前后变化恰好就是那个时间趋势的一个估计。一减时间趋势被抵消剩下的就更接近我们想要的因果效应。这个减两次的动作就是DID名字的由来。1.2 DID的核心识别逻辑两次差分抵消了什么把上面的直觉写成公式就很清楚了。假设有处理组T和对照组C时期分为处理前和处理后两个时间点我们用简单的均值表达DID估计量 (Y_T,后 - Y_T,前) - (Y_C,后 - Y_C,前)展开成四项就是处理组后 - 处理组前 - 对照组后 对照组前。在回归模型里通常写成这样# 基准DID回归模型 # Y beta0 beta1 * Treat beta2 * Post beta3 * (Treat * Post) error # beta3 就是我们最关心的DID估计量这里的Treat是分组虚拟变量处理组1对照组0Post是时间虚拟变量处理后1处理前0而两者的交互项Treat * Post的系数beta3就是双重差分的效应估计。beta1吸收了处理组和对照组之间不随时间变化的固有差异beta2吸收了全样本共同的时点变化。为什么这么建模因为DID默认了一个加减法的世界观——两组之间的差异是稳定加性的。这个假设看似简单但它其实就是后面要重点讲的平行趋势假设的回归版本。理解到这一层你在跑回归时就不会只盯着beta3的显著性而会去追问这个加性结构成立吗提示很多新手会把Treat和Post的主效应忘了加只放交互项。这在两期两组的简单场景里还能凑合但在多期面板里会严重偏误务必把主效应都带上。1.3 从ATE到CATE平均效应之外的故事DID估出来的是一个平均处理效应ATE意义上的数。但实际业务里平均往往是骗人的。一个活动对整体提升了2%可能是对老用户提升了10%对新用户却是-8%两个一平均就成了2%。如果你拿着一句整体提升2%去做决策很容易翻车。这就是因果推断CATE要解决的问题。CATE是Conditional Average Treatment Effect的缩写英文里常写成Conditional Average Treatment Effect翻译过来是条件平均处理效应意思是在给定某些特征X比如用户年龄、历史消费、活跃度的条件下处理效应的期望值。它回答的是这个活动对哪一类人最有效而不是这个活动平均有没有效。ATE是CATE在所有人群上的加权平均CATE则是把人群切开看。可以这么理解ATE告诉你要不要做这件事CATE告诉你对谁做这件事、做到多细。在增长、推荐、定价这些场景里CATE往往是真正能指导落地的那个数。这也是为什么这两年因果推断和异质性效应估计这么火——大家已经不满足于有没有用而是想知道对谁有用、值不值。2. 三大核心假设每一条都要能自证DID的估计量能不能信全看假设。我把最关键的三条拎出来配上我踩过的坑逐条讲。2.1 平行趋势假设DID的命门平行趋势假设说人话就是如果处理组没被处理它的结果变量应该和对照组走出平行的轨迹。注意不是要求两条线一样高而是要求它们的变化节奏一致。处理组本来可以比对照组高但得齐步走。这是DID最核心、也最脆弱的假设。为什么它是命门因为处理效应恰恰就是靠偏离平行来识别的——处理组在接受处理后如果偏离了原本应该和对照组平行的轨迹那个偏离量就被我们认定为效应。但如果这两条线本来就不平行那偏离量里就混进了本来的趋势差异估计量就脏了。我见过最常见的翻车现场选对照组时图省事随手拿了一个大盘或者另一个城市结果处理组本来就在一个更快增长的赛道上处理前两条线斜率就不一样,处理后再一减,效应被严重高估。所以选对照组这件事绝不是随便找个没被处理的就行得在业务上说得通——为什么这两组在没干预时应该同步演变。怎么验证最直观的是画时间趋势图看处理前的多期里两组是否保持平行。更规范的做法是事件研究法event study把每一期的相对效应都估出来# 事件研究法的示意伪代码 # 生成相对时间虚拟变量 event_time 相对处理时点的期数 # 基准期通常设为处理前一期-1作为参照 # Y ~ 各期event_time虚拟变量 个体固定效应 时间固定效应 # 看处理前各期系数是否接近0且不显著如果处理前的系数一串都贴着0附近且不显著平行动势就比较可信如果处理前就有一堆显著系数说明两组本来就在分叉这时候要么换对照组要么上更高级的估计方法。注意事件研究法的基期选择不是随便的。设成-1是为了让处理前一期做参照这样处理前系数能直接读出偏离基期的程度。基期选得不好会让人误判趋势。我个人习惯至少展示处理前4到5期期数太少看不出趋势。2.2 个体处理稳定性假设SUTVASUTVA全称Stable Unit Treatment Value Assumption翻译成个体处理稳定性假设。它包含两层意思第一一个人的结果只受自己是否被处理影响不受别人是否被处理影响第二处理没有多种版本同一个处理对所有人都是一个东西。第一层涉及溢出效应spillover。举个例子如果处理组是某个城市的用户对照组是邻近城市的用户而用户之间会互动那么处理组的活动可能通过社交关系渗到对照组污染对照。这时候DID估出来的效应会被稀释——因为对照也被半处理了。第二层涉及处理版本一致性。比如你评估发优惠券的效果A组收到的是满100减20B组收到的是满50减10那这其实不是同一个处理混在一起估计意义就打了折扣。我在做区域类评估时就吃过溢出的亏。当时一个活动在某几个城市试点对照组选了地理上相邻的城市结果发现两边的用户重叠度很高活动信息通过社群传播到了对照城市效应估计被压得偏低。后来把对照组换成了距离更远、结构相似的城市结果才稳定下来。判断有没有溢出一个土办法是看地理或社交网络上处理组和对照组的接触距离。业务上离得越近、互动越密溢出风险越高反之可以放宽。真拿不准的时候可以做一个距离衰减的稳健性检验看效应是否随对照与处理的距离变化而变化。2.3 外生性与无预期效应这一条稍微绕一点但很关键。DID隐含假设处理的发生时点相对于个体来说不可预期或者说是外生的。如果个体能提前预期到处理、并据此调整行为那处理前的那段基线就被污染了平行趋势也可能不成立。预期效应anticipation effect是典型的坑。举个例子如果大家都知道下个月要发补贴可能这个月就先按兵不动消费往后挪导致处理前最后一两期的数据就已经动了。这时候事件研究法会看到处理前一期系数就开始有苗头处理效应的识别基期就不干净了。另一个相关问题是选择偏差。处理组和对照组是不是自选进去的比如工具/功能是用户自己点开通的那点开通的人本来就更有意愿这种自选会直接破坏可比性。DID虽然通过双重差分消掉了一部分不随时间变化的差异但如果是随时间变化的选择性——比如高活跃用户在特定时期更倾向开通——那DID也救不了。处理这类问题的思路有三条一是尽量找自然形成的、外生的断点作为处理时点二是在模型里加入可能在处理前就变化的协变量做条件平行趋势三是用一些准实验设计比如断点回归、合成控制来交叉验证。我自己做评估时凡是处理组是用户主动参与的都会格外警惕往往还要补一套倾向得分匹配PSM来构造更可比的对照。3. 从公式到落地DID的实操全过程理论说完了接下来是能直接抄作业的部分。我按数据准备、基准估计、平行趋势检验、稳健性四步走,把每一步的关键操作和坑都摊开。3.1 数据结构与变量编码DID最舒服的数据形态是面板数据panel data也就是同一批个体在多个时间点上的观测。核心字段通常是四个个体ID、时间、处理分组、结果变量。我整理一份标准的变量表方便对照字段名含义取值示例user_id个体唯一标识10001period时间期数1, 2, 3, 4treat是否处理组0 或 1post是否处理后0 或 1y结果变量留存率、GMV等编码时有两个细节要注意。第一post的划分必须以处理时点为准处理前为0、处理及之后为1一旦处理时点在个体间不同交错处理就不能只用一个post了得用相对时间。第二个体ID务必是稳定不变的如果用户会注册多个账号或者ID会变面板结构就会断裂固定效应也就失去意义。还有一个容易被忽略的点平衡面板 vs 非平衡面板。如果个体在不同期有进有出面板就不平衡。非平衡面板本身不致命但如果进出与处理相关比如处理导致用户流失就会引入选择性偏误。我当时评估一个改动时发现处理组流失率明显更高后来才意识到能留存到处理后的样本本身就是被筛选过的这种情况下要谨慎解释结果。3.2 基准DID的回归实现到了动手环节。两期两组的场景最简单可以手算差分但现实多是多期面板推荐直接用固定效应回归。用Python的话linearmodels或者statsmodels都能做。import pandas as pd import statsmodels.formula.api as smf # 假设 df 有四列user_id, period, treat, post, y # 加入 treat 与 post 的交互项以及个体固定效应 model smf.ols( y ~ treat * post C(user_id) C(period), datadf ).fit(cov_typecluster, cov_kwds{groups: df[user_id]}) print(model.summary()) # 重点看 treat:post 这一项的系数就是DID估计量这里的C(user_id)和C(period)就是个体固定效应和时间固定效应。有了固定效应其实treat和post的主效应会被部分吸收但把它们带上更稳妥方便阅读系数含义。关于标准误这是很多人忽略但极其重要的细节。面板数据里同一个体多期观测通常是相关的普通标准误会低估不确定性、把不显著的估成显著。所以一定要做聚类稳健标准误cluster-robust standard errors聚类层级一般选个体。如果处理是在群体层面分配的比如按城市、按门店那聚类到那个层面更合适。提示聚类层级选错了显著性结果会差很多。一个粗略原则是处理分配在哪个层级就聚类在哪个层级。如果处理是城市级别的而你只聚类到个人标准误会偏小容易得出过于乐观的结论。3.3 平行趋势检验的几种做法基准结果出来后第一件事不是欢呼而是证伪——拼命找证据说明平行趋势不成立。这就是我反复强调的态度DID的可信度来自你对假设的诚实检验。做法一是画趋势图。把处理组和对照组的结果变量均值按期数画出来处理前如果两条线基本平行就初步过关。这个图一定要画它是最直观的沟通工具汇报时也最有说服力。做法二是事件研究法前面提过。把相对时间的虚拟变量放进去看处理前系数是否接近0。这里有个细节如果处理前系数整体偏正或偏负但都不显著可能是样本量不够如果显著偏离那就要警惕了。# 事件研究法思路 # 构造 rel_time period - treat_period对处理组 # 对照组设定一个虚拟的基准期 # Y ~ C(rel_time) * treat 固定效应 # 观察 rel_time 0 的交互项系数做法三是安慰剂检验。把处理时点人为提前到真正处理之前用假处理跑一遍DID。如果假处理也能跑出显著效应那说明你的模型其实在捕捉某种趋势而不是真实效应真结果也就不可信了。我特别爱做这个检验因为它能狠狠打脸那些看起来很美的结果。3.4 稳健性检验清单一个能拿得出手的DID分析通常要过下面这一串检验。我把它做成清单方便你逐条勾检验项目的做法平行趋势检验验证核心假设事件研究法、趋势图安慰剂检验排除虚假效应提前处理时点、随机分配处理更换对照组检验结果对对照选择敏感度换几组不同对照分别估更换时间窗检验时间敏感性缩短或延长考察期加入协变量检验条件平行趋势加入处理前协变量更换标准误聚类层级检验推断稳健性个体/群体层面分别聚类其中更换对照组我做得最多。因为对照组的选择往往带有主观性如果换个对照结果就翻天覆地那这个结论就不牢靠。理想状态是换几组合理的对照系数方向和量级都比较稳定你才有底气说这个效应是真的。4. 进阶从DID到CATE异质性处理效应怎么估4.1 交叠DID带来的一堆麻烦现实中的处理时点常常是交错的有的城市上半年试点有的下半年才推。这种叫交错DIDstaggered DID也叫交叠DID。它看起来只是多期处理时点不同但实际会引发一个大问题用传统双向固定效应TWFE估计结果可能有偏甚至出现系数符号和真实效应相反的怪象。原因在于当处理时点交错时已经处理的组会充当后面处理组的对照而那些已经处理过的组此时已经被处理污染了拿它们当对照就破坏了可比性。学界提出了负权重的概念来描述这种偏误来源。说白了就是有些比较在数学上被赋予了负的权重导致估计量偏离真实效应。这几年针对交叠DID出现了不少改进方法比如基于分组的估计、堆叠DIDstacked DID、以及一些专门处理异质性处理效应的估计框架。它们各自适用不同场景堆叠DID为每个处理组各自构造一个干净的对照池再汇总。操作直观适合处理时点数量不多的情况。分组-时点估计对每个处理组×处理时点单元分别估计再加权汇总能避免负权重。基于个体处理效应的框架直接估出每个处理情形下的效应再聚合成需要的整体效应。我个人的经验是处理时点如果是两三个、样本量够用堆叠DID最省心如果处理时点很多、又想顺便看异质性那上更系统的框架更合适。选哪种关键看你想要的是一个整体效应还是一堆条件效应。4.2 异质性效应的几条估计路径聊完交叠DID就绕不开CATE。怎么估条件平均处理效应主流有两大类。第一类是元学习器meta-learner思路把任意机器学习模型包一层用不同方式组合来估处理效应。常见的有S-learner把所有特征和处理变量一起喂给一个模型预测时改动处理变量看输出差异。简单但处理变量容易被大量特征淹没模型可能低估它的作用。T-learner处理组和对照组各训一个模型用两个模型的预测差作为效应。直观但对小样本的处理组不友好。X-learner针对两组样本量不均衡做的改进先估各自的效应再交叉补全实践里对付不平衡挺有用。DR-learner结合倾向得分和结果回归doubly robust抗模型误设定能力强一些。第二类是因果森林causal forest这类专门为异质性效应设计的树模型。它通过大量树的平均来估每个样本的条件效应并且能给出置信区间。因果森林的一个好处是自带诚实估计机制——用一部分数据决定分裂、另一部分估计叶子节点的效应减少过拟合。用Python的话econml和causalml是常用库里面封装了上述多数方法。跑之前一定要把倾向得分估出来当协变量或权重尤其是处理分配不随机的时候# 伪代码示意元学习器思路 from econml.metalearners import XLearner from sklearn.ensemble import GradientBoostingRegressor xl XLearner( modelsGradientBoostingRegressor(), propensity_modelGradientBoostingRegressor() ) xl.fit(Y, T, XX) # Y结果T处理X异质性特征 cate xl.effect(X_new) # 输出每个样本的条件效应4.3 结果怎么读、怎么用CATE估出来后别急着做一个花哨的热力图就完事。我一般看三件事。第一分布。CATE是每个个体的一个数先看它的分布——均值大概多少有没有明显的正负两极。如果均值接近0但方差很大说明有人受益有人受损整体上没用但局部有大机会。第二特征重要性。哪些特征最能解释CATE的差异因果森林能给出特征重要性排序这直接告诉你哪一类人效应最强。比如你发现新用户这个特征很重要那就意味着活动对新老用户的效应差异显著下次可以针对性调整。第三可操作性。这是最容易翻车的地方。有些高CATE的人群可能本身就很难触达、或者规模很小即使效应大整体收益也有限。所以更实用的做法是把CATE排序后算增益曲线uplift curve——看针对前10%、前20%的人做定向投放能多拿到多少增量。这才是真正能拿去和业务方对话的东西。注意CATE的估计对特征X的选择很敏感而且容易过拟合。至少要做一个训练集外的验证看估出来的效应在新样本上是否稳定。我见过不少把CATE估得非常漂亮、一上真就崩的案例。5. 常见踩坑与排查速查表5.1 平行趋势不通过怎么办这是最高频的问题。发现处理前两组就不平行别慌先搞清楚原因再对症下药。如果是因为对照组选得差那就换对照组。找那些在处理前走势更接近的群体或者用倾向得分匹配先构造一对更像的样本。如果是因为季节性或大盘波动可以加入时间固定效应或者用更细的时间粒度来控制。如果发现处理前一两期就开始偏离趋势有斜率变化那很可能是预期效应。这时候要么把这几期从基期里剔除要么把处理时点认定为偏移后的时点。如果实在找不到平行趋势那要坦诚承认并考虑换设计——比如合成控制法用多个未处理单元加权合成一个假的对照它比普通DID在对照组构造上更讲究适合单个处理单元比如一个城市的场景。不管怎样不要为了让结果好看去手动裁剪样本。这是最要命的学术不端式操作短期看是调平了长期一定出问题。我宁愿要一个坦白显示假设不成立、效应存疑的结论也不要一个粉饰过的漂亮数字。5.2 标准误与聚类的坑我见过太多系数显著其实是标准误算错的产物。几个高频错误没做聚类直接用了默认的OLS标准误显著性虚高。聚类层级过细或过粗聚类到个体通常没问题但如果处理是群体分配的聚类到个体就偏小了反过来聚类层级太粗检验会过于保守。聚类数目太少如果聚类单元只有个位数比如只有5个城市常规聚类标准误本身就不靠谱得用专门的有限聚类校正方法。用普通日记账做多期面板多期面板里自相关严重普通标准误会失真最好用能处理序列相关的方法。一个实用技巧是同时报告两种标准误下的结果如果结论一致可信度更高如果在某种聚类下就不显著了那下结论时就得留一手。5.3 数据层面的常见陷阱数据本身的质量比模型选择重要得多。我把数据层面的坑单独拎出来。第一处理时点定义模糊。同一次活动有的用户当天就触达了有的三天后才看到那处理时点到底是哪一天定义不清post变量就是错的后面全白做。我的做法是明确以用户首次被触达的时间为准并做敏感性检验。第二结果变量被处理本身影响。比如评估某个功能对留存的影响但这个功能会改变用户的行为习惯进而影响你对活跃的定义。这时结果变量可能同时被处理直接和间接影响解释要小心。第三样本损耗attrition。前面提过处理组和对照组的流失率如果不同留下来的样本就是被筛选的会引入偏误。至少要检查两组的损耗率差异大就要在模型里做处理比如逆概率加权。第四测量口径变化。评估期内如果后台口径变了、埋点改了数据口径会断裂这种坑最隐蔽往往跑半天结果都不对劲才发现是口径问题。做长周期评估前一定先对一遍数据字典。下面这个速查表我平时放在手边遇到问题直接对照现象可能原因排查方向系数方向与预期相反负权重、对照组污染换交叠DID方法、检查对照处理前就不平行对照选得差、预期效应事件研究法、换对照、剔基期显著性异常高标准误未聚类加聚类稳健标准误结果换对照就变结论不稳健多组对照交叉验证效应异常大或小溢出、样本损耗检查接触距离、流失率假处理也有显著效应存在共同趋势加时间趋势、换设计6. 一些我自己的实操心得写到这儿理论、假设、实操、进阶都过了一遍。最后分享几点零散但真心有用的体会。关于对照组选择我的原则是宁可费劲不图省事。一个评估里最花时间的往往不是跑模型而是论证这个对照组合不合理。业务逻辑上说不通的对照模型再花哨也是空中楼阁。我习惯先画图、后跑回归图过不了眼后面都别谈。关于CATE落地我现在的态度是先证伪再用。拿到一组异质性效应先问三个问题这批高效应人群是不是样本量太小导致的偶然这个特征是不是和结果变量本身高度相关可能是伪因果这个策略在业务上可行吗三个都过了才考虑拿去做定向。过早相信CATE比不做CATE还危险因为它会给你一种我懂了的错觉。关于汇报我强烈建议把平行趋势图、安慰剂检验、多组对照结果一起呈现。别只报一个漂亮的系数。业务方可能看不懂事件研究法但他们看得懂处理前两条线是不是平行这张图。把不确定性摊在桌面上反而更能赢得信任。关于工具Python生态里statsmodels、linearmodels做基准DID够用了进阶的econml、causalml做CATE配合scikit-learn的树模型效果不错。R里的相关包在异质性估计上也很成熟。选哪个不重要重要的是别把工具当黑箱——搞不清楚它内部的识别假设再先进的库也帮不了你。因果推断这件事说到底是一个对抗自我欺骗的过程。DID看起来很朴素但正因为朴素它的每一个假设都赤裸裸地摆在台面上逼你去直面那些不完美。我做了这么多年越来越觉得一个诚实的、假设检验做足的DID分析远胜过一个堆满高级方法却回避核心假设的花架子。这也是我把这套笔记一路写下来的原因——工具会更新但对假设的敬畏得一直在。
