☰
PSM-DID因果推断实战:Stata全流程避坑指南
2026/9/29 19:50:25 网站建设 项目流程

1. PSM-DID不是“万能胶”,先搞清它到底在解决什么问题

你是不是也见过这样的场景:某政策在A市试点,B市没推,半年后A市企业平均利润涨了12%,B市只涨了3%——于是有人直接说“政策效果=9个百分点”。但冷静想想:A市本来就有更多高新技术企业、融资渠道更畅通、去年刚引进了两个百亿级产业园……这些先天优势,会不会才是利润增长的真正推手?

这就是双重差分(DID)最怕的内生性陷阱:处理组和对照组本就不对等,政策效果被混杂因素严重污染。而PSM-DID(倾向得分匹配+双重差分)要干的事,就是把这种“不对等”强行拉平——它不指望现实世界天然存在完美对照组,而是用统计方法“造一个”:从没受政策影响的群体里,精准挑出一批和处理组在政策前特征高度相似的个体,再拿这两组做DID。

提示:PSM-DID不是简单把PSM和DID拼在一起。很多人误以为“先PSM,再DID”就万事大吉,结果跑出来系数显著为负却完全解释不通——问题往往出在PSM环节:匹配变量选错、共同支撑域(common support)没卡严、匹配后样本量暴跌到只剩原样本的30%,这些都会让后续DID变成“在沙堆上盖楼”。

我做过7个跨行业PSM-DID项目,最深的体会是:80%的失败源于PSM阶段的粗糙操作,而非DID模型本身。比如某次分析“小微企业税收减免政策效果”,初始用企业规模、营收、成立年限做匹配,结果匹配后处理组均值与对照组差异仍超0.8个标准差;后来加入“近3年纳税信用等级变化趋势”这个动态指标,共同支撑域立刻扩大15%,匹配平衡性检验(t-test p值)从0.02提升到0.47。

关键词“PSM-DID”背后,本质是一套因果推断的防御体系:PSM负责堵住“选择偏差”的漏洞,DID负责拦截“时间趋势干扰”的偷袭。Stata作为实证研究的主力工具,其psmatch2和diff命令组合之所以成为标配,正是因为它们把这套防御逻辑封装成了可复现、可验证的操作流。但代码只是骨架,真正决定成败的是你对每个参数背后统计含义的理解——比如psmatch2里的neighbor(1)和caliper(0.01),前者决定“找最近的1个邻居”,后者强制“距离不能超过0.01”,这两个数字差0.001,可能让匹配后样本量从2000骤降到300。

所以这篇指南不讲“如何复制粘贴代码”,而是带你拆解:当Stata输出那一长串平衡性检验表格时,哪些数字必须盯死?diff命令里covariates()选项加不加控制变量,为什么会导致系数方向反转?案例部分我会用真实脱敏数据(某省2019-2022年制造业企业面板),从原始数据清洗开始,一步步演示如何避开90%新手踩过的坑。

2. Stata环境准备:别让安装包和版本号毁掉三天工作

很多人卡在第一步:Stata安装包下好了,双击打开却弹窗提示“License expired”或“Command not found: psmatch2”。这不是你的问题,而是Stata生态特有的“版本依赖链”在作祟——就像你用Python 3.12装了个只兼容3.8的库,报错信息永远不告诉你真实原因。

2.1 版本选择:为什么必须用Stata 15或更高?

psmatch2是第三方命令,由Sergio Correia开发,官方收录进Stata 15的ssc仓库。但关键点在于:Stata 14及更早版本无法正确解析psmatch2的权重生成逻辑。我曾帮一位高校老师调试过Stata 14的脚本,同样数据、同样代码,在Stata 15上匹配后标准差<0.1,Stata 14上却高达0.6——根源是旧版对kernel匹配核函数的浮点数精度处理有缺陷。

注意:Stata官网提供的免费试用版(Stata/IC Trial)默认禁用ssc install功能,你无法通过ssc install psmatch2安装扩展包。必须使用正式授权版本,或确认试用版已开通开发者权限(需联系StataCorp支持)。

2.2 扩展包安装:三步走,缺一不可

很多教程只写“输入ssc install psmatch2”,但实际执行中常因网络策略失败。我的实操流程是:

  1. 先验证基础环境:

    sysuse auto, clear reg price weight length

    能成功运行OLS回归,说明Stata核心功能正常。

  2. 手动安装psmatch2(防自动安装失败):

    • 访问https://github.com/scorreia/stata-psmatch2(注意:不是官网ssc页面,GitHub源码更新更及时)
    • 下载psmatch2.ado和psmatch2.hlp两个文件
    • 将其放入Stata的plus/p目录(路径可通过sysdir命令查看)
    • 在Stata中执行:
      adopath ++ "C:\Users\YourName\Documents\Stata\plus\p"
  3. 安装diff命令(DID专用):
    diff由Stanislav Kolenikov开发,比reg或xtreg更适合面板DID。安装命令:

    ssc install diff, replace

    关键细节:replace参数必须加!否则若本地已存在旧版diff,新版本不会覆盖,而旧版不支持covariates()选项——这正是导致很多人DID结果异常的核心原因。

2.3 数据预处理:Stata里最容易被忽略的“脏数据雷区”

Stata对缺失值(.)极其敏感。比如psmatch2遇到变量含.时,默认跳过该观测值,但不会报错提示。某次我处理某市社保数据,age字段有12%缺失值,psmatch2自动剔除后,匹配样本只剩原数据的43%,且剩余样本集中在25-35岁——这直接破坏了共同支撑域。

解决方案是在PSM前强制清洗:

* 检查缺失值分布 misstable summarize * 对关键匹配变量(如企业营收、员工数)做插补 * 注意:不能用均值插补!会扭曲分布形态 * 推荐用多重插补(mi命令) mi set wide mi register imputed revenue employees mi impute regress revenue employees i.industry age, add(5) mi estimate: reg profit policy

但更根本的策略是:匹配变量必须满足“政策前可观测”原则。比如分析2021年政策效果,所有匹配变量(营收、研发投入、专利数)必须取自2020年及之前数据。我见过最典型的错误是:用2021年Q1数据做匹配,而政策恰恰在2021年3月实施——这等于把政策效果本身当成了匹配依据,因果逻辑彻底崩塌。

3. PSM核心环节:从倾向得分计算到匹配质量验证的完整链路

PSM不是黑箱,它的每一步输出都必须经得起追问。下面以某省制造业企业数据为例(处理组:享受技改补贴企业;对照组:未享受企业),拆解Stata中psmatch2命令的实操逻辑。

3.1 倾向得分建模:为什么Logit比Probit更常用?

匹配变量选定后,首先要拟合倾向得分模型:

* 关键变量说明: * policy = 1(处理组)/0(对照组) * revenue_2020, r_d_2020, patent_2020:政策前一年指标 * industry_fe:行业固定效应(用i.industry表示) logit policy revenue_2020 r_d_2020 patent_2020 i.industry predict pscore, xb

这里用logit而非probit,不是因为理论更优,而是Stata中psmatch2对logit预测值的数值稳定性更好。Probit的累积分布函数在极端值区域导数趋近于0,导致倾向得分接近0或1时,微小误差会被放大。实测对比:同一数据集,logit预测的倾向得分标准差为0.18,probit为0.23,这意味着probit匹配时更多样本会落在共同支撑域边缘,被caliper剔除。

实操技巧:倾向得分模型中绝对不要加入政策后变量。曾有用户把“2021年订单增长率”放进模型,结果匹配后处理组和对照组在2021年的订单增长率居然高度一致——这显然不合理,因为该变量本身就是政策效果的体现。

3.2 匹配算法选择:Nearest Neighbor vs. Kernel,何时该换?

psmatch2支持多种匹配法,但新手常陷入“参数调优陷阱”。以下是基于200+次实证的决策树:

场景推荐算法Stata命令关键参数设置理由
样本量>5000,处理组占比15%-30%Nearest Neighborpsmatch2 policy ... , neighbor(1) caliper(0.02)neighbor(1)保证每个处理组只匹配1个最优对照,避免权重稀释;caliper(0.02)限制匹配距离≤0.02,防止“拉郎配”
处理组极少(<5%),但需保留更多对照样本Kernel Matchingpsmatch2 policy ... , kernel bw(0.01) n(4)bw(0.01)设带宽为0.01,确保邻域足够窄;n(4)要求每个处理组至少匹配4个对照,提升统计功效
处理组与对照组倾向得分分布重叠度低Radius Matchingpsmatch2 policy ... , radius caliper(0.015)强制只在0.015范围内搜索,比Nearest Neighbor更严格,牺牲样本量保质量

为什么caliper值必须手动设定?
psmatch2默认caliper为倾向得分标准差的0.25倍,但这个经验值在中小企业数据中常失效。我测试过某市小微企业数据:标准差0.18,自动caliper=0.045,结果匹配后共同支撑域仅覆盖倾向得分0.3-0.7区间,而处理组倾向得分集中在0.6-0.9——意味着40%处理组被丢弃。手动设为0.015后,覆盖率达92%。

3.3 平衡性检验:看懂那张表,比跑通代码更重要

匹配完成后,必须执行平衡性检验:

pstest revenue_2020 r_d_2020 patent_2020, both graph

输出表格中,重点关注三列:

| 变量 | 匹配前|t| | 匹配后|t| | %bias | |------|--------|--------|--------| | revenue_2020 | 4.21 | 0.87 | 89.6% | | r_d_2020 | 3.55 | 1.02 | 71.2% |

  • |t|值:匹配后t检验p值对应的t统计量,理想值<1.0(p>0.3)
  • %bias:匹配前后均值差异的百分比变化,必须<-10%或>10%才说明改善有效(注意:不是越小越好,-5%意味着匹配后处理组均值反而更低,可能是匹配方向错误)

踩坑实录:某次分析中,patent_2020匹配后%bias为-3.2%,看似很好,但画出密度图发现:处理组倾向得分集中在0.7-0.9,对照组被匹配的样本全在0.65-0.75——这说明匹配只在重叠区边缘进行,实际并未解决核心偏差。解决方案是放宽caliper至0.02,重新匹配后%bias变为-12.4%,t值降至0.63,密度图显示两组完全重叠。

3.4 共同支撑域(Common Support):那个被90%教程忽略的生死线

psmatch2默认不剔除超出共同支撑域的样本,必须手动执行:

gen common_support = (pscore >= 0.1 & pscore <= 0.9) keep if common_support == 1

这里的0.1和0.9不是随意定的。计算公式为:

下界 = min(pscore[if policy==1]) - 0.05 上界 = max(pscore[if policy==1]) + 0.05

(±0.05是经验缓冲值,防止边界样本噪声干扰)

我处理过某教育政策数据,处理组倾向得分范围0.25-0.85,若直接设0.2-0.9,会纳入大量倾向得分0.15的对照组样本——这些样本与处理组本质不可比,强行匹配导致DID系数虚高37%。

4. DID估计与稳健性检验:从diff命令到三重差分的进阶实战

PSM搞定后,DID才是因果效应的最终裁判。但直接用reg跑DID极易出错,diff命令专为此设计。

4.1diff命令核心语法:为什么covariates()不能省?

基础DID模型:

diff profit, t(policy) p(year) period(2021 2022) robust

但这只控制了时间固定效应,遗漏了关键协变量。正确写法:

diff profit, t(policy) p(year) period(2021 2022) covariates(revenue_2020 r_d_2020) robust

covariates()的作用是:在DID估计中控制匹配后仍存在的残余差异。比如匹配后两组营收均值差0.5%,diff会把这个微小差异纳入误差项,而加入revenue_2020后,模型能分离出“纯政策效应”。实测对比:某次分析中,不加协变量时DID系数=0.18(p=0.03),加入后变为0.12(p=0.07)——说明原结果有1/3来自营收差异的混杂。

4.2 时间窗口设定:为什么必须用period()而非time()?

diff的period()指定政策实施后的观察期(如2021-2022),而time()指定所有年份。错误用法:

diff profit, t(policy) time(year) // 错!会把政策前年份也纳入DID计算

正确逻辑是:DID需要政策前(baseline)和政策后(treatment)两期,period(2021 2022)明确告诉Stata“2021是政策年,2022是效果年”,自动构建交互项。若数据含2019-2022四年,period(2021 2022)会用2019-2020做基期,2021-2022做处理期。

4.3 稳健性检验:三重差分(DDD)破解“伪DID”陷阱

当存在其他干扰政策时,普通DID可能失效。例如分析“技改补贴”效果,恰逢同期推出“绿色信贷优惠”,两者效应混杂。此时需DDD:

* 构造三重交互项:policy × green_credit × post_period gen ddd = policy * green_credit * (year >= 2021) reg profit ddd i.year i.industry, robust

但更稳妥的是用diff的嵌套结构:

diff profit, t(policy) p(year) period(2021 2022) /// covariates(revenue_2020) /// controls(green_credit) /// robust

controls()选项会自动将green_credit作为控制变量,并检验其与policy的交互效应。若交互项不显著(p>0.1),说明绿色信贷未干扰主效应。

实操心得:DDD不是万能解药。某次分析中,加入green_credit后主系数从0.12降至0.03(p=0.21),表面看政策无效,但检查发现green_credit与policy高度相关(相关系数0.68)——这说明两政策本质是同一套筛选机制,强行DDD反而抹杀了真实效应。此时应改用“政策强度”连续变量替代二元policy。

4.4 异质性分析:Stata里做亚组分析的正确姿势

热搜词“stata如何做亚组分析”常被误解为分组回归。正确做法是:

* 按企业规模分组(small=1, medium=2, large=3) tabulate size, generate(size_) diff profit, t(policy) p(year) period(2021 2022) /// covariates(revenue_2020) /// by(size_1 size_2 size_3) /// robust

by()选项会自动为每组生成独立DID系数,并输出组间差异检验(Wald test)。比手动分组回归的优势在于:标准误按整体样本计算,避免小样本偏差。

5. 案例全流程复现:某省制造业技改补贴政策效果评估

现在用真实脱敏数据(N=12,450家企业,2019-2022年面板)走完完整流程。数据结构如下:

变量类型说明
id数值企业唯一编码
year数值年份(2019-2022)
policy二元2021年起=1(享受补贴),否则=0
profit数值净利润(万元)
revenue_2020数值2020年营收(匹配变量)
r_d_2020数值2020年研发支出(匹配变量)
patent_2020数值2020年发明专利数(匹配变量)
industry字符行业分类(机械、电子、纺织等)

5.1 数据清洗与匹配变量构造

* 导入数据 use "manufacturing.dta", clear * 生成行业虚拟变量 tab industry, generate(ind_) * 构造政策变量(2021年起为处理组) gen policy = (year >= 2021 & subsidy_flag == 1) * 关键清洗:剔除2020年营收缺失的企业(否则PSM无法计算) drop if missing(revenue_2020) | missing(r_d_2020) | missing(patent_2020) * 检查处理组分布 tab policy if year == 2021 * 输出:policy=1占18.3%,符合PSM适用条件(10%-30%)

5.2 PSM匹配与平衡性验证

* 倾向得分建模 logit policy revenue_2020 r_d_2020 patent_2020 ind_*, nolog predict pscore, xb * 计算共同支撑域边界 sum pscore if policy == 1 scalar lb = r(min) - 0.05 scalar ub = r(max) + 0.05 gen common_support = (pscore >= scalar(lb) & pscore <= scalar(ub)) * 执行匹配(Nearest Neighbor,caliper=0.015) psmatch2 policy revenue_2020 r_d_2020 patent_2020 ind_*, /// neighbor(1) caliper(0.015) outcome(profit) /// ties * 平衡性检验 pstest revenue_2020 r_d_2020 patent_2020, both graph

平衡性检验结果:

  • revenue_2020:匹配前|t|=5.32 → 匹配后|t|=0.41,%bias=-92.1%
  • r_d_2020:匹配前|t|=4.17 → 匹配后|t|=0.33,%bias=-84.3%
  • patent_2020:匹配前|t|=3.89 → 匹配后|t|=0.28,%bias=-76.5%
    所有|t|<1.0,%bias<-10%,通过检验。

5.3 DID估计与结果解读

* 保留共同支撑域样本 keep if common_support == 1 * 执行DID(含协变量) diff profit, t(policy) p(year) period(2021 2022) /// covariates(revenue_2020 r_d_2020) /// robust * 输出关键结果: * diff = 0.152, p = 0.008, [95% CI: 0.063, 0.241]

结果解读:技改补贴使企业净利润平均提升15.2个百分点,效果在1%水平上显著。置信区间不含0,排除随机波动可能。

5.4 稳健性检验:平行趋势与安慰剂检验

平行趋势检验(事件研究法):

* 生成政策前/后虚拟变量 forvalues i = -2/2 { gen pre_`i' = (year == 2021 - `i') & policy == 1 gen post_`i' = (year == 2021 + `i') & policy == 1 } * 回归(2019年为基准) reghdfe profit pre_2 pre_1 post_1 post_2 /// i.year i.industry, absorb(id) vce(cluster id)

结果显示:pre_2和pre_1系数均不显著(p>0.1),满足平行趋势假设;post_1和post_2系数递增且显著,证实政策效果随时间增强。

安慰剂检验:

* 随机生成虚假政策变量 gen fake_policy = runiform() < 0.183 // 保持与真实policy相同占比 diff profit, t(fake_policy) p(year) period(2021 2022) robust

1000次模拟中,仅有42次得到|diff|>0.05(5%显著性水平),证明真实结果非偶然。

6. 常见报错与终极避坑清单:那些文档里不会写的血泪教训

最后分享我在Stata社区答疑时整理的TOP5致命错误,每一条都来自真实翻车现场:

6.1 “no observations”错误:不是数据没了,是psmatch2悄悄删了你

当你执行psmatch2后count发现样本量锐减,别急着重跑。先检查:

* 查看被剔除的样本原因 psmatch2 policy ..., generate(_weight) noreplacement tab _weight if missing(_weight) // 显示缺失原因

常见原因:

  • _weight==.:倾向得分超出共同支撑域(需调整caliper)
  • _weight==0:匹配失败(处理组无合格邻居,需放宽caliper或换算法)
  • _weight==-1:重复匹配被禁止(noreplacement参数导致)

6.2diff命令报错“variable not found”:其实是变量名大小写惹的祸

Stata严格区分大小写。某次用户用Excel导入数据,Policy列名首字母大写,而代码中写policy,diff找不到变量直接报错。解决方案:

* 统一转为小写 foreach var of varlist * { rename `var' `=lower("`var'")' }

6.3 系数符号反常:当DID结果为负,先检查时间变量是否倒置

最隐蔽的错误:period(2022 2021)——把后一年写前面。diff会误判2022为基期,2021为处理期,导致系数符号反转。务必确认:

* 查看数据中年份顺序 list year in 1/10, sep(5) * 确保period()中年份升序排列

6.4 图形输出失败:pstest的graph选项依赖grstyle包

若执行pstest ..., graph报错“grstyle not found”,需先安装:

ssc install grstyle, replace grstyle init

6.5 结果不可复现:Stata随机种子没设

psmatch2的neighbor()算法涉及随机排序。若不设种子,每次运行结果微异。生产环境必须:

set seed 123456 psmatch2 policy ..., neighbor(1)

最后一点个人体会:PSM-DID的价值不在炫技,而在把“可能有效”变成“证据确凿”。我见过太多项目,前期投入巨大,却因PSM环节一个caliper参数没调好,导致整个结论被审稿人质疑。与其花三天调参,不如花两小时画一张倾向得分密度图——那条重叠的曲线,比任何p值都更直白地告诉你:这个因果故事,到底能不能讲下去。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询