
前阵子一个师弟把论文初稿发我方法部分赫然写着“倾向得分匹配PSM”。我只回了一句如果你的研究问题关心的是整个目标人群的平均处理效应匹配这个做法从第一步就跑偏了。这不是抬杠是我把倾向得分加权和倾向得分匹配放到同一套数据里反复对比跑过之后越来越确信的一件事。这篇就把两套思路掰开揉碎讲清楚——它们为什么不一样、各自估算的到底是什么量、在Python里怎么做倾向得分加权、以及权重算完以后那些绕不开的坑。1. 当审稿人说“做一下倾向得分匹配”时我为什么想拦一下1.1 倾向得分本来有好几个用途匹配只是被用得最多的一个倾向得分propensity score的定义不复杂在给定一组协变量X的条件下个体接受处理Z1的概率记作e(x)P(Z1|Xx)。Rosenbaum和Rubin在1983年证明了一个漂亮的性质——在给定倾向得分的条件下处理分配和协变量条件独立。意思是只要我们把倾向得分控制住那组混杂因素在两组之间的分布就“看起来像”随机试验里那样均匀了。但这里的关键是控制住倾向得分的方法远不止一种常见至少四类。匹配给每个处理组个体找一个倾向得分相近的对照组个体配对后比较。分层把倾向得分切成若干区间在每一层内做两组比较再汇总。加权用倾向得分的倒数给每个人一个权重再造出一个两组协变量分布均衡的伪总体。回归调整直接把倾向得分作为协变量放进结局回归模型。奇怪的是过去十几年里论文和软件默认选项几乎都变成了匹配。Stata里一个psmatch2横行了快二十年各种教科书把“倾向得分匹配”当成因果推断代名词。审稿人看到“PSM”三个字母就觉得平衡性没问题看到IPTW反而可能皱眉。这导致很多研究者压根没有仔细想过匹配并不是倾向得分的唯一归宿甚至在很多场景下是最糟糕的那个归宿。1.2 “找平衡”的安全感掩盖了匹配的三个代价匹配之所以让人放心是因为它的逻辑特别好懂处理组个体是男的、40岁、本科那我就去对照组里找一个同样是男的、40岁、本科的人来比。视觉上一对一很像随机对照试验的配对设计审稿人看着舒服。但舒服不等于正确。我把这些年在实战里反复体会到的三个代价先摆出来后面几节会逐一展开。代价一样本被扔掉。一对一最近邻匹配通常只能保住一部分样本剩下大量未配上的个体直接被排除在分析之外。这意味着样本量缩水、统计功效下降也意味着“匹配完的样本”已经不是你最初收集的那个样本。代价二目标人群被换掉。匹配后的分析人群是“能找到匹配对象的处理组个体”再加上“被选中当对照的对照组个体”这既不是全部处理组也不是全人群最终估计出来的效应到底是谁的效应很多人说不清楚。代价三结果对算法参数敏感。卡尺取0.01还是0.05、是放回还是不放回、一比一还是一比多不同的匹配设置给出的结果可能相差不少审稿人问你为什么选这个参数你很难给出让人信服的理由。我见过太多人拿着一份几千人的数据匹配完只剩一半还在那汇报匹配后的均值差。每次看到这种操作我都想问一句你那扔掉的另一半样本凭什么就不配进入你的因果结论2. 匹配的本质是“找替身”加权的本质是“重造一个伪总体”2.1 匹配到底做了什么事样本缩水、目标变成ATT先看一组虚拟数字。假设你有一个真实观察性研究处理组1000人对照组4000人。你用1:1最近邻匹配卡尺设为0.02最后成功匹配上的处理组个体只有650人。你会怎么做绝大多数人的习惯是650对行就用这1300人分析。剩下350名处理组个体和3350名对照组个体就这样被“优化”掉了。被匹配掉的处理组个体有一个共同点他们在协变量空间里找不到足够接近的对照组个体。翻译成大白话就是他们长得跟对照组“不像”。可问题是这些“不像”的人恰恰代表了处理组里跟对照组差异最远的那部分人。你把他们扔了处理组只剩下跟对照组长得像的人然后你得出一个结论说“处理有效应”。严格来说这个结论只对“能匹配上的处理组个体”成立学名叫做ATT受处理者的平均处理效应的一部分更准确地说是“在共同支持域内的ATT”。如果处理效应真的是一个常数那ATE和ATT是一样的扔掉样本不改变效应的值只是损失精度。但是只要效应存在异质性——比如年轻人获益更大、病情重的患者获益更小——那么把处理组里“跟对照组不像”的人丢掉得到的结果必然偏离真正想要的全人群效应。这就是为什么我在审稿时看到一份匹配后的分析一定会追问研究者你报告的是ATE还是ATT你丢掉的那部分样本是不是刚好效应最强的那部分2.2 IPTW权重是怎么来的逆概率加权的直观推导倾向得分加权的核心公式非常简单ATE权重形如w_i Z_i / e(x_i) (1 - Z_i) / (1 - e(x_i))先拆开看直觉。处理组个体Z_i1权重视倾向得分的倒数。一个倾向得分很低的处理组个体说明在观察数据里像他这样背景的人很少被分到处理组那么他在数据里是“稀缺的”所以给他一个更大的权重让他在伪总体里多代表几个“看不见的同类”。对照组个体同理权重是1减去倾向得分的倒数倾向得分很高却仍然落在对照组的个体同样罕见同样值得放大。这么做的结果是什么加权之后两组在协作变量上的边缘分布都会向“全体样本的分布”靠拢于是我们得到一个虚拟的随机化样本处理组被放大成“如果所有人都接受处理会怎样”的伪总体对照组被放大成“如果所有人都没接受处理会怎样”的伪总体。两者均值之差就是ATE。这个过程可以理解成调查抽样里的“事后加权”——性别比例偏了就调权重把比例掰回来只不过这里掰的是处理分配的概率。我一直觉得加权远比匹配优雅的原因是它一个样本都没有浪费而且估计目标从一开始就清清楚楚地指向总体。你没引入任何“挑朋友凑对子”的主观参数只要倾向得分模型定了权重就是唯一的。2.3 一个关键但常被忽略的问题你的因果参数到底是ATE还是ATT写到这里必须停下来问一句你的研究问题到底要什么如果问题是“这项政策如果推广到全部合资格人群平均效果是多少”你要的是ATE。如果问题是“已经参与了项目的这批人他们从中得到了多少好处”你要的是ATT。研究计划书、论文引言里的研究目标决定了你该用哪个参数。可是绝大多数实证论文写研究目标时都含糊其辞最后做了什么参数的估计自己也说不清。匹配默认给的是ATT还不一定完整加权默认给的是ATE。你拿一个问ATE的问题去匹配等于把结论建立在被删减过的人群上这是原则性错误。当然加权同样可以估计ATT只要换一套权重w_ATT P(Z1) * Z_i / e(x_i) P(Z1) * (1 - Z_i) / (1 - e(x_i)) * e(x_i) / (1 - e(x_i))看着复杂实际上是把对照组样本往处理组的协变量分布上拉。如果你真想回答ATTIPTW也能做没必要非去匹配。反过来匹配要估计ATE却很麻烦因为你得保证每个控制组个体都能找到匹配这在逻辑上就绕。明白这一点之后很多方法选择上的纠结其实已经解决了一半。3. Python实战从倾向得分估计到IPTW权重的完整流水线3.1 第一步模拟一份带混杂的数据并估计倾向得分空谈无益直接上代码。我习惯用Python跑整个链路本身是开源、可复现、而且统计推断部分用statsmodels就能覆盖。先造一份模拟数据连续协变量X1、X2二分类协变量X3处理分配由协变量决定结局同时受处理和协变量影响。import numpy as np import pandas as pd import statsmodels.api as sm from sklearn.linear_model import LogisticRegression np.random.seed(42) n 2000 X1 np.random.normal(0, 1, n) X2 np.random.normal(0, 1, n) X3 np.random.binomial(1, 0.4, n) # 处理分配依赖协变量 true_logit 0.2 0.5 * X1 - 0.3 * X2 0.8 * X3 p_score 1 / (1 np.exp(-true_logit)) Z np.random.binomial(1, p_score) # 结局处理效应为1.2 Y 1.5 1.2 * Z 0.8 * X1 - 0.5 * X2 0.6 * X3 np.random.normal(0, 1, n) df pd.DataFrame({X1: X1, X2: X2, X3: X3, Z: Z, Y: Y}) print(df.groupby(Z)[[X1, X2, X3]].mean())你会看到两组协变量均值有差异这就是混杂存在的直观证据。接下来估计倾向得分我直接用逻辑回归起步注意只放混杂因素不放中介变量和工具变量——这是协变量选择的基本红线。covariates [X1, X2, X3] logit LogisticRegression(max_iter1000) logit.fit(df[covariates], df[Z]) df[e] logit.predict_proba(df[covariates])[:, 1] # 检查重叠打印倾向得分的分位数 print(df.groupby(Z)[e].describe().T)预处理阶段一定要看一眼倾向得分的分布。如果两组得分范围几乎没有交集那不管匹配还是加权都很悬——倒不是说不能做但结论必须限定在共同支持域内。重叠太差时强行给极端个体加权后面的权重会爆炸。3.2 第二步计算ATE权重并做归一化权重公式按前面写的实现。计算完直接检查权重分布尤其注意最大值和均值。df[w_ATE] np.where(df[Z] 1, 1 / df[e], 1 / (1 - df[e])) # 归一化权重除以均值让权重平均为1、总和等于样本量 df[w_ATE_norm] df[w_ATE] / df[w_ATE].mean() print(df.groupby(Z)[w_ATE_norm].describe().T)为什么建议做这一步理论上未归一化的IPTW估计量是无偏的但有限样本里极端权重容易带来很大的方差。Hajek估计量——也就是把所有加权和除以权重总和的形式——在有限样本下往往更稳。归一化权重正是把每个个体的权重按均值缩放到“平均权重为1”实际估计时等价于Hajek型估计量方差更小而且能避免某个权重特别大的个体凭空主导结果。我自己的习惯是样本量几千以内一律用归一化权重样本量特别大的时候两种权重差别基本可以忽略但归一化总不会错。3.3 第三步用标准化差异SMD检查平衡性平衡性检验是因果推断流程里的质检环节。很多人习惯只检验“差异是否显著”但这是个误区——样本量足够大时微小且无实际意义的差异也会被标为显著。所以我只推荐用标准化差异SMD看效应量大小不看p值。下面定义一个函数计算加权后的SMD以0.1为经验阈值。def weighted_smd(df, var, weight_colw_ATE_norm): treated df[df[Z] 1] control df[df[Z] 0] m1 np.average(treated[var], weightstreated[weight_col]) m0 np.average(control[var], weightscontrol[weight_col]) v1 np.average((treated[var] - m1) ** 2, weightstreated[weight_col]) v0 np.average((control[var] - m0) ** 2, weightscontrol[weight_col]) return (m1 - m0) / np.sqrt((v1 v0) / 2) print(加权前) for v in covariates: print(f{v}: SMD {weighted_smd(df, v):.4f}) # 直接拿全样本比较相当于没加权 print(加权后) for v in covariates: print(f{v}: SMD {weighted_smd(df, v):.4f})以我跑过的数据经验加权后协变量SMD普遍能降到0.1以内有些能压到0.05以下。如果还有协变量不达标两个思路一是换更灵活的倾向得分模型比如加交互项、样条项二是检查是不是有极端权重在捣乱配合下一节的稳定化处理一起解决。切记SMD检查必须在权重修正完成后重新做一遍顺序错了等于白查。4. 权重不修一下真不敢用极端权重、稳定化与截断4.1 权重爆炸的成因与后果权重公式本身很诚实倾向得分e(x)越接近0处理组个体的权重就越接近无穷大对照组e(x)接近1时同理。模拟数据里看不明显真实数据中经常出现倾向得分低到0.01的处理组个体它一个代表100个人只要它的结局稍微特殊一点就能把整体效应估计带偏。后果是两层的。第一层方差膨胀置信区间大得离谱效应估计失去精度。第二层估计结果被极少数“幸运偏差”个体绑架换一个样本点结论可能就变了。这跟调查加权里“权重大就天然有杠杆作用”的问题一模一样。我在实操中判断权重是否健康的标准很简单看一下权重最小值、中位数、99分位数如果99分位数是中位数的几十倍甚至上百倍那就要处理了。4.2 稳定权重乘上边际概率把权重拉回正常尺度一个在实际工作中用了就回不去的做法是稳定权重stabilized weights。公式长这样sw_i Z_i * P(Z1) / e(x_i) (1 - Z_i) * P(Z0) / (1 - e(x_i))和基础IPTW的差别是分子多乘了一个边际处理概率。效果很直观基础权重的分子是1当倾向得分很小时权重会非常大稳定权重分子是P(Z1)这个还不到1的数相当于给权重整体降了一个档次让权重的均值接近1、分布不再那么夸张。p_treat df[Z].mean() df[sw_ATE] np.where(df[Z] 1, p_treat / df[e], (1 - p_treat) / (1 - df[e])) print(df.groupby(Z)[sw_ATE].describe().T)稳定权重和归一化权重之间并不冲突。实际使用中我经常两个都算主分析用稳定权重归一化稳健性分析里换用原始IPTW结论一致那就说明没有被权重处理方式左右。4.3 截断是双刃剑方差小了目标人群也变了截断trimming是更激进的手段常见有两种做法。按倾向得分截断把e(x)小于0.05或大于0.95的样本直接删除。按权重截断把权重超过99分位数的样本强行压到99分位数的值。第一种在两组重叠度差的时候比较有效第二种专门对付单点霸王权重。但必须想清楚截断本质上是在主动放弃一部分样本它和匹配扔样本在精神上其实有相似处——换了方差也动了目标人群。截断后的ATE不再属于全人群而是属于“倾向得分处于某个区间的子人群”。所以我的建议是截断只作为敏感性分析出现不要作为主分析偷偷用。主分析用稳定权重或归一化权重然后在稳健性检验里换不同的截断阈值比如5%、10%看结论稳不稳。如果截断前后效应估计明显变化说明结论本身对权重处理方式敏感这个问题必须写进论文的局限性里绕是绕不过去的。5. 效应估计与诊断权重算完以后才进入下半场5.1 效应量怎么算加权均值差和加权回归权重算好、平衡性达标之后下一步才是估计处理效应。最简单的办法是对处理组和对照组的Y做加权均值差这是非参数思路。更常见的是加权线性回归把处理指示和协变量放进模型权重作为回归权重。这样做的额外好处是统计软件直接给出系数和标准误同时它带有一定的双重稳健性质——哪怕倾向得分模型略偏结局模型也能兜一部分底。X_design sm.add_constant(df[[Z, X1, X2, X3]]) wls_model sm.WLS(df[Y], X_design, weightsdf[sw_ATE]).fit(cov_typeHC1) print(wls_model.summary().tables[1])需要注意加权线性回归里的Z系数就是ATE的点估计。真实效应设定为1.2模拟数据跑出来通常会在1.2附近浮动置信区间能覆盖真实值这就说明整个流程没有系统性偏倚。5.2 稳健标准误是标配否则置信区间会偏窄提起权重很多人第一个担心是“要不要考虑权重本身是估计出来的”。这个问题在学术上很微妙忽略倾向得分估计的不确定性标准误会有轻微低估。但在实际工作中最要紧的不是这个而是异方差稳健标准误——因为权重本身会引入异方差普通标准误会严重失真。上面的代码用了cov_typeHC1这是三明治标准误的一个版本。如果你习惯R语言对应的是sandwich包里的vcovHC。还有更省心的选择是bootstrap直接把权重估计纳入抽样过程一步到位。我一直是主分析用HC1快速出结果最终定稿前用bootstrap复核。两者差距不大那结论就站稳了差距大先把极端权重问题查一遍再说。5.3 共支持诊断与Love plot可视化不能说谎统计量会撒谎图一般不会。我每个因果推断项目固定产出三张图倾向得分重叠直方图、加权前后SMD的Love plot、权重分布箱线图。import matplotlib.pyplot as plt # 倾向得分重叠 plt.figure(figsize(6, 4)) for z, label, color in zip([0, 1], [Control, Treated], [gray, firebrick]): subset df[df[Z] z] plt.hist(subset[e], bins30, alpha0.5, labellabel, colorcolor) plt.xlabel(Propensity score) plt.ylabel(Frequency) plt.legend() plt.show() # Love plot示意实际可以循环协变量画 import seaborn as sns smd_raw [weighted_smd(df, v) for v in covariates] smd_weighted [weighted_smd(df, v, sw_ATE) for v in covariates] fig, ax plt.subplots(figsize(5, 4)) y_pos np.arange(len(covariates)) ax.scatter(smd_raw, y_pos, labelRaw, colorgray) ax.scatter(smd_weighted, y_pos, labelWeighted, colorfirebrick) ax.axvline(0, colorblack, lw0.8) ax.axvline(-0.1, linestyle--, colorsteelblue) ax.axvline(0.1, linestyle--, colorsteelblue) ax.set_yticks(y_pos) ax.set_yticklabels(covariates) ax.set_xlabel(Standardized Mean Difference) ax.legend() plt.show()Love plot的核心就是看那些点有没有全部收进-0.1到0.1的通道里。比任何表格都直观。我遇到过一个数据集表格里所有SMD都通过了结果图一画出来就发现某个分类协变量在加权后被一个权重特别大的个体拉偏这种细节不看图根本发现不了。6. 匹配真的应该被扔掉吗我的决策框架6.1 什么时候匹配仍然可以接受前面批判了匹配那么多但我不认为匹配是绝对错误。任何一个做过真实数据分析的人都知道没有银弹只有适配。至少三类场景匹配依然是我的候选方案。第一研究问题明确要ATT。政策评估里经常问“这批已经参与培训的人受益多大”存量参与者的效果比全人群外推更有政策含义。第二处理组样本量极小且每个个体都珍贵。比如罕见病的真实世界研究总共就150个用药患者你还一个个做匹配去扔样本反过来这时候加权的极端权重风险也高需要谨慎。第三共同支持域严重不足处理组和控制组倾向得分分界线分明。此时加权会产生大量极端权重匹配反而通过显式限制在共同支持域里给出一个方差更小、解释也更直接的局部效应。此外匹配领域本身也在进化像全匹配full matching、遗传匹配genetic matching都比传统PSM好不少至少没有“扔掉样本”这个大毛病。但它们的普及度远不如加权审稿人认可度也见仁见智。6.2 我在实际项目里的决策顺序这些年跑下来的经验我给自己定了一套流程每次拿到观测数据先过一遍大脑再动手。第一步先明确研究问题想要ATE还是ATT。这个定不下来后面所有方法都是空中楼阁。绝大多数公共卫生、政策评估问题要的都是ATE那就直接锁定加权路线。第二步看共同支持域。倾向于得分的重叠对不对重叠差的话ATE本身就要打折这时候才考虑匹配去估一个诚实的ATT或者在论文里明确把结论限定在共同支持域内。第三步主分析用加权稳健性分析做矩阵式交叉。主分析用稳定权重归一化版IPTW配HC1标准误稳健性分析里我会换未归一化权重、换截断阈值、换一个倾向得分模型设定再看结论稳不稳。如果审稿人非要求匹配我把PSM结果放进稳健性附录同时注明它估的是ATT跟主分析的ATE不是同一个参数请勿直接比较。这样一套组合拳下来等于把“为什么不用匹配”这个问题提前化解了。审稿人更多会跟你讨论参数定义而不是质疑你方法选错。最后分享一个很朴素的实战体会做因果推断方法论洁癖没有意义意义来自研究问题本身。当你在报告里清楚写下“本研究关心的是全人群平均处理效应因此采用倾向得分加权”这句话本身就比任何方法都更能保护你。我自己所有项目现在都默认把IPTW作为主分析拿匹配结果和不做调整的原始比较放进稳健性附录这种主次分明的呈现方式这两年在审稿里没有因为方法选择再起过争议。