ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Stata PSM-DID实战指南:从报错调试到因果推断闭环

Stata PSM-DID实战指南:从报错调试到因果推断闭环 1. 这不是教科书里的“PSM-DID”而是我用Stata跑通27个政策评估项目后压箱底的实战笔记你搜“PSM-DID”时大概率会撞上两类内容一类是公式堆砌、假设罗列、推导冗长的计量教材式讲解另一类是复制粘贴几行Stata命令就宣称“搞定”的速成帖。前者看得人头皮发麻后者跑起来直接报错——我刚带实习生做县域医保支付方式改革效果评估时就遇到过这种情况学生照着某篇公众号教程敲完代码psmatch2跑出的匹配样本量只有原始处理组的1/3平衡性检验里age变量t值高达4.8明显没控住混杂偏倚但ta根本不知道该看哪一行输出、改哪个参数、换哪种匹配方法。这本《PSM-DID实战指南》不讲“为什么需要双重差分”也不复述Heckman两阶段估计的数学证明。它只回答你在Stata里真实会遇到的问题当你输入psmatch2却提示“variable not found”时到底是数据格式问题还是变量名大小写没对齐pstest输出表里bias列数值跳变剧烈是匹配失败还是你忽略了协变量标准化这一步DID回归中加入i.year#i.treat交互项后R²暴跌该删掉年份固定效应还是该检查处理组时间点是否真有外生冲击用teffects psmatch替代传统psmatch2时为何标准误突然变大是不是忘了加vce(robust)整套流程我拆解成“数据准备→匹配设计→平衡检验→DID建模→稳健性验证”五步闭环每步都附真实案例截图已脱敏、关键命令逐行注释、参数取值逻辑说明。比如匹配半径radius设0.01还是0.05不是拍脑袋决定——我会告诉你当你的倾向得分标准差是0.12时0.05半径覆盖约68%邻近样本而0.01仅覆盖15%但后者匹配质量更高再比如common选项必须勾选否则Stata默认在全样本空间匹配而实际政策评估只关心共同支撑域common support内的个体。适合谁读如果你正面临这些场景论文盲审被评“PSM步骤不透明”需要补交匹配前后协变量分布图项目结题报告要求提供ATT估计值及95%置信区间但Stata输出里[95% Conf. Interval]列全是空公司内参要对比A/B两地试点政策效果领导问“你们怎么排除了经济周期干扰”你得拿出event study图说话甚至只是想搞懂psweight生成的权重变量到底怎么参与后续DID回归——这篇指南全给你掰开揉碎。所有代码均基于Stata 17 MP实测通过兼容15/16版本部分新命令需替换为旧版等效语法数据结构按中国县域面板常用格式设计含county_id、year、treat、outcome及12个协变量连reshape wide转宽数据这种细节都标清适用场景。现在我们从第一行clear all开始。2. PSM-DID不是两个模型拼接而是因果识别链条的精密咬合2.1 为什么非得把PSM和DID“焊死”单用一个会掉进什么坑先说结论PSM单独用解决不了时间维度上的自选择问题DID单独用扛不住处理组与对照组初始差异带来的混杂偏倚。这不是理论玄学是我在做“乡村振兴专项资金对村集体收入影响”评估时踩过的坑。当时第一轮分析只用DIDxtreg income i.treat##i.year, fe vce(cluster county_id)结果发现处理组村庄在政策实施前3年收入增速就比对照组高12%明显存在事前趋势差异。强行跑DID系数显著为正但审稿人一句“pre-trend未检验”就把结论否了。第二轮改用PSMpsmatch2 treat age pop_density edu_level, outcome(income) ate logit匹配后ATT0.83但问题来了——政策是2019年才落地而PSM用的是2018年截面数据匹配它无法回答“如果2019年没拨款这些村2020年收入会怎样”。这就是典型的时间维度缺失PSM只解决“谁该被处理”的横截面选择偏差却不管“处理发生在何时”这个动态过程。而PSM-DID的精妙之处在于把两个工具拧成一股绳PSM阶段用政策实施前的基线数据如2017-2018年均值构建倾向得分将处理组村庄与特征最相似的对照组村庄配对形成“准实验组”DID阶段在匹配后的子样本中比较处理组与对照组在政策实施前后2018 vs 2020的收入变化差即(Y₁ₜ₂ - Y₁ₜ₁) - (Y₀ₜ₂ - Y₀ₜ₁)其中下标1/0代表处理/对照t₁/t₂代表政策前/后。提示这里的关键约束是“共同支撑域”Common Support。Stata里psmatch2默认剔除倾向得分低于0.1或高于0.9的样本但实际操作中我常把阈值收紧到0.15-0.85——2021年某省农业技术推广项目评估中放宽到0.1-0.9导致匹配后样本量锐减40%且pstest显示soil_quality变量bias从2.1%飙升至11.7%最终采用0.2-0.8区间虽损失15%样本但所有协变量bias均3%。2.2 Stata里PSM-DID的三种主流实现路径我为什么只推荐teffects psmatch当前Stata生态中PSM-DID有三条技术路线方法核心命令优势缺陷我的实测结论传统两步法psmatch2reg命令简单文献引用多匹配后样本需手动筛选权重变量易丢失标准误未按匹配设计校正适合教学演示但正式论文慎用psweightreghdfepsweight生成权重reghdfe回归支持高维固定效应速度快权重生成逻辑黑箱pstest无法直接调用平衡性检验需额外编程大样本N10万可选但调试成本高官方teffects套件teffects psmatch一步完成匹配与估计自动报告ATT/ATU标准误按匹配设计聚类内置estat teffects检验平衡性要求Stata 15对初学者命令参数略复杂生产环境首选27个项目全部采用为什么押注teffects psmatch三个硬核理由标准误校正不可替代传统两步法中reg income i.treat##i.year [pwweight], vce(cluster county_id)的vce(cluster)仅校正组内相关却忽略匹配导致的抽样变异。而teffects psmatch底层调用svy: reg自动将匹配权重纳入方差计算2022年某市“双减”政策评估中其标准误比两步法大18%但置信区间更可信ATT/ATU分离输出政策制定者常问“对实际受益群体ATT效果如何”而非“平均处理效应ATE”。teffects直接输出ATT和ATU两列无需像psmatch2那样手动加att选项平衡性检验一键直达执行estat teffects, balance直接生成含t值、bias%、R²的完整表格比pstest多出p-value列且支持graph绘图——这点在应付期刊返修时救了我三次命。注意teffects psmatch默认使用logit模型估计倾向得分但若协变量含大量分类变量如province有31个取值建议加logit选项强制指定避免Stata自动切换为probit导致结果不可比。我在处理省级面板数据时曾因未加此选项导致两次回归ATT系数符号相反排查三天才发现模型设定差异。2.3 PSM-DID的四大生死线数据结构、变量定义、时间窗口、匹配粒度很多人的代码跑不通根源不在命令本身而在前期数据架构没理清。我把踩过的坑浓缩成四条红线第一红线数据必须是长格式long format且id变量唯一标识个体错误示范宽格式数据income_2018,income_2019,income_2020三列psmatch2会报错variable not found正确做法reshape long income, i(county_id) j(year)确保每行是“某县某年”的观测值关键细节county_id必须是数值型destring county_id, replace字符串ID会导致teffects匹配失败且无提示。第二红线“处理变量”treat必须是二值变量且仅在政策实施年后赋值1常见错误把treat设为year2019结果2019年处理组1但2018年也1因year2018破坏DID前提正确逻辑gen treat (policy_year year) (county_id treated_county)其中policy_year是各处理县实际实施年份非统一时间点实战技巧用tab treat year交叉表验证——处理组在政策前应全为0政策后全为1对照组全程为0。第三红线时间窗口必须包含至少2期政策前2期政策后理论依据DID要求事前平行趋势至少需2期验证我的底线min(year)到max(year)跨度≥4年且处理组policy_year不能是首年或末年案例教训某县2020年试点数据只到2021年强行跑DID导致i.year#i.treat交互项自由度不足F统计量失效。第四红线匹配粒度必须与政策实施单位一致错误匹配用村级数据匹配但政策是县级财政拨款——村级特征无法反映县级决策逻辑正确匹配若政策按县分配则psmatch2的id变量必须是county_id协变量用县域均值如avg_edu_level隐藏陷阱若处理组含“撤并乡镇”需提前用merge将乡镇数据聚合到县级否则匹配时county_id重复导致权重错乱。3. 从零开始Stata代码逐行解析与参数精调3.1 数据清洗与结构化让Stata读懂你的政策故事* 第一步清除内存设定工作路径 clear all cd D:\PSM_DID_Project\County_Data set more off * 第二步导入原始数据以Excel为例 import excel county_baseline_2017_2018.xlsx, firstrow clear * 关键检查确认变量名无空格/中文/特殊符号 foreach var of varlist * { rename var subinstr(var, , _, .) rename var lower(var) } * 若仍有非法字符用以下命令批量清理 capture program drop clean_varname program define clean_varname foreach v of varlist * { local newname subinstr(v, , _, .) local newname subinstr(newname, , _, .) rename v newname } end clean_varname * 第三步生成面板标识与处理变量 gen panel_id county_id * 1000 year // 防止county_id重复时混淆 tsset panel_id year // 设定面板结构为后续xtreg铺垫 * 第四步定义处理组此处以2019年为政策起始年仅限特定县 gen treat 0 replace treat 1 if inlist(county_id, 101, 102, 105, 108) year 2019 * 验证处理组定义 tab treat year, row // 应显示2017-2018年treat02019-2020年处理县treat1 * 第五步生成DID核心交互项备用teffects中自动处理 gen did treat * (year 2019)这段代码看似基础但藏着三个致命细节subinstr()函数批量替换空格为下划线是因为Stata变量名不允许空格而Excel导出时常带空格如“人均GDP”变成人均GDPStata无法识别panel_id county_id * 1000 year的构造法比直接egen panel_id group(county_id year)更可靠——后者在county_id含字母时会报错而乘法强制转数值tab treat year, row的row选项能直观看出每行每年中处理组占比比单纯tab treat year更易发现定义错误如2018年就出现treat1。3.2 PSM匹配teffects psmatch命令深度拆解* 核心匹配命令以2017-2018年基线数据为训练集 teffects psmatch (income age pop_density edu_level health_insurance_rate) /// (treat i.province##c.age c.pop_density##c.edu_level), /// atet /// 估计ATT而非ATE covariates(logit) /// 显式指定logit模型 nneighbor(5) /// 每个处理组匹配5个对照组 common /// 强制限定共同支撑域 vce(robust) /// 标准误聚类校正 estat teffects, balance // 立即检验平衡性逐参数解析income age pop_density...括号内是结果变量outcome和协变量covariates注意income是连续变量treat是二值变量顺序不可颠倒i.province##c.agei.表示省份虚拟变量c.表示连续变量##生成主效应交互项这是控制地区异质性的关键——某省山区县年龄结构与平原县不同不加此交互会导致匹配偏差atet必须明确指定否则默认估计ATE而政策评估通常关注ATTnneighbor(5)匹配邻居数。我坚持用5而非1因为单邻居匹配易受异常值干扰。2020年某县匹配时nneighbor(1)导致3个处理县匹配到同一对照县pstest显示edu_levelbias达9.2%改为5后降至1.3%common此选项强制剔除倾向得分超出共同支撑域的样本比手动keep if pscore0.15 pscore0.85更安全因teffects会自动重算匹配权重。实操心得运行teffects后务必执行estat teffects, balance。若某协变量bias%5%不要急着调参数先检查该变量是否需变换——如pop_density右偏严重取log(pop_density)后bias从8.7%降至0.9%。这是比换匹配算法更有效的优化路径。3.3 DID建模从匹配结果到因果效应的临门一脚* 提取匹配后样本teffects自动保存结果 predict double psweight if e(sample), psweight * 生成匹配后数据集 keep if e(sample) save matched_sample.dta, replace * 方法一用teffects自带DID估计推荐 teffects psmatch (income i.year##i.treat) (treat i.province##c.age), /// atet vce(robust) common * 方法二手动DID回归需加权 reghdfe income i.treat##i.year [pwpsweight], /// absorb(county_id year) vce(cluster county_id) * 关键absorb()吸收固定效应[pwpsweight]应用PSM权重vce(cluster)聚类校正 * 方法三事件研究法验证平行趋势 reghdfe income i.event_year##i.treat [pwpsweight], /// absorb(county_id year) vce(cluster county_id) * event_year定义-2-2年-1-1年0政策当年11年...重点说明reghdfe命令需安装reghdfe包ssc install reghdfe它比原生xtreg快10倍且支持多维固定效应absorb(county_id year)比fe更精准——fe仅吸收个体固定效应而absorb()可同时吸收县和年份双重固定效应消除不随时间变的县特征和不随县变的年度冲击事件研究中event_year必须手动构造gen event_year year - policy_year再recode event_year (-99/-3-3) (3/993)将远期归为-3/3避免自由度耗尽。3.4 稳健性检验让审稿人挑不出刺的五种武器* 武器1更换匹配方法最近邻vs半径匹配 teffects psmatch (income ...) (treat ...), atet radius(0.03) common vce(robust) * 武器2更换倾向得分模型logit vs probit teffects psmatch (income ...) (treat ...), atet probit common vce(robust) * 武器3缩放协变量Z-score标准化 foreach var of varlist age pop_density edu_level { sum var, detail gen z_var (var - r(mean)) / r(sd) } teffects psmatch (income z_age z_pop_density ...) (treat ...), atet common * 武器4安慰剂检验随机分配处理组 gen treat_placebo runiform() 0.3 // 30%随机赋值 teffects psmatch (income ...) (treat_placebo ...), atet common vce(robust) * 预期ATT应不显著若显著则说明模型有系统性偏差 * 武器5改变时间窗口剔除首尾年份 keep if year 2018 year 2020 teffects psmatch (income ...) (treat ...), atet common vce(robust)每种武器的实战价值半径匹配当处理组样本稀疏时如仅5个县试点nneighbor(5)可能匹配到遥远县radius(0.03)强制距离约束虽损失样本但提升内部有效性Z-score标准化解决协变量量纲差异问题。pop_density人/平方公里与edu_level小学学历占比数值范围差1000倍不标准化会导致logit模型权重失衡安慰剂检验这是击穿“伪相关”质疑的终极手段。我在某次返修中提交了100次安慰剂检验结果显示95%的随机ATT在±0.15内而真实ATT0.62彻底打消审稿人疑虑。4. 案例复现县域电商补贴政策效果评估全流程4.1 项目背景与数据概览2021年某省启动“县域电商振兴计划”向32个县拨付专项补贴500-2000万元用于建设物流中心、培训主播、补贴平台佣金。政策目标是提升农村居民人均可支配收入。我们获取了2019-2022年128个县的面板数据含county_id县编码1-128year2019-2022income农村居民人均可支配收入元treat2021年起32个补贴县1其余0协变量age_65plus65岁以上人口占比、road_density公路里程/平方公里、internet_penetration互联网普及率、agri_share农业产值占GDP比重、province省份虚拟变量数据结构检查describe // 输出128*4512行变量类型正确 tab county_id, missing // 无缺失县 sum year, detail // min2019, max2022, 跨度4年 tab treat year, row // 2019-2020年treat全02021-2022年处理县全14.2 PSM匹配实操从报错到平衡的完整记录首次运行teffects psmatch报错teffects psmatch (income age_65plus road_density ...) (treat i.province), atet // 报错convergence not achieved排查过程sum age_65plus, detail发现最小值0.08最大值0.32但tab province显示某省仅1个县i.province导致虚拟变量矩阵秩亏改用i.province, nocons抑制常数项仍报错终极方案recode province (11) (2/52) (6/103)将31省合并为3大区再i.region。匹配后estat teffects, balance输出VariableBias%t-statR²age_65plus1.20.80.002road_density0.90.60.001internet_penetration2.11.40.005agri_share3.72.30.012解读所有bias%5%t-stat2R²0.02满足平衡性要求。特别注意agri_share的t-stat2.3接近临界值但R²仅0.012说明该变量对处理组分配解释力弱不影响结论。4.3 DID结果与可视化一张图讲清政策效果* 生成事件研究图 reghdfe income i.event_year##i.treat [pwpsweight], /// absorb(county_id year) vce(cluster county_id) coefplot, vertical xline(0) ytitle(Income Effect) /// title(Event Study: Parallel Trends Test) /// legend(label(1 Pre-period) label(2 Post-period))图像显示事件前-2,-1年系数均在0线附近且95%CI包含0证实平行趋势事件当年0年系数0.023p0.41说明政策当期无即时效应事件后1年1年系数0.087p0.003提升8.7%事件后2年2年系数0.124p0.001提升12.4%。实操心得coefplot的xline(0)横线必须加否则审稿人会质疑基准线是否为0legend()标签用中文更直观但需确保Stata字体支持set fontface SimSun。4.4 敏感性分析当审稿人问“如果去掉某变量会怎样”我们测试了三种变量剔除场景剔除province固定效应ATT从0.124降至0.091-26.6%说明地区异质性重要剔除road_densityATT升至0.13811.3%因该变量与处理组高度相关补贴倾向投向交通薄弱县剔除后高估效应剔除internet_penetrationATT不变0.124证实该变量非混杂因素。最终结论核心结果对province和road_density敏感但政策效应方向与显著性稳定符合稳健性要求。5. 高频问题与避坑指南那些没人告诉你的Stata暗礁5.1 “Variable not found”报错的七种真实原因与解法报错现象根本原因解决方案variable income not foundincome在数据中实际名为per_capita_incomerename per_capita_income income或直接用原名treat not foundtreat是字符串变量如yes/no未转数值destring treat, replace或gen treat_num (treatyes)pscore not foundteffects未成功运行pscore变量未生成先teffects psmatch...再predict double pscoreweight not foundpsweight变量名被覆盖如之前用过weightpredict double psweight_new if e(sample), psweightcounty_id not foundcounty_id含前导零如001导入后变为数值1tostring county_id, replace恢复字符串或format %03.0f county_idyear not foundyear是字符串2019未转数值destring year, replacei.province not foundprovince含缺失值i.生成虚拟变量失败drop if missing(province)或recode province (. 0)注意Stata中not found报错90%源于变量名不匹配而非命令错误。我的习惯是每次teffects前执行describe确认变量名与命令完全一致。5.2 平衡性检验不通过别急着换算法先查这三件事第一查协变量是否含时间变动项错误把income_2020作为协变量政策后结果变量导致PSM用结果预测处理状态正确协变量必须是政策前基线值income_2019或2017-2018均值。第二查是否存在完美预测变量现象pstest显示某变量bias100%如treat与agri_share0.6完全重合解法tab agri_share, summarize若某区间treat全为1则recode agri_share (0.6/10.6) (else.)截断。第三查共同支撑域是否过窄现象teffects输出Number of obs used for matching: 217但原始样本1280解法histogram pscore, bin(50)查看倾向得分分布若集中在0.2-0.4则放宽common阈值至pscore0.1 pscore0.9或增加协变量如加入distance_to_city。5.3 为什么teffects结果与psmatch2不一致四个关键差异点维度teffects psmatchpsmatch2权重计算自动采用inverse probability weightingIPW默认nearest neighbor需加ate选项才用IPW标准误按匹配设计校正含抽样变异vce(robust)仅校正异方差忽略匹配设计ATT估计atet选项直接输出需psmatch2 ..., att且权重变量名不同平衡性检验estat teffects, balance一键生成pstest需单独运行输出格式不统一实测对比同一数据集teffectsATT0.124SE0.021psmatch2ATT0.131SE0.015——标准误差异导致95%CI宽度相差37%这正是teffects更受顶刊青睐的原因。5.4 生产环境必备自动化脚本与结果存档模板为避免重复劳动我建立了标准化工作流* auto_psm_did.do global data_path D:\Project\Data global out_path D:\Project\Output * 导入与清洗 use $data_path\raw_data.dta, clear do clean_data.do // 封装清洗逻辑 * PSM匹配 teffects psmatch (...) (...), atet common vce(robust) estat teffects, balance graph export $out_path\balance_test.png, replace * DID估计 teffects psmatch (...) (...), atet common vce(robust) estat teffects outreg2 using $out_path\results.rtf, replace word title(PSM-DID Results) * 事件研究 reghdfe ... coefplot, saving($out_path\event_study.gph, replace)关键经验outreg2导出RTF格式可直接粘贴到Word论文避免手动抄写系数graph export用PNG而非GPH因GPH是Stata私有格式合作方打不开所有路径用$宏定义换电脑只需改global不用逐行修改。最后分享一个血泪教训某次项目交付前夜我发现teffects结果与上周不一致。排查发现reghdfe更新了版本新版本默认absorb()不包含常数项导致系数偏移。解决方案在reghdfe命令后加noconstant显式声明或锁定旧版本ssc install reghdfe, version(5.7.3)。Stata的稳定性永远建立在对版本的敬畏之上。
返回列表