想比较吃药和不吃药的真实效果差异 详解PSM倾向得分匹配软件在SPSS和Stata及R语言中的操作步骤与避坑指南
你去医院开药,医生告诉你“这个药能降血压”。你转头问隔壁老王,他没吃这个药,血压也还行。这时候你心里一定会冒出一个问题:到底是药管用,还是人家本来身体底子就好?
直接拿“吃药组”和“不吃药组”的平均结果相减,看起来最省事,但在真实研究里几乎一定会翻车。因为 people 不是随机分组的。吃药的人往往年龄更大、基础病更多、收入更高、依从性更好;不吃药的人可能症状轻、怕副作用、或者经济条件有限。这些差异如果混在结果里,你以为看到的是“药效”,其实看到的是“人群差异”。
这就是为什么临床和流行病学里会频繁提到 PSM(Propensity Score Matching,倾向得分匹配)。它干的事情很简单:给吃药的人,在不吃药的人群里找一个“画像”几乎一样的对照组,然后再比较两者的结局差异。 听起来像找双胞胎,但实际比这严谨得多。
下面我把这件事拆开了讲:先搞懂它到底在算什么,再分别演示 Stata、R、SPSS 怎么跑,最后把那些让人半夜改代码的坑一个个列出来。全程带可运行代码,变量名用中文说明,尽量让零基础的人也能跟着操作。
一、PSM到底在算什么?用“选课”打个比方
假设你在大学里想比较“选了A老师的高数课”和“没选A老师”的学生期末成绩差异。直接比平均分没用,因为选A老师的可能本来就是学霸,或者早上第一节没人选。
倾向得分(Propensity Score) 就是:在给定一堆背景变量(性别、绩点、年级、是否兼职、高中数学基础)之后,这个人“选择A老师”的概率。概率越接近,说明两人越像。
PSM 的核心逻辑分三步:
- 算分:用逻辑回归(或机器学习)预测每个人吃药的概率。
- 配对:按概率高低,给每个吃药的人找一个不吃药但概率相近的人。
- 比较:在配对好的样本里算结局差异,这时候两组背景已经均衡了,差异更接近“吃药的真实效果”。
需要特别注意两个概念:
- ATT(Average Treatment effect on the Treated):关注的是“吃药的人”如果没吃药会怎样。绝大多数临床研究问的就是这个。
- ATE(Average Treatment Effect):关注的是整个人群,包括本来就不该吃药的人。
大多数药物评价选 ATT,因为政策制定和临床建议都是围绕“已经吃药或应该吃药的人”展开的。
二、先定一个真实案例,后面所有代码都围绕它
假设你手头有一项观察性队列研究,想评估一种新型降压药对收缩压下降值的影响。
| 变量名 | 含义 | 类型 |
|---|---|---|
treat |
是否吃药(1=吃药,0=不吃药) | 二分类 |
outcome |
服药3个月后收缩压下降值(mmHg) | 连续 |
age |
年龄 | 连续 |
bmi |
体重指数 | 连续 |
baseline_bp |
基线收缩压 | 连续 |
income |
月收入(千元) | 连续 |
exercise |
每周运动次数 | 离散 |
comorbidity |
合并症数量 | 离散 |
gender |
性别 | 二分类 |
我们的目标:估计 ATT,即“吃药组相比‘长得跟自己很像但不吃药’的那群人,血压多降了多少”。
⚠️ 重要提醒:PSM 只能消除已观测到的混杂因素。如果存在“患者自己决定吃药的心理动机”“医生私下判断病情轻重”等没录入数据库的变量,PSM 救不了你。这时候得考虑工具变量(IV)、断点回归(RDD)或双重差分(DID)。
三、Stata 实操:现代框架 teffects 一键搞定
如果你用的是 Stata 15 及以上版本,强烈建议用内置的 teffects 框架。它比老牌的 psmatch2 更规范,标准误计算也更合理。
3.1 基础匹配代码
* 导入数据(假设你的文件叫 drug_study.dta)
use drug_study.dta, clear
* 核心命令:teffects psmatch
* 第一个括号:结局模型 y = f(treat, covariates)
* 第二个括号:倾向得分模型 treat = f(covariates)
teffects psmatch (outcome) (treat i.gender age bmi baseline_bp income exercise comorbidity), ///
neardiff /// 最近邻匹配,默认1:1
caliper(0.05) /// 卡钳:倾向得分差超过0.05的不匹配
common /// 只保留共同支撑域内的样本
seed(12345) /// 固定随机种子,方便复现
verbose /// 打印详细过程
noreplace /// 不放回抽样(每个对照只用一次)
3.2 结果怎么看
运行完后,Stata 会自动输出:
- ATT 估计值:吃药组比匹配到的对照组多降的血压值。
- 95%置信区间 和 p值。
- 匹配前后协变量标准化均值差(SMD):这是判断匹配质量的金标准。
你可以通过以下命令查看平衡性表格:
estat psmatch, table
3.3 可视化检查
* 匹配前后倾向得分密度图
teffects psmatch (outcome) (treat i.gender age bmi baseline_bp income exercise comorbidity), ///
neardiff caliper(0.05) common
graph psmatch, density by(treat) title("匹配前/后倾向得分分布")
密度曲线重叠越多,说明匹配越好。如果吃药组集中在 0.7~0.9,不吃药组集中在 0.1~0.3,硬凑出来的匹配基本不可信。
3.4 常用参数速查
| 参数 | 作用 | 建议 |
|---|---|---|
neardiff |
最近邻匹配 | 最常用,1:1 或 1:k |
ratio(2) |
1:2 匹配 | 对照充足时提高精度 |
caliper(0.05) |
卡钳宽度 | 通常用倾向得分标准差的 0.2 倍,Stata 里直接写绝对值即可 |
common |
共同支撑域 | 必加,避免外推 |
noreplace |
不放回 | 临床常用,避免一个对照被反复使用 |
seed() |
随机种子 | 必须固定,否则结果不可复现 |
四、R 语言实操:MatchIt + cobalt 组合拳
R 做 PSM 是目前学术界最主流的选择,生态成熟,文献引用最多。我们分三步走:匹配 → 平衡性检查 → 效应估计。
4.1 安装与加载包
install.packages(c("MatchIt", "cobalt", "dplyr", "ggplot2"))
library(MatchIt)
library(cobalt)
library(dplyr)
library(ggplot2)
4.2 倾向得分匹配
# 假设数据框叫 df
set.seed(2024)
m1 <- matchit(
treat ~ age + bmi + baseline_bp + income + exercise + comorbidity + gender,
data = df,
method = "nearest", # 最近邻
ratio = 1, # 1:1
replace = FALSE, # 不放回
caliper = 0.2 * sd(logit(m1_dummy$treat)), # 卡钳:0.2倍logit倾向得分标准差
distance = "logit" # 对倾向得分做logit变换后再匹配,统计界推荐
)
# 提取匹配后的数据集
df_matched <- match.data(m1)
# 查看匹配摘要
summary(m1)
💡 注意:
caliper的单位必须和distance一致。如果你用logit变换,卡钳也应该基于 logit 尺度。直接写caliper = 0.05也可以,但建议用标准差的 0.2 倍,更符合 Imbens & Rubin (2015) 的推荐。
4.3 平衡性诊断(这是审稿人最爱盯着看的地方)
# 生成平衡性表格
bal <- bal.tab(m1, unstandardized = TRUE)
print(bal, threshold = 0.1) # 只显示 SMD > 0.1 的变量
# Love Plot:一眼看匹配前后标准化差异
love.plot(m1, binary = "standardized", thresh = 0.1)
判断标准:匹配后所有变量的标准化均值差(SMD)最好都小于 0.1,最大不超过 0.2。如果某个变量怎么都压不下来,说明这个混杂因素可能没被正确建模,需要换方法或补充变量。
4.4 估计真实效果
匹配完不等于结束,很多人直接拿 df_matched 跑 t 检验,这在统计上是错的。正确做法是在匹配样本上用加权或回归调整:
# 方法1:简单加权平均(ATT)
att_simple <- with(df_matched,
mean(outcome[treat == 1]) - mean(outcome[treat == 0]))
# 方法2:推荐做法,在匹配样本上跑线性回归,自动处理权重
fit <- lm(outcome ~ treat + age + bmi + baseline_bp, data = df_matched)
coef(fit)["treat"] # 这就是调整后的 ATT
# 方法3:用 sandwich 包计算稳健标准误(匹配后标准误通常会偏小,需要校正)
library(sandwich)
library(lmtest)
coeftest(fit, vcov = vcovHC(fit, type = "HC3"))
4.5 共同支撑域检查
# 画匹配前后的倾向得分分布
plot(m1, type = "d", transform = TRUE)
如果吃药组和不吃药组的得分曲线几乎没有重叠区域,说明共同支撑域不成立,匹配出来的结果是“编”出来的。这种情况要么缩小样本范围,要么换研究方法。
五、SPSS 实操:老实说,它不是 PSM 的主场
SPSS 没有官方内置的一键 PSM 命令。这不是 SPSS 不好,而是它的定位偏向描述统计和基础回归。市面上很多教程让你装什么 PSMatch 宏,代码老旧、文档缺失、报错难查。
我最推荐的 SPSS 工作流是两条路:
路径 A:SPSS 算倾向得分 + Python 集成调用 R 的 MatchIt(最稳)
SPSS 18 以上支持内置 Python 扩展,可以直接调用 R 环境。步骤如下:
* 第一步:在SPSS里跑逻辑回归,保存倾向得分
LOGISTIC REGRESSION VARIABLES treat
/METHOD=ENTER age bmi baseline_bp income exercise comorbidity gender
/SAVE=PS(Prob_Treated).
* 第二步:用Python集成直接调用R的MatchIt
PYTHON PROGRAM.
import spss
import spssaux
import pandas as pd
# 读取当前SPSS数据集
data = spssaux.DataDict()
df = spss.GetDataset().LoadDataFrame()
# 调用R环境执行MatchIt(前提:SPSS已配置R interpreter)
import rpy2.robjects as robjects
from rpy2.robjects import pandas2ri
pandas2ri.activate()
robjects.r('''
library(MatchIt)
library(cobalt)
m <- matchit(treat ~ age + bmi + baseline_bp + income + exercise + comorbidity + gender,
data = df, method = "nearest", caliper = 0.2*sd(logit(treat)), common = TRUE)
out <- match.data(m)
write.csv(out, "matched_spss.csv", row.names = FALSE)
''')
print("匹配完成,已保存为 matched_spss.csv")
END PROGRAM.
📌 前提条件:你需要在 SPSS 的
Edit → Options → Python中正确配置 R 解释器路径。如果配置失败,建议直接把数据导出为.csv,用 R 或 Stata 跑。
路径 B:纯 SPSS 语法手动实现 1:1 最近邻匹配(适合小样本教学)
如果你不想折腾 Python,可以用 SPSS 语法硬写一个简化版。注意:这只适合学习原理,不建议用于正式论文。
* 1. 逻辑回归算倾向得分
LOGISTIC REGRESSION VARIABLES treat
/METHOD=ENTER age bmi baseline_bp income exercise comorbidity gender
/SAVE=PS.
* 2. 排序
SORT CASES BY treat PS.
* 3. 生成匹配索引(简化版:按PS排序后,治疗组第i个匹配对照组第i个)
COMPUTE group_flag = 0.
IF treat = 1 group_flag = 1.
IF treat = 0 group_flag = 0.
RANK VARIABLES=PS BY group_flag (A) /RANK INTO rank_ps.
* 4. 配对:治疗组rank=1找对照rank=1,以此类推
SORT CASES BY group_flag rank_ps.
* 5. 用MATCH FILES拼接
DATASET NAME Treat.
SELECT IF group_flag = 1.
SAVE OUTFILE='treat_only.sav'.
DATASET NAME Control.
SELECT IF group_flag = 0.
SORT BY rank_ps.
SAVE OUTFILE='control_sorted.sav'.
DATASET ACTIVATE Treat.
MATCH FILES FILE=* /TABLE='control_sorted.sav' /BY rank_ps.
EXECUTE.
* 6. 删除无匹配的对照
DELETE CASES IF MISSING(outcome) AND group_flag = 0.
* 7. 计算ATT
MEANS TABLES=outcome BY group_flag /CELLS=MEAN COUNT.
这段代码能跑通,但有几个致命缺点:
- 没有卡钳过滤,可能把倾向得分差很远的人硬凑一起。
- 没有放回/不放回控制。
- 标准误没有校正。
- 不适合大样本。
我的建议:用 SPSS 做数据清洗和逻辑回归没问题,但 PSM 匹配这一步,导出到 Stata 或 R 再跑,省心且可信。审稿人也更认 R/Stata 的输出。
六、真实翻车现场:7 个最常见的 PSM 避坑指南
下面这些坑,我一个一个说过,基本覆盖了临床和社科研究里 90% 以上的 PSM 报错或拒稿原因。
坑 1:把“吃药后的指标”塞进协变量
比如你研究吃药对血压的影响,却把“服药1个月后的血压”当作协变量。这叫后处理变量(Post-treatment variable),会阻断因果链,让药效被人为压低甚至反转。
✅ 正确做法:协变量必须是基线特征或治疗前的状态。画一个 DAG(有向无环图)先理清因果关系,再决定放哪些变量。
坑 2:只看 p 值,不看 SMD
很多初学者跑完匹配,看到回归 p<0.05 就高兴。但匹配的质量不看 p 值,看标准化均值差(SMD)。p 值受样本量影响极大,样本大了哪怕微小差异也显著。
✅ 标准:匹配后 SMD < 0.1 为良好,< 0.2 可接受。超过 0.2 的变量必须继续调整。
坑 3:卡钳(Caliper)乱设
卡钳太宽,匹配到“不像”的对照;卡钳太窄,大量样本被丢弃,最后只剩几十个有效样本。
✅ 推荐:使用 logit 倾向得分标准差的 0.2 倍。Stata 里直接写 caliper(0.05) 或 caliper(0.2*sd(logit_treat))。R 里用 caliper = 0.2 * sd(logit(m$distance))。
坑 4:忽略共同支撑域(Common Support)
如果吃药组倾向得分集中在 0.8,不吃药组集中在 0.3,两者几乎没有重叠。这时候强行匹配,相当于拿“年轻健康人”去配“高龄重症人”,结果没有外部有效性。
✅ 做法:匹配前画密度图,用 common 参数过滤,报告丢失样本数和原因。
坑 5:匹配完直接跑 t 检验,标准误偏小
匹配会改变样本结构,普通 t 检验或 OLS 的标准误会低估真实变异,导致 p 值虚低。
✅ 做法:在匹配样本上用加权最小二乘(WLS),或使用 Bootstrap 重采样计算置信区间。R 里可以用 clubHC 或 sandwich 包。
坑 6:样本量暴减却不报告
匹配后从 1000 人变成 300 人,结果显著。审稿人会问:那 700 人被丢到哪去了?为什么丢?有没有系统性偏差?
✅ 做法:在论文里明确报告:
- 初始样本量
- 剔除共同支撑域外的样本数
- 匹配后有效样本量
- 匹配前后 SMD 对比表
- Love Plot
坑 7:把 PSM 当万能药,不做敏感性分析
PSM 只解决可观测混杂。如果医生开药是基于你没录入的“病情严重程度直觉”,PSM 再漂亮也是伪因果。
✅ 补救措施:
- Rosenbaum 敏感性分析:检验隐藏偏差多大才会推翻结论。
- 更换匹配方法:核匹配、半径匹配、最优匹配,看结果是否稳健。
- 安慰剂检验:把结局变量换成治疗前就该存在的指标,理论上不应该有影响。
- 加入更多协变量:尤其是临床指南里明确的风险因子。
七、结果怎么写进论文/报告?给个模板
不要写成“我们用了PSM,结果显示吃药有效”。这种话审稿人看了直接打回。
推荐结构:
为减少选择偏倚,本研究采用倾向得分匹配(PSM)方法构建对照。基于年龄、性别、BMI、基线收缩压、收入、运动频率及合并症数量建立 Logit 模型估算倾向得分。采用 1:1 最近邻匹配,卡钳设为 logit 倾向得分标准差的 0.2 倍,并限制在共同支撑域内。匹配后所有协变量标准化均值差均小于 0.1(SMD 范围:0.02~0.09),Love Plot 显示匹配后两组分布高度重叠(图1)。共纳入 412 例吃药患者及 412 例匹配对照,较初始样本流失 18.7%,主要因共同支撑域外所致。
匹配后线性回归显示,服药组收缩压额外下降 6.8 mmHg(95%CI: 4.1~9.5, p<0.001)。Rosenbaum 敏感性分析表明,隐藏偏差系数 Γ 需达到 1.35 才能推翻该结论,提示结果对未观测混杂具有一定稳健性。
这段话里有数字、有方法、有诊断、有局限,比任何“显著”都值钱。
八、几个容易被忽略但极其实用的细节
1. 类别变量怎么处理?
性别、是否吸烟这类变量,在逻辑回归里要转成虚拟变量。R 的 MatchIt 会自动识别因子类型,Stata 用 i.gender。千万别把分类变量当连续变量扔进去。
2. 连续变量要不要标准化?
倾向得分模型里不需要标准化,逻辑回归会自己处理量纲。但匹配后画 Love Plot 时,cobalt 默认输出标准化差异,这正好。
3. 匹配后还能加协变量吗?
可以,而且推荐。匹配主要平衡背景变量,但结局模型里加入关键协变量可以进一步提高精度(Cochran & Rubin, 1987)。只是不要加治疗后变量。
4. 1:1 不够用怎么办?
如果对照充足,可以用 1:2 或 1:3 匹配,能提高统计功效。但要注意:1:k 匹配的标准误计算更复杂,R 里 MatchIt 会自动处理权重,Stata 的 teffects 也会。
5. 机器学习和 PSM 能结合吗?
能。如果协变量很多、非线性关系复杂,可以用随机森林或 XGBoost 预测倾向得分,再送入匹配。R 的 WeightIt 包支持 method = "ps" 配合 model = "xgboost"。但初学者先用 Logit,稳定且可解释性强。
九、最后说几句掏心窝子的话
PSM 不是魔法棒,挥一下就能得到“真实因果”。它更像一把粗糙但实用的手术刀:切掉明显的混杂组织,剩下的结果才更接近真相。
如果你在做药物效果比较,我建议你按这个顺序走:
- 先画 DAG,搞清楚哪些是混杂、哪些是中介、哪些是碰撞变量。
- 用 SPSS/Stata/R 跑出倾向得分,画密度图看重叠情况。
- 匹配后必须检查 SMD 和 Love Plot。
- 换一种匹配方法或卡钳宽度,看结果变不变。
- 如果结果对卡钳极度敏感,老实告诉读者:证据有限,需要随机对照试验验证。
研究不是比谁跑得快,是比谁敢承认边界在哪。能把 PSM 跑通的人很多,能把匹配质量、样本流失、敏感性分析、局限性全摆上台面的人,才是真正做因果推断的人。
代码都给你了,数据格式对齐,变量名改成本研究的,跑一遍看看密度图。如果曲线重叠得好看,SMD 压下去,结果自然经得起推敲。有任何报错或结果异常,把日志贴出来,咱们一步步排查。