
1. 为什么你需要一套“有程序”的PSM方案做数据分析或因果推断的朋友大概率都遇到过这样的场景你想评估某件事的“效果”但手里只有观测数据没有随机分组。比如我早年做用户运营分析时想判断“参加过训练营的用户比没参加的用户留存率高出多少”直接一算训练营用户留存率高出20%但这个数根本不敢写进汇报里——因为不是训练营带来了留存而是本来就更活跃、更爱学习的人更倾向于报名训练营。这就是典型的“自选择偏差”。筛掉这个偏差靠的就是倾向得分匹配PSMPropensity Score Matching。它的核心任务是把“无法随机分组”的现实尽可能还原成“近似随机分组”的样子先算出每个样本“接受处理”的概率倾向得分然后在控制组里找到得分接近的个体一个个“配平”让两组在可观测特征上尽量可比最后再去比较结果差异。用这套方法你需要解决的不只是“点到为止”的平衡性检验更是一整套从数据清洗、模型设定、匹配执行到效果估计与稳定性验证的流程。网上关于PSM的资料非常零散绝大多数帖子只讲概念不讲“程序”或者只贴一段R代码、没有任何操作注释跑起来才发现处处是坑。这篇文章我就从实际操作出发把手写PSM的每一个步骤、每一段核心Python代码、每一个常被忽略的细节讲透。无论你是刚接触因果推断的科研党、写毕业论文的学生还是做业务数据分析、用户增长、政策评估的从业者这篇文章都能给你一套能直接拿去改用的工具。2. PSM怎么用才不算用错2.1 一个核心公式和它的适用边界倾向得分匹配的本质是用一个“一维综合指标”化解多维协变量失衡的难题。Rosenbaum和Rubin在1983年提出了倾向得分定义给定一组协变量 X个体接受处理 T1比如参加训练营的概率是e(X) P(T1 | X)匹配之后我们真正想估计的是“处理组平均处理效应”ATTAverage Treatment Effect on the TreatedATT E[ Y(1) − Y(0) | T1 ]其中 Y(1) 是处理组个体接受处理后的结果Y(0) 是它“如果没有接受处理”的结果——但后者观测不到所以需要从控制组里找替身。匹配解决的就是如何给每个处理组个体找到最像的“反事实替身”。但PSM的成立必须同时满足两条关键假设可忽略性Ignorability / Unconfoundedness给定协变量 X 之后个体是否接受处理与实际结果无关。换句话说所有影响分组的因素我们都观测到了、也放进模型里了。那些没观测到的比如一个人的潜在毅力、隐藏偏好就得交给敏感性分析来补。共同支撑Common Support两组样本的倾向得分需要有足够大的重叠区间。如果处理组的得分普遍在0.8以上控制组普遍在0.3以下没有重叠匹配就无从谈起。理解PSM最强的场景是“观察性数据下的选择偏差校正”。但用之前也必须泼一盆冷水PSM不能解决“用变量选漏了”的问题。它只对“已观测到的协变量”进行配平没观测到的变量依然会带来偏倚。这一点我在无数论文里看到被含糊带过实际操作中却必须自己心里有数。2.2 什么时候该用PSM什么时候不该用判断一个场景该不该上PSM只看三件事。第一你是否明确有“处理组”和“控制组”两个分离的群体或状态。例如“参加了培训/没参加”“上线了新功能/没上线”“发放了优惠券/没发放”。如果没有明确的二分处理变量PSM直接失效。第二你是否面临两组样本在背景特征上系统性失衡。如果两组本来就分布均衡就直接用回归或t检验上PSM反而是给自己找麻烦。第三你是否能“合理说出”影响人群分组的变量都在数据里。比如做电商干预分析用户的活跃度、历史购买频次、最近浏览时长、渠道来源这些变量必须有。变量不够做了也白做。不该用PSM的场景也很典型分组的决定因素里存在较强的未观测因素。举个例子用户因为“喜欢某个新功能”才主动使用而“喜欢”这个态度变量你不可能量化到数据里那PSM估计出来的效果仍然有偏。这种情况下你应该考虑工具变量、断点回归或者做DID双重差分法而不是强行匹配。提示PSM和DID经常被一起用叫PSM-DID。思路是先匹配、再用匹配后样本做双重差分用来对付时间趋势和部分时不变遗漏变量。这不是本文的重点但如果你刚好在评估一项政策或一次营销活动的纵向效果值得在跑完基础PSM后进一步探索。3. 动手之前你把模型和数据准备到位了吗3.1 协变量选择的“三条纪律”很多新手跑PSM上来就是对数个变量猛跑逻辑回归最后平衡性不过关又不知道该加谁、减谁。我在实操中总结出三条铁律。第一协变量必须是“分组之前”或“处理之前”的变量。处理后的中介变量比如参加了训练营之后的活跃时长绝对不允许进模型否则等于有可能部分吸收了处理效应既偏又乱。第二协变量应当同时和处理状态、结果变量相关。只和分组相关、跟结果无关的变量会无谓降低匹配效率只和结果相关、跟分组无关的变量进不进模型影响不大但放了也无大碍有时还能提高估计精度。实操上我一般从“直觉上可能影响分组的背景变量”入手优先保留基础人口学特征、历史行为变量、渠道来源、地域、设备等。第三千万别贪多。一个常见的误区是以为变量越全面越好结果模型越跑越臃肿倾向得分趋近0或1导致样本区间严重重叠不足。在业务场景里我一般控制协变量在10~20个之间学术研究里也要先靠领域知识筛选出真正“重要”的变量而不是把上百个变量一股脑塞进去。3.2 数据清洗的几条硬性要求准备PSM所需的数据集时有几件脏活必须提前干完否则后期每一个都在等你的“回炉重造”。处理缺失值倾向得分逻辑回归模型不允许缺失值尤其是连续变量。业务数据的缺失率如果低于5%可以直接用中位数或众数填充高于10%需要认真考虑是不是“缺失本身”就是一个重要信息例如用户没填年龄也许因为年龄段的某种特征可以专门造一个“是否缺失”的哑变量。处理类别变量把类别变量转为哑变量。但注意类别层级过多比如城市有几百个时建议先做粗聚类否则模型自由度消耗过多分组变量的“分辨力”也不稳定。处理离群值倾向得分逻辑回归对极端值敏感。像消费金额这种高度右偏的变量实操上建议做对数变换或者缩尾处理99%分位缩尾防止一两个超级用户把模型的拟合方向带偏。3.3 处理组与控制组的规模比例PSM最舒服的样本结构是控制组规模大于处理组最好能到2~3倍以上。因为控制组是“候选池”池子越大给处理组每个个体找到相近“替身”的机会越多匹配质量越高。如果控制组数据非常少也不是完全不能做但需要调整匹配策略比如采用“卡尺内最近邻匹配”“带放回匹配”并且在最后的平衡性诊断上多花精力。如果看过匹配后的样本标准化偏差异常高那结论就要谨慎再谨慎。4. 核心程序逐步拆解从逻辑回归到匹配执行4.1 先造一份仿真数据为了让你能原样跑通整套逻辑我造了一份贴近业务场景的仿真数据10000条观测1000个处理组、9000个控制组故意模拟常见的失衡结构协变量包含年龄、历史消费金额、在线时长、渠道来源、会员等级和城市等级这6个维度。处理组的分配概率与这些变量有关——也就是说数据里天然存在选择偏差。import numpy as np import pandas as pd np.random.seed(42) n 10000 data pd.DataFrame({ age: np.random.randint(18, 60, n).astype(float), log_spend: np.random.normal(5, 1.5, n), # 销售额的对数值 online_minutes: np.random.normal(80, 40, n), channel: np.random.choice([paid, organic, social], n, p[0.3, 0.5, 0.2]), member_level: np.random.choice([1, 2, 3], n, p[0.5, 0.3, 0.2]), city_tier: np.random.choice([1, 2, 3], n, p[0.4, 0.35, 0.25]), }) # 构造处理变量实际分组受年龄、消费、渠道、会员等级影响 logit_ps ( -2.5 0.03 * (data[age] - 40) 0.45 * (data[log_spend] - 5) 0.015 * (data[online_minutes] - 80) np.where(data[channel] paid, 0.7, 0) np.where(data[channel] social, 0.3, 0) np.where(data[member_level] 2, 0.4, 0) np.where(data[member_level] 3, 0.8, 0) ) ps_true 1 / (1 np.exp(-logit_ps)) data[treatment] np.random.binomial(1, ps_true)注意我在生成数据时就固定了一个真实的倾向得分生成机制。现实中我们看不见这个机制这正是PSM需要估计的原因。4.2 用逻辑回归估计倾向得分估计倾向得分业界标准做法是“处理变量对协变量跑Logistic回归”得到的预测概率就是倾向得分。为什么是Logistic回归而不是线性回归因为处理变量是0/1二分类线性回归的拟合值可能跑到0~1范围之外且残差不满足正态、方差齐性等假设。虽然线性概率模型LPM在某些场景也能给出近似结果但逻辑回归天然把输出限制在(0,1)区间根本逻辑与“概率”匹配。import statsmodels.api as sm # 分类变量哑变量化 X pd.get_dummies( data[[age, log_spend, online_minutes, channel, member_level, city_tier]], drop_firstTrue, ).astype(float) y data[treatment] X sm.add_constant(X) model sm.Logit(y, X).fit(dispFalse) data[ps] model.predict(X) print(data[ps].describe())跑完你会看到处理组平均倾向得分明显高于控制组这是自选择偏差的直接体现。此时千万别急着匹配先做一件事检查倾向得分的分布和重叠区间。我常用一个简单可视化的方法分别打印处理组、控制组ps的分位数看有没有明显的“断头”。print(data[data[treatment] 1][ps].quantile([0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99])) print(data[data[treatment] 0][ps].quantile([0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99]))如果两组分位数完全错开那说明共同支撑几乎不存在匹配形同虚设。正常情况应当有比较明显的重叠区域。4.3 共同支撑区间检查“共同支撑”听起来像是学术黑话用大白话说就是你的得分区间有没有重叠。如果某个处理组个体的ps是0.95但控制组里ps最高才0.6你在控制组根本找不到“像它”的人拿一个0.6的人冒充它的反事实误差非常大。常规做法是取处理组ps的最小值作为左边界控制组ps的最大值作为右边界或者反过来再取交集只保留落在区间内的样本。另一种更稳健的做法是“修剪trimming”删掉两组ps分布尾部的极端样本一般修剪1%~5%。比如删掉处理组ps最低的2%和控制组ps最高的2%匹配质量往往会明显提升。4.4 匹配方式选型最近邻、卡尺、核匹配怎么选PSM执行的核心是“匹配算法”。这里我给出Python里可手写的三个实现思路最近邻匹配Nearest Neighbor Matching, NNM为每个处理组样本找ps值最接近的控制组样本。用“是否放回”再分成两类。不放回意味着一个控制组样本只能用一次好处是匹配结果更“干净”坏处是可能为了凑对而拉远距离放回则允许一个控制组样本匹配多次匹配距离更小但重复使用的样本会降低有效样本量。卡尺匹配Caliper Matching在最近邻基础上加上距离限制只有ps之差小于某个阈值才允许匹配。卡尺通常取“倾向得分标准差的20%”这个经验规则出自Rosenbaum和Rubin的研究也有人取0.02、0.05这样的绝对值。为什么是20%标准差它本质上是平衡“匹配偏差”和“方差”的折中卡尺太小匹配成的对子太少方差变大卡尺太大对子差距大偏差上身。这个阈值在实操中一定要反复试。核匹配与局部线性回归匹配不用“一对一”的硬配对而是给所有控制组样本加权权重取决于ps距离。它的好处是样本利用率高匹配后信息损失少。缺点是权重极端时标准化偏差的诊断更麻烦且并不总是优于前两者。先说结论在实际项目中我最常用的是“卡尺0.02 最近邻放回”组合。不放回在业务数据里经常会损失太多处理组样本导致结论样本范围大幅变化放回配上小卡尺既能控制距离又能保证配对数量。论文写作场景里建议同时报告最近邻1:1、1:4、卡尺和核匹配的结果作为稳健性检验。from sklearn.neighbors import NearestNeighbors treated data[data[treatment] 1].copy() control data[data[treatment] 0].copy() # 带卡尺的最近邻匹配放回卡尺设为 0.02 caliper 0.02 control_ps control[ps].values.reshape(-1, 1) nn NearestNeighbors(n_neighbors1, metriceuclidean) nn.fit(control_ps) matched_indices [] for i, row in treated.iterrows(): dist, idx nn.kneighbors([row[ps]], return_distanceTrue) if dist[0][0] caliper: matched_indices.append((i, control.iloc[idx[0][0]].name, dist[0][0])) matched_treated_idx [pair[0] for pair in matched_indices] matched_control_idx [pair[1] for pair in matched_indices] matched pd.concat([treated.loc[matched_treated_idx], control.loc[matched_control_idx]])跑完这段代码你会得到一个“配对完成”的数据集。这里我特意处理了去重不彻底的问题放回匹配意味着同一个控制组样本可能被重复匹配多次但匹配后计算时建议按“处理组每个个体一套唯一对照”来保存而不是把重复样本展开成多行。如果按重复展开相当于同一个控制组个体的结果被数了好几遍会低估标准误。5. 匹配完成了还不够平衡性检验才是灵魂5.1 标准化偏差如何计算与判读匹配做得好不好不看过程看结果。结果的核心指标叫“标准化偏差”d (mean处理组协变量 - mean控制组协变量) / sqrt((s²处理组 s²控制组) / 2)分母是两组方差均值的平方根用来消除量纲影响。业界通用经验阈值是匹配后所有协变量的标准化偏差绝对值不超过10%更严格的说法是5%。超过这个值说明这个变量在两组间仍有差异匹配没达到预期。用手写计算其实很简单def compute_std_diff(df, col, treat_coltreatment): t df[df[treat_col] 1][col] c df[df[treat_col] 0][col] pooled_std np.sqrt((t.var() c.var()) / 2) return (t.mean() - c.mean()) / pooled_std for col in [age, log_spend, online_minutes, member_level, city_tier]: before compute_std_diff(data, col) after compute_std_diff(matched, col) print(f{col}: before{before:.4f}, after{after:.4f})注意匹配前很多变量偏差较大比如年龄偏差可能到0.3以上匹配后需要大幅回落。如果某个变量匹配后仍然偏差高于0.1你需要复盘要么是这个变量与分组的关系太强最近邻卡尺不够用了要么是控制组样本量不足。5.2 除了标准化偏差还要看哪些指标业界完整报告里通常同时汇报三件事各协变量匹配前后的标准化偏差对比配一张Love Plot最好看匹配前后各变量的t检验p值。但我要提醒一句p值只能辅助参考它受样本量影响太大。样本量大了任何细微差异都容易显著样本量小了再大的差异也可能不显著。所以p值不应该作为最终判据标准化偏差才是。匹配后处理组和控制组的倾向得分分布重合度。如果匹配后两组ps分布还是明显错位说明匹配策略失败。Love Plot的Python实现也不复杂用matplotlib画一个分散点图即可import matplotlib.pyplot as plt cols [age, log_spend, online_minutes, member_level, city_tier] std_before [compute_std_diff(data, c) for c in cols] std_after [compute_std_diff(matched, c) for c in cols] fig, ax plt.subplots(figsize(8, 6)) y np.arange(len(cols))[::-1] ax.scatter(std_before, y, labelBefore Matching, markero) ax.scatter(std_after, y, labelAfter Matching, markers) ax.axvline(0, colorblack, lw0.8) ax.axvline(0.1, colorred, linestyle--, lw1) ax.axvline(-0.1, colorred, linestyle--, lw1) ax.set_yticks(y) ax.set_yticklabels(cols) ax.legend() ax.set_xlabel(Standardized Difference) plt.tight_layout() plt.show()5.3 匹配质量不过关怎么办如果平衡性检验没通过我按经验给你三个排查步骤。第一步检查样本重叠。如果大量处理组样本因卡尺过小被丢弃导致匹配后样本量大打折扣先把卡尺放宽比如0.05、0.1重跑。匹配算法本质是一個權衡匹配距离和有效样本量互为代价。第二步检查逻辑回归模型设定。是不是漏了重要的交互项或非线性项比如“年龄×是否为付费渠道”可能才是分组的真实驱动因素只放主效应永远拟合不好。这时候手动给倾向得分模型加交互项或多项式项往往立竿见影。第三步考虑换协变量集合或换匹配方法。有时问题不在算法而在于某个协变量本身区分度太高比如历史消费几千元以上的用户几乎全是处理组这种情况再合适的匹配也没法“无中生有”地造出对照组。如果样本充足宁可剔除这个“绝杀性变量”也绝不强行保留。6. 匹配后如何估计处理效应与稳健性检验6.1 匹配样本上的ATT计算匹配完成后直接对匹配后样本再跑回归得到的系数就是ATT。但注意样本已经不是随机抽样的原始样本而是“匹配后的样本”此时回归模型里的协变量可加可不加。加上协变量有好处既能修正残余的微小不平衡又能降低结果变量的残差方差估计精度更高。我一般习惯“配对后回归仍保留所有协变量”这也是许多顶刊在应用PSM时的标准做法。import statsmodels.formula.api as smf # 构造处理效应所需的结果变量仿真处理组真实效应 3 data[outcome] ( 10 3 * data[treatment] 0.05 * data[age] 0.2 * data[log_spend] np.random.normal(0, 1, n) ) # 回归估计结果 ~ 处理 协变量 matched_reg smf.ols( outcome ~ treatment age log_spend online_minutes member_level city_tier, datamatched, ).fit() print(matched_reg.params[treatment])此外还有个细节如果使用放回匹配同一个控制组个体被重复使用时匹配后样本并不是独立的直接跑普通回归会低估标准误。恰当做法是使用聚类稳健标准误聚类到“控制组个体ID”层面。上面代码里为了讲解简洁没有展示但项目实操时务必加上。6.2 敏感性分析面对未观测混杂的最后一道防线PSM始终受“未观测混杂因素”威胁。比如我们做的运营分析用户本身的学习意愿可能同时影响报名和留存而这个变量没有被测量。审稿人或者业务方常会问“你凭什么说没有隐藏变量”常见应对是做个简单的敏感性分析思路是假设存在一个未观测变量U它同时影响分组取处理与不处理的几率比Gamma值和结果要强到什么程度才能推翻当前结论行业里最简单的工具是R的rbounds包和Stata的psensitivity命令。Python生态里目前没有特别成熟的现成包所以一个“手动但讲逻辑”的替代方案是把某个你猜可能“漏掉的代理变量”硬塞进倾向得分模型观察结果是否大幅变化。如果结果依然稳健至少说明常见遗漏变量对结论的扰动有限。6.3 报告结果时这些信息必须交代不论是写论文还是写分析报告PSM结果至少要交代这些要素倾向得分模型的具体设定哪些协变量、是否含交互项、样本量匹配方法最近邻还是卡尺卡尺多大是否放回1:1还是1:n共同支撑区间处理修剪了多少样本匹配前后平衡性对比表匹配后的样本量效应估计值及其标准误稳健性检验结论只报一行“匹配后ATT为3.2”的报告或论文质量基本可以被直接打回。以上任何一项缺失读者都无从判断你的匹配到底做得好不好。7. 常见报错与避坑经验速查我在跑PSM的全流程里遇到过不少看着眼熟的问题统一整理成速查表给你。问题常见原因排查与解决逻辑回归不收敛协变量中存在完全分离现象某一变量组合下处理变量全是1或全是0检查交叉表删除完全分离的变量或做惩罚回归Firth回归匹配后处理组只剩几十人卡尺设得太小或共同支撑区间太窄调大卡尺改用不放回最近邻修剪尾部数据平衡性检验中某个分类变量的Dummy不平衡分类变量层级太多稀疏类导致匹配不上一对粗分类合并层级去掉极少数类样本匹配后ATT与简单均值差差异巨大匹配前的差异主要是混杂导致的假象重视匹配后结果检查匹配后分布是否合理别急着质疑算法无法区分匹配效果和结果模型设定两组在协变量上仍不平衡回归又引入复杂非线性项以平衡性诊断为主不要指望复杂回归补救使用了放回匹配但标准误未处理同一控制组样本重复使用未聚类改用聚类稳健标准误分层到控制组个体ID还有几个实操细节我踩过之后想特别提醒。用Python的pymatch库要留意它很久没更新了在较新的Python版本下运行时可能报依赖错误。这时候别硬刚库直接用statsmodelsNearestNeighbors手写也不复杂就是上面代码那几行的事。scikit-learn的逻辑回归LogisticRegression不带p值做论文需要显著性检验时务实用statsmodels Logit。数据量很大的时候做“1:1不放回最近邻”可以使用排序双指针方式比两层循环快得多。上面为了可读性用了NearestNeighbors的朴素写法但n几十万时建议把控制组ps先排序再用np.searchsorted找最近的位置。匹配前先把数据随机打乱避免原始数据排序带来的顺序偏差。我有一次没打乱数据匹配结果偏差水平时好时坏后来发现是数据按城市排序、匹配过程产生路径依赖。打乱一次就稳了。8. 一个完整的PSM流程清单给看完整篇文章的朋友总结成可以直接照做的checklist明确处理变量、结果变量和协变量集合协变量必须是处理前的变量。数据清洗缺失值、哑变量、离群值处理。用Logistic回归估计倾向得分并检查ps分布与重叠区间。选择匹配方式推荐先跑“卡尺0.02 最近邻 放回”作为基准方案再换1:1不放回和核匹配做稳健性。检查标准化偏差是否所有变量|d|0.1不达标就调整模型设定或匹配参数。在匹配样本上估计ATT加上协变量回归并处理聚类稳健标准误。报告共同支撑、匹配前后平衡性表格以及稳健性检验。回到我自己的经验PSM不是“跑完就交差”的工具它是一个从数据到假设再到估计的完整闭环。最容易出问题的阶段其实不是“匹配”这个算法动作而是匹配前后的诊断与调参过程。第一次跑出来平衡性不过关时不要慌那反而是对变量和数据理解的开始。后面每跑通一次你对自己数据的了解也会上一个台阶。最后再补充一个我常用的经验无论用什么匹配方式都要把匹配后的标准化偏差表和Love Plot存下来。这不仅是论文审稿的“通行证”也是业务汇报里说服对方“我们的两组确实可比了”的最直观证据。数据从不撒谎但你对数据的处理方式可能会骗过自己所以每一个细节都值得较真。