把DML(Double Machine Learning,双重机器学习)搬进Stata这件事,我前前后后折腾了快两周。虽然现在回头看,核心代码不过七八行,但过程中踩过的坑,加起来足够写满两页A4纸。这篇东西就是把那些坑和解决方式都记下来,给同样想在Stata里跑DML、做因果推断的朋友提个醒。无论你是正在复现论文里的双重机器学习结果,还是想在政策评估、实证研究里用机器学习处理高维控制变量,这篇文章应该都能帮你省下不少调试时间。
先说清楚适用范围。DML这个方向这几年在计量经济学里非常火,尤其是处理“控制变量太多、传统线性回归不敢放”的场景。很多人第一次接触它是在论文里看到DDML这个缩写,其实是一个东西:Double/Debiased Machine Learning,双去偏机器学习。本文记录的是我在Stata里跑通DML的全过程,包括环境配置、核心代码、常见报错,以及若干只有亲手跑过才会发现的细节。
1. DML不是黑魔法:先搞懂它在做什么
1.1 解决传统回归搞不定的高维控制变量问题
DML最核心的价值,是在高维协变量下估计因果效应。举个例子,你想估计教育年限对工资的影响,理论上要控制的能力、家庭背景、地域、行业特征可能有几十上百个变量。传统做法是线性回归,把所有这些控制变量都塞进去,但问题来了:X和Y的真实关系大概率不是线性的,塞进去也是设定偏误;不塞进去,遗漏变量偏误又跑不掉。
机器学习模型擅长在高维、非线性环境下做预测,你给它几百个变量,它能灵活拟合出复杂的函数关系。但纯机器学习估计出来的因果效应是有问题的——机器学习天生带正则化,这个特性会让估计系数向零收缩,直接拿去做因果推断会得到有偏的结论。DML就是在这个背景下杀出来的:它用机器学习去拟合高维控制变量,同时通过特殊机制把正则化带来的偏差消掉,最终得到一个传统回归意义上的因果效应估计量。
1.2 交叉拟合和Neyman正交:DML的两个关键机制
DML的做法可以拆成两步理解。第一步,用机器学习分别预测处理变量D和结果变量Y。第二步,算残差,把D的残差对Y的残差做回归,回归系数就是因果效应。为什么要做残差化?打个比方,你想知道跑步能不能降低血压,但身高体重年龄这些因素也在影响血压。DML先用机器学习把X对血压的影响“剥离”掉,剩下的残差血压里X的影响已经很小,再和跑步残差做对比,就干净多了。
这里有两个细节决定了DML和普通的“先预测再回归”不一样。
第一个是交叉拟合(cross-fitting)。模型在训练集上拟合、在另一部分数据上预测,防止过拟合。传统做法如果拿全部数据既训练又预测,机器学习模型会把噪声也学进去,造成严重的偏差。交叉拟合的做法是把样本分成K折,轮流用K-1折训练模型、在剩下的1折上计算残差,循环K次,每个样本的残差都来自没有见过它的模型,这样就最大程度避免了过拟合干扰。
第二个是Neyman正交得分。这个概念可以通俗理解为:目标估计量对第一阶段的机器学习误差“不敏感”。即使机器学习模型没有把X和Y的关系拟合到完美,有一点预测偏差,这个偏差也不会传导到最终的因果效应估计上。DML之所以叫“双重”,正是因为它靠这两层机制同时压制了“正则化偏差”和“过拟合偏差”。
2. 跑DML之前,先把环境和依赖弄明白
2.1 版本要求和安装流程
在Stata里实现DML,最主流的选择是SSC上的ddml命令包。装之前先确认你的Stata版本——ddml要求Stata 16及以上,版本低了不仅语法兼容有问题,很多底层依赖也跑不起来。我一开始用的是Stata 15,装是装上了,但一跑就报unkown command或者语法解析错误,查了半天才发现是版本不达标,白折腾一场。
ssc install ddml, replace输入这行命令,Stata会自动把ddml和它依赖的辅助包一起装好。这里提醒一下:ddml的依赖包挺多,比如ftools、reghdfe、estout等等,都是自动安装的。安装的时候网络一定要稳定,装到一半断网会留下一堆半残的包,后面跑起来各种莫名奇妙的报错。我因为网络问题重装过两次,虽然最终装成功了,但时间成本确实浪费了不少。
2.2 Python环境:最大的隐形地雷
这是最容易被忽略、也是最容易卡住人的一步。ddml本身是用Stata写的,但它支持的学习器(learner)非常多,其中一部分底层调用的是Python或者R。比如pystacked学习器,底层就是Python的scikit-learn;rlearner学习器底层是R。如果你只用默认的LASSO、岭回归这些Stata原生的学习器,可能不需要配置外部环境,但一旦想用随机森林、XGBoost这类灵活的模型,Python必须提前配好。
配置方法是告诉Stata你的Python解释器在哪里:
python set exec "C:/Python39/python.exe", permanently路径要换成你自己机器上Python的实际安装路径。然后确认这个Python环境里装好了scikit-learn、pandas、numpy这些依赖库:
pip install scikit-learn pandas numpy这里有一个常见组合建议:Python 3.8或3.9搭配Stata 16/17,比较稳。Python版本太新,在某些版本的pystacked下反而会报一些莫名其妙的错误。我有一次在Python 3.12下跑randomforest,报错报得我怀疑人生,换回3.9一切都安静了。
2.3 数据格式的隐藏要求
ddml对数据格式的要求比较严格,有一个特别容易踩的坑:它不支持因子变量。你不要想着在变量列表里写i.x1或者c.x1#c.x2这种,它会直接报错。我第一次跑的时候就在这上面栽了跟头。解决方法是提前生成虚拟变量:
tabulate education, gen(edu_dum)然后把生成的虚拟变量放进协变量列表。另外,变量名建议都用英文加下划线,不要用中文变量名,也别带空格。Stata本身能处理,但ddml内部做矩阵运算时碰到特殊字符容易莫名其妙地报错,没必要给自己找麻烦。
数据里的缺失值也要提前清理干净。机器学习模型对缺失值容忍度低,pystacked这类学习器遇到缺失值会直接罢工。先用drop if missing(Y) | missing(D)把关键变量的缺失样本筛掉,再进入DML流程。这个问题我在初跑时没注意,结果报错信息完全指不到缺失值上,排查了很久才找到根因。
3. 从零跑通:核心代码与调参思路
3.1 一次完整的DML流程示例
下面用一个实际例子把整个流程串起来。假设我要估计教育年限对工资的影响,数据里有工资y、教育年限edu,还有一批控制变量x1到x20,包括年龄、工龄、地域、行业、父母教育背景等等。处理变量edu是连续的,所以这是一个连续处理变量的DML模型。完整代码长这样:
* 1. 初始化:设定结果变量、处理变量和协变量 ddml init y, endog(edu) varlist(x1-x20) * 2. 指定学习器:至少指定一个用于Y方程、一个用于D方程 ddml learners(lasso, learner(lassoregress)) ddml learners(rf, learner(randomforest)) * 3. 交叉拟合:设置折数和重复次数 ddml crossvalidate, kfold(5) reps(1) * 4. 估计处理效应 ddml estimate, vce(cluster id)第一行ddml init是整个模型的基础。y是结果变量,endog()里放的才是我们关注的处理变量,varlist()里放控制变量。注意这里的endog不是传统工具变量回归里那个“内生变量”的意思,在ddml的语境里,它就是“我们的处理变量D”。
第二步指定学习器,这是决定模型表现的关键一环。ddml支持很多学习器,最常用的包括lassoregress(LASSO)、ridgeregress(岭回归)、elasticnet(弹性网)、randomforest(随机森林)、xgboost等。建议至少指定两种学习器,让ddml自己通过交叉验证去比较预测表现。
第三步ddml crossvalidate执行交叉拟合。kfold设定折数,默认是2。想要更稳妥的结果可以设成5。reps代表重复次数,默认是1,我一般至少跑到50甚至100,结果会更稳定,但耗时也明显增加。数据量大的时候,这个时间成本要提前算进去。
最后ddml estimate输出估计结果。vce(cluster id)是指定聚类稳健标准误,id是样本个体的ID变量。没有聚类需求的话可以直接用vce(robust)。
3.2 学习器选择:不同场景用不同的组合
刚接触ddml的同学经常纠结学习器怎么选。结合我的实际体验,给一个简单的参考思路:
| 学习器 | 适用场景 | 备注 |
|---|---|---|
| lassoregress | 高维稀疏变量,基准模型首选 | 速度快、结果稳定、好解释 |
| ridgeregress | 变量间共线性较强 | 系数收缩但不停留于零 |
| elasticnet | LASSO和岭回归的折中 | 兼顾变量选择和共线性 |
| randomforest | 非线性关系、变量交互多 | 样本量要够,小样本慎用 |
| xgboost | 大样本、强非线性 | 调参成本高,跑得慢 |
| pystacked | 想用scikit-learn全家桶 | 需要提前配置Python环境 |
我的习惯是:先跑一个LASSO基准模型,把整个流程跑通,确认数据没问题、结果符合直觉,然后逐步加复杂学习器,比如随机森林或者XGBoost。不要一上来就把所有学习器全都堆上去,又慢又不好排查问题。
3.3 两个你一定会遇到的模型设定细节
处理变量是二值的时候要特别小心。比如估计“是否参加职业培训”对收入的影响,D是0/1变量。很多人直接把它放进endog(),学习器还用lassoregress,结果发现估计出来的ATE方向跟常识相反。原因很简单:二值处理变量的D方程本质上是一个分类问题,用连续回归的损失函数去拟合0/1变量,效果自然很差。这时候需要换用适合分类的学习器,比如learner(probit)或者基于logit的super learner。我自己第一次犯这个错误时,完全没往模型设定上想,白白折腾了一天。
还有一个细节是随机种子。DML的结果天然带有随机性,因为交叉拟合时要随机划分样本。不设随机种子的话,每次跑出来的ATE都会略有不同,审稿人找你要do文件重跑一遍,结果对不上,非常尴尬。所以跑之前先设好随机种子:
set seed 12345这个习惯建议从一开始就养成,不要等到结果出锅了才想起来。
4. 高频报错和排查实录
4.1 报错速查表
跑DML过程中遇到的报错,大部分都可以归到下面这几种情况。我把它们整理成了一张速查表,方便你对照排查:
| 报错现象 | 常见原因 | 解决方式 |
|---|---|---|
| command ddml is unrecognized | ddml未安装,或Stata版本过低 | ssc install ddml, replace;升级到Stata 16+ |
| factor variables not allowed | ddml不支持因子变量 | 用tabulate生成虚拟变量 |
| ModuleNotFoundError: No module named 'sklearn' | Python环境缺少依赖库 | pip install scikit-learn |
| Python executable is not valid | python set exec路径不对 | 重新指定Python解释器路径 |
| insufficient observations | 样本量过小,折后训练集太小 | 扩大样本量或减少kfold折数 |
| convergence not achieved | 估计不收敛,共线性或样本太少 | 检查协变量共线性,减少变量数量 |
| r(198) invalid 变量名 | 变量名含特殊字符或语法错误 | 检查变量名,统一用英文下划线命名 |
4.2 样本量太小,DML真的跑不动
这是最让人沮丧的坑。我一开始用的数据只有300多个观测值,这个体量跑普通OLS完全没问题,但放到DML里,交叉拟合之后一折训练集只有60个样本,机器学习模型在60个样本上拟合出来的预测值波动极大,最后ATE的置信区间宽到失去意义。
后来总结的经验是:DML的大样本属性非常强,样本量少于1000时要格外谨慎。如果数据实在没法扩大,几个补救办法可以试一下:一是把kfold从默认的5降到2,让每折训练集尽量大;二是学习器换成更稳定的lassoregress,别用随机森林这种方差大的;三是直接换回传统线性回归,承认数据量不支撑DML的发挥余地。有时候承认条件不够,也是一种效率。
4.3 处理变量是分类变量时,结果为什么会反了
我帮一个同学排查过这个问题。他的处理变量是“是否参加培训”,0/1变量,跑出来的ATE显著但方向是负的,跟理论预期完全相反。检查了变量编码、缺失值、样本筛选,都没问题。最后翻文档才反应过来:他在ddml learners里用的全是lassoregress这类连续回归学习器,没有为二值D指定分类学习器。
D方程要预测的是一个分类标签,你用连续回归的损失函数去拟合0/1变量,拟合出来的预测值几乎都是平滑的中间值,对应的残差结构完全错乱,最终ATE自然就偏了。解决办法是在D方程里明确指定分类学习器,比如用probit学习器,或者用pystacked配合logistic回归,模型设定对了,结果立刻就恢复正常。
4.4 kfold设得太多,结果来回跳动
有段时间我图“严谨”,把kfold设成10,想着折数越多越稳定。结果完全相反——ATE每次跑都不一样,标准差巨大,同一个数据换个种子结果就变了。原因很简单:数据量中等的情况下,10折意味着每折训练样本太少,模型在少量样本上容易过拟合,折与折之间差异被放大。
后来改回5折,结果立刻稳定下来。这里想说的核心教训是:DML里的kfold和reps两个参数是联动的,折数多不代表更稳健,要看总样本量能不能撑住。我的建议是样本量1000左右用5折,3000以上可以考虑10折,同时把reps提高到100来平滑随机波动。
5. 结果解读、稳健性汇报与最终建议
5.1 ddml的输出表格到底怎么看
ddml estimate跑完后,屏幕上会输出三类核心参数:ATE(平均处理效应)、ATET(处理组的平均处理效应)、ATEN(未处理组的平均处理效应)。绝大多数论文里报告的是ATE,它代表整个样本平均意义上处理变量对结果变量的影响。如果你关心的是“政策作用于实际受影响群体”的效应,那ATET更合适。
这里要强调一个容易混淆的点:ATE并不是“机器学习模型预测出来的差值”,它本质上还是“D的残差对Y的残差回归的系数”。所以你在解读时,可以沿用传统回归的直觉去理解——只不过这个系数经过了正交化处理的修正,在高维控制变量下更可信。
5.2 稳健性检验怎么汇报,审稿人才会认
做实证研究,稳健性检验是必不可少的一环。DML的稳健性检验有固定的套路,建议至少覆盖以下几个维度:
- 换学习器组合:至少报告一个线性学习器(如LASSO)加一个非线性学习器(如随机森林),结果方向一致说明结论不依赖特定模型。
- 换交叉拟合折数:比如从5折改成10折或者2折,抑制采样随机波动的影响。当然结合前面说的,折数选择要参考样本量。
- 增加重复次数:reps从1提高到100,结果应该基本一致,标准差会小幅缩小。
- 增删控制变量:在varlist里加入或删除一部分变量,ATE方向不翻转是底线。
- 子样本分析:如果数据支持,分样本跑一遍,增强结论的外部效度。
汇报格式上,我一般会把学习器列表、折数、重复次数、随机种子、样本量完整写在论文的注释或者附录里,这是最基本的学术规范。
5.3 一点个人实操体会
最后说点实际的。我现在的习惯是:跑DML之前,先用普通OLS跑一遍,给自己一个基准直觉——DML的结果即使再漂亮,也不应该和传统回归方向相悖,如果方向都反了,先别怀疑DML“高深”,大概率是自己模型设定或者数据处理有问题。DML不是万能药,它解决的是高维控制变量下的偏差问题,但内生性问题它也不可能替你背锅。工具变量的逻辑该用还得用,DML和IV结合的场景也越来越多。
这篇踩坑实录,算是我用一个个不眠之夜换来的。做研究遇到报错不丢人,丢人的是同一个坑踩三次。希望这篇东西能帮你在Stata里少走点弯路。