The V Lab
适用对象公共卫生、流行病学、医学与健康数据科学学习者
学习时长约 150–210 分钟
先修要求回归、置信区间、因果推断基础与基本 R 语法

本页是 PSM 实务深入篇 本教程聚焦 propensity score matching(PSM)的设计、执行和诊断。潜在结局、目标试验、DAG、标准化、IPW 与 AIPW 的完整框架见 因果推断详解。这里不会把各种方法重复一遍,而是把一个 PSM 分析从问题定义完整走到报告。

关于模拟数据 全部个体、治疗、结局与反事实真值均由固定随机种子生成,不含真实个人健康信息。实际研究中无法同时观察同一个人的两个潜在结局;本页展示“真值”只是为了检验分析流程,不能视为现实证据。

如何使用本教程

推荐按以下顺序学习:

定义因果问题与 estimand → 锁定治疗前变量 → 估计倾向评分 → 匹配 → 检查重叠、样本流与平衡 → 冻结设计 → 分析结局 → 做敏感性分析与报告

这是一个“先设计、后看结局”的工作流。所有主要代码只依赖 R 自带功能、MatchIt 与渲染页面所需的 knitr。页面中的结局变量是连续型 6 个月临床指标,数值越低越好。

学习目标

完成本教程后,你应能够:

  • 解释 propensity score 及其平衡作用,而不把它误认为因果效应;
  • 区分 ATE、ATT、ATC,并说明匹配失败或丢弃样本如何改变目标人群;
  • 写出一致性、可交换性、正值性与无干扰假设;
  • 根据时间顺序与因果结构选择变量,避开治疗后变量和碰撞点;
  • 使用 glm() 理解 PS 模型,并用 MatchIt::matchit() 完成 1:1 最近邻匹配;
  • 正确解释 link = "linear.logit"、0.2 SD caliper、replacement 与 ratio;
  • 审计匹配前后样本流、倾向评分重叠、SMD、方差比与 eCDF;
  • 使用 subclass 保留配对结构,估计匹配样本 ATT 及置信区间;
  • 区分设计诊断与结局分析,不用显著性或结局挑选匹配方案;
  • 将 PSM 迁移到二分类、计数和时间结局,并识别所需的不同推断方法;
  • 说明未测量混杂、缺失数据和时间变化治疗超出普通基线 PSM 的边界;
  • 写出包含目标人群、样本损失、平衡与局限的可审核报告。

1 PSM 在解决什么问题

1.1 从潜在结局到目标估计量

令 A∈{0,1}A\in\{0,1\} 表示接受干预,Y(1)Y(1) 与 Y(0)Y(0) 表示同一个体在两种策略下的潜在结局。常见目标包括:

ATE⁡=E{Y(1)−Y(0)},ATT⁡=E{Y(1)−Y(0)∣A=1}. \operatorname{ATE}=E\{Y(1)-Y(0)\}, \qquad \operatorname{ATT}=E\{Y(1)-Y(0)\mid A=1\}.

ATE 针对整个目标人群;ATT 针对实际接受治疗者;ATC 则针对实际对照者。三者在效应异质或支持区域不同的时候可以不同。PSM 不是一个脱离 estimand 的按钮:匹配方向、是否丢弃样本、是否允许重复使用对照,都应服务于预先定义的目标。

本教程的主要问题是:

在观察到且可找到合适对照的干预接受者中,如果这些人接受干预而不是对照管理,6 个月结局平均相差多少?

初始意图是 ATT;但 caliper 使一部分干预者无法匹配,因此最终可识别的目标更准确地称为保留干预者中的 ATT(matched-sample ATT)。必须报告这次目标人群收窄,而不能只写“估计了 ATT”。

1.2 倾向评分的定义与平衡性质

对治疗前协变量 XX,倾向评分是:

e(X)=P(A=1∣X). e(X)=P(A=1\mid X).

若给定 XX 后治疗分配与潜在结局可交换,并存在正值性,那么在正确使用倾向评分形成可比组后,可以比较结局。其核心角色是压缩用于治疗分配的一组协变量,以帮助构造平衡设计,不是预测谁“应该”治疗,也不是个体因果效应。

PSM 通常按相近的 e(X)e(X) 或 logit {e(X)}\{e(X)\} 把治疗者与对照者配对。成功标准不是 PS 模型的 AUC、似然比检验或系数 p 值,而是匹配后治疗前协变量是否充分平衡、目标样本是否清楚、重叠是否可信。

检验你的理解:PS 模型是否越会预测治疗越好? 不一定。极高区分度可能表示两组几乎没有共同支持。PS 模型是设计工具,评价重点是匹配后的协变量平衡与重叠,而不是最大化预测准确率。

1.3 识别假设不能由软件检验

假设 在本问题中的含义 可做的工作
一致性 “干预”和“对照”有足够明确的版本,观察结局等于实际接受策略的潜在结局 明确剂量、开始时间、依从性与版本
条件可交换性 给定所选治疗前变量后,不再有共同影响治疗与结局的原因 时间顺序、DAG、领域知识、敏感性分析
正值性 对目标协变量组合,两种策略都有可能出现 画重叠图、检查极端 PS、限制目标人群
无干扰 一人的治疗不改变另一人的结局 说明传播、机构或网络效应是否可能存在
正确测量与模型 治疗、结局和关键协变量测量充分,设计模型可形成平衡 数据审计、灵活项、平衡诊断

匹配可以处理已测量且被正确使用的基线混杂,不能创造随机化,也不能检验“所有混杂都已测量”。

2 设计阶段:先不查看结局

2.1 变量选择来自因果结构,不来自 p 值

PS 模型应在明确时间零点后,仅使用治疗开始前已知的变量。优先纳入:

  • 治疗与结局的共同原因;
  • 强结局预测因子,即使其与治疗的样本关联不强;
  • 反映共同原因、临床决策和入组机制的可靠代理变量。

谨慎或避免纳入:

  • 治疗后的中介变量;
  • 由治疗与结局原因共同影响的碰撞点;
  • 只影响治疗、几乎不影响结局的强工具型变量,因为它可能恶化重叠并放大残余混杂;
  • 仅因单变量 p 值小、缺失模式方便或逐步选择保留的变量。

本例使用年龄、BMI、吸烟、基线严重度和治疗前生物标志物。结局没有进入 psm_design_data,也不会用于挑选 caliper、ratio 或 PS 公式。

stopifnot(
  nrow(psm_design_data) == 1200,
  all(psm_design_data$treatment_num %in% c(0, 1)),
  !anyNA(psm_design_data),
  !"outcome" %in% names(psm_design_data)
)

data_preview <- transform(
  head(psm_design_data, 6),
  treatment = ifelse(treatment == "Intervention", "干预", "对照"),
  smoker = ifelse(smoker == "Yes", "是", "否"),
  severity = c(
    Mild = "轻度", Moderate = "中度", Severe = "重度"
  )[as.character(severity)]
)

knitr::kable(
  data_preview[, c(
    "id", "treatment", "age", "bmi", "smoker", "severity", "biomarker"
  )],
  col.names = c(
    "编号", "实际策略", "年龄", "BMI", "当前吸烟",
    "基线严重度", "标准化生物标志物"
  ),
  caption = "设计阶段数据的前 6 行(不含结局)"
)
设计阶段数据的前 6 行(不含结局)
编号 实际策略 年龄 BMI 当前吸烟 基线严重度 标准化生物标志物
1 对照 55.9 18.8 是 轻度 0.91
2 干预 66.5 23.4 是 轻度 0.15
3 对照 51.8 40.4 是 中度 0.62
4 对照 59.4 23.9 是 轻度 -0.19
5 干预 75.5 20.4 否 轻度 0.21
6 对照 65.4 30.9 否 重度 -2.38
cohort_audit <- data.frame(
  总样本 = nrow(psm_design_data),
  对照组 = sum(psm_design_data$treatment_num == 0),
  干预组 = sum(psm_design_data$treatment_num == 1),
  干预比例 = pct(mean(psm_design_data$treatment_num == 1)),
  check.names = FALSE
)

knitr::kable(cohort_audit, caption = "匹配前队列审计")
匹配前队列审计
总样本 对照组 干预组 干预比例
1200 710 490 40.8%

队列包含 490 名干预者和 710 名对照者。只有在变量含义、单位、编码、时间顺序、重复记录和缺失均确认后,才应开始匹配。

2.2 用 logistic 回归估计 PS

最常见的工作模型为:

logit⁡{e(X)}=log⁡e(X)1−e(X)=β0+βTX. \operatorname{logit}\{e(X)\} =\log\frac{e(X)}{1-e(X)} =\beta_0+\beta^T X.

先用普通 glm() 可以清楚看到所估计的量;随后 matchit() 会用相同公式建立距离。

ps_model <- glm(
  treatment_num ~ age + bmi + smoker + severity + biomarker,
  data = psm_design_data,
  family = binomial()
)

psm_design_data$ps_probability <- predict(ps_model, type = "response")
psm_design_data$ps_logit <- predict(ps_model, type = "link")

ps_coefficient_table <- data.frame(
  项 = names(coef(ps_model)),
  logit系数 = unname(coef(ps_model)),
  check.names = FALSE
)

knitr::kable(
  ps_coefficient_table,
  digits = 3,
  caption = "治疗分配 logistic 工作模型的系数"
)
治疗分配 logistic 工作模型的系数
项 logit系数
(Intercept) -7.602
age 0.066
bmi 0.093
smokerYes 0.806
severityModerate 0.413
severitySevere 1.171
biomarker 0.270

连续变量的线性 logit、交互和缺失处理都属于模型设定。若基于领域知识或匹配后的协变量诊断发现平衡不足,可在不查看结局的前提下加入预先合理的非线性项,例如 I(age^2) 或治疗前变量之间的交互。不要根据治疗模型系数是否显著来删变量。

不要用结局调匹配参数 如果反复尝试公式、caliper 或匹配算法,最后选择结局差异最大或 p 值最小的方案,设计阶段就被结果污染。应使用协变量平衡、重叠、样本保留和预先说明的临床可比性来冻结设计,然后只打开结局一次。

3 在 R 中执行 1:1 最近邻匹配

3.1 主规格:ATT、最近邻、0.2 SD caliper

主设计采用:

  • estimand = "ATT":从每名干预者出发寻找对照;
  • method = "nearest":最近邻匹配;
  • ratio = 1:每名保留干预者匹配一名对照;
  • replace = FALSE:同一对照不能重复使用;
  • link = "linear.logit":在 PS 的 logit 线性预测值尺度上匹配;
  • caliper = 0.20, std.caliper = TRUE:两人的 logit 距离不超过该距离标准差的 0.2 倍;
  • m.order = "closest":在贪心最近邻算法中优先处理当前更近的候选配对;它不等同于最小化所有配对总距离的 optimal matching。
m.out <- MatchIt::matchit(
  treatment_num ~ age + bmi + smoker + severity + biomarker,
  data = psm_design_data,
  method = "nearest",
  distance = "glm",
  link = "linear.logit",
  estimand = "ATT",
  ratio = 1,
  replace = FALSE,
  caliper = 0.20,
  std.caliper = TRUE,
  m.order = "closest"
)

stopifnot(
  isTRUE(all.equal(unname(m.out$distance), psm_design_data$ps_logit))
)

match_summary <- summary(m.out, un = TRUE, standardize = TRUE)
matched_design <- MatchIt::match_data(m.out, data = psm_design_data)

m.out$distance 在这里是 logit 线性预测值,不是 0 到 1 的概率;需要概率时可用 plogis(m.out$distance)。caliper 也因此作用于 logit 尺度。改变 link 会改变距离的尺度,不能只复制数值 0.2 而忽略定义。

3.2 先看共同支持,不急着看结局

ps_probability <- plogis(m.out$distance)
control_density <- density(
  ps_probability[psm_design_data$treatment_num == 0],
  from = 0,
  to = 1
)
treated_density <- density(
  ps_probability[psm_design_data$treatment_num == 1],
  from = 0,
  to = 1
)

plot(
  control_density,
  col = palette_psm["orange"],
  lwd = 2.4,
  xlim = c(0, 1),
  ylim = c(0, max(control_density$y, treated_density$y)),
  xlab = "估计倾向评分",
  ylab = "密度",
  main = "匹配前的共同支持",
  las = 1
)
lines(treated_density, col = palette_psm["teal"], lwd = 2.4)
legend(
  "topright",
  legend = c("对照", "干预"),
  col = c(palette_psm["orange"], palette_psm["teal"]),
  lwd = 2.4,
  bty = "n"
)
两条倾向评分密度曲线。对照组密度更多集中在较低概率,干预组向右移动;中间区域重叠,而两端重叠较弱。

匹配前干预组与对照组的估计倾向评分分布。两组有共同支持,但干预组整体向较高概率移动,尾部个体较难找到可比对象。

“两组 PS 范围有交集”只是最低限度,不等于处处有可靠正值性。应结合密度、局部样本量、具体协变量组合和被排除者特征。尾部少数对照支撑大量干预者时,即使范围重叠,也可能依赖脆弱外推。

3.3 样本流与匹配距离

sample_flow_raw <- match_summary$nn[
  c("All", "Matched", "Unmatched", "Discarded"),
  ,
  drop = FALSE
]
sample_flow <- data.frame(
  阶段 = c("原始样本", "成功匹配", "未能匹配", "匹配前主动丢弃"),
  对照 = sample_flow_raw[, "Control"],
  干预 = sample_flow_raw[, "Treated"],
  check.names = FALSE
)

knitr::kable(sample_flow, caption = "1:1 最近邻匹配的样本流")
1:1 最近邻匹配的样本流
阶段 对照 干预
All 原始样本 710 490
Matched 成功匹配 375 375
Unmatched 未能匹配 335 115
Discarded 匹配前主动丢弃 0 0
pair_groups_design <- split(matched_design, matched_design$subclass)
pair_logit_differences <- vapply(
  pair_groups_design,
  function(x) {
    abs(
      x$distance[x$treatment_num == 1] -
        x$distance[x$treatment_num == 0]
    )
  },
  numeric(1)
)
caliper_width <- 0.20 * sd(m.out$distance)

distance_audit <- data.frame(
  配对数 = length(pair_logit_differences),
  caliper宽度 = caliper_width,
  最大实际配对距离 = max(pair_logit_differences),
  中位实际配对距离 = median(pair_logit_differences),
  check.names = FALSE
)

knitr::kable(
  distance_audit,
  digits = 3,
  caption = "logit PS 尺度上的 caliper 与实际配对距离"
)
logit PS 尺度上的 caliper 与实际配对距离
配对数 caliper宽度 最大实际配对距离 中位实际配对距离
375 0.203 0.202 0.002

主规格形成 375 对,共保留 375 名干预者与同数对照。115 名干预者没有 caliper 内的可用对照。最大实际距离为 0.202,低于 0.203 的 caliper。

样本损失不仅是“精度变低” 未匹配的 115 名干预者不再由主要比较代表。因此主要结果针对 375 名保留干预者,而不是自动推广到全部 490 名干预者。应比较保留者与未匹配者的基线特征,并在标题、表格和讨论中明确目标人群。

retained_treated_ids <- matched_design$id[matched_design$treatment_num == 1]
treated_population_table <- rbind(
  全部干预者 = c(
    n = sum(psm_design_data$treatment_num == 1),
    mean_age = mean(psm_design_data$age[psm_design_data$treatment_num == 1]),
    mean_bmi = mean(psm_design_data$bmi[psm_design_data$treatment_num == 1]),
    severe = mean(
      psm_design_data$severity[psm_design_data$treatment_num == 1] == "Severe"
    )
  ),
  保留干预者 = c(
    n = length(retained_treated_ids),
    mean_age = mean(matched_design$age[matched_design$treatment_num == 1]),
    mean_bmi = mean(matched_design$bmi[matched_design$treatment_num == 1]),
    severe = mean(
      matched_design$severity[matched_design$treatment_num == 1] == "Severe"
    )
  ),
  未匹配干预者 = {
    u <- psm_design_data$treatment_num == 1 &
      !psm_design_data$id %in% retained_treated_ids
    c(
      n = sum(u),
      mean_age = mean(psm_design_data$age[u]),
      mean_bmi = mean(psm_design_data$bmi[u]),
      severe = mean(psm_design_data$severity[u] == "Severe")
    )
  }
)

treated_population_display <- data.frame(
  人群 = rownames(treated_population_table),
  人数 = treated_population_table[, "n"],
  平均年龄 = treated_population_table[, "mean_age"],
  平均BMI = treated_population_table[, "mean_bmi"],
  重度比例 = pct(treated_population_table[, "severe"]),
  check.names = FALSE
)

knitr::kable(
  treated_population_display,
  digits = 1,
  caption = "全部、保留与未匹配干预者的基线特征"
)
全部、保留与未匹配干预者的基线特征
人群 人数 平均年龄 平均BMI 重度比例
全部干预者 全部干预者 490 62.9 29.2 23.7%
保留干预者 保留干预者 375 61.0 28.7 20.3%
未匹配干预者 未匹配干预者 115 69.0 30.8 34.8%

4 平衡诊断:匹配是否真的成功

4.1 SMD 是主角,基线 p 值不是

对连续变量,标准化均差可写为:

SMD⁡=X‾1−X‾0s*, \operatorname{SMD} =\frac{\bar X_1-\bar X_0}{s^*},

其中 s*s^* 是预先定义的标准化尺度。在本页的 ATT 设计中,MatchIt 使用原始干预组的标准差作为 SMD 分母,并在匹配前后保持同一分母;ATE 或 ATC 的参考尺度可能不同。分类变量通常拆成指示变量逐一检查。SMD 不随样本量机械变小,适合描述设计平衡;基线显著性检验回答“总体均值是否可能相同”,不是“差异是否足以造成混杂”。

绝对 SMD 小于 0.1 是常用经验线,不是自动合格证。重要变量可能需要更严格标准;均值平衡也不能保证尾部、非线性和交互平衡。

balance_terms <- setdiff(rownames(match_summary$sum.all), "distance")
balance_labels <- c(
  age = "年龄",
  bmi = "BMI",
  smokerNo = "不吸烟",
  smokerYes = "吸烟",
  severityMild = "轻度",
  severityModerate = "中度",
  severitySevere = "重度",
  biomarker = "生物标志物"
)

balance_table <- data.frame(
  协变量 = unname(balance_labels[balance_terms]),
  匹配前SMD = match_summary$sum.all[balance_terms, "Std. Mean Diff."],
  匹配后SMD = match_summary$sum.matched[
    balance_terms, "Std. Mean Diff."
  ],
  匹配前方差比 = match_summary$sum.all[balance_terms, "Var. Ratio"],
  匹配后方差比 = match_summary$sum.matched[
    balance_terms, "Var. Ratio"
  ],
  匹配前最大eCDF差 = match_summary$sum.all[balance_terms, "eCDF Max"],
  匹配后最大eCDF差 = match_summary$sum.matched[
    balance_terms, "eCDF Max"
  ],
  check.names = FALSE
)

max_smd_before <- max(abs(balance_table$匹配前SMD), na.rm = TRUE)
max_smd_after <- max(abs(balance_table$匹配后SMD), na.rm = TRUE)

knitr::kable(
  balance_table,
  digits = 3,
  caption = "治疗前协变量的匹配前后平衡诊断"
)
治疗前协变量的匹配前后平衡诊断
协变量 匹配前SMD 匹配后SMD 匹配前方差比 匹配后方差比 匹配前最大eCDF差 匹配后最大eCDF差
age 年龄 0.566 -0.017 0.992 0.958 0.237 0.035
bmi BMI 0.371 -0.009 1.049 1.052 0.148 0.040
smokerNo 不吸烟 -0.312 0.000 NA NA 0.151 0.000
smokerYes 吸烟 0.312 0.000 NA NA 0.151 0.000
severityMild 轻度 -0.296 -0.066 NA NA 0.144 0.032
severityModerate 中度 0.076 0.033 NA NA 0.037 0.016
severitySevere 重度 0.252 0.038 NA NA 0.107 0.016
biomarker 生物标志物 0.250 -0.031 0.991 1.052 0.131 0.040

治疗前协变量的最大绝对 SMD 从 0.566 降到 0.066。这里特意不把 distance 行算作“协变量最大值”:PS 本身接近平衡并不能替代逐个协变量的诊断。

love_before <- abs(balance_table$匹配前SMD)
love_after <- abs(balance_table$匹配后SMD)
love_order <- order(love_before)
love_y <- seq_along(love_order)

plot(
  love_before[love_order],
  love_y,
  pch = 16,
  col = palette_psm["orange"],
  xlim = c(0, max(love_before) * 1.08),
  yaxt = "n",
  xlab = "绝对标准化均差",
  ylab = "",
  main = "匹配前后协变量平衡",
  las = 1
)
points(
  love_after[love_order],
  love_y,
  pch = 17,
  col = palette_psm["teal"]
)
axis(2, at = love_y, labels = balance_table$协变量[love_order], las = 1)
abline(v = 0.10, lty = 2, col = palette_psm["vermillion"])
legend(
  "bottomright",
  legend = c("匹配前", "匹配后", "|SMD|=0.10"),
  col = c(
    palette_psm["orange"],
    palette_psm["teal"],
    palette_psm["vermillion"]
  ),
  pch = c(16, 17, NA),
  lty = c(NA, NA, 2),
  bty = "n"
)
横向点图比较八个协变量匹配前后的绝对标准化均差。匹配前多个橙色点超过 0.10;匹配后的蓝绿色点全部位于 0.10 左侧。

治疗前协变量的匹配前后绝对标准化均差(Love plot)。虚线 0.10 是常用经验参考,而不是充分因果条件。

4.2 方差比与 eCDF 补充均值诊断

方差比(VR)比较两组离散程度;MatchIt 在这里报告干预组方差除以对照组方差,匹配后使用相应匹配权重。连续变量的理想值接近 1。0.5–2 或更严格的 0.8–1.25 可作问题筛查线,但不能机械决定因果有效性。二分类指示变量的方差由均值决定,表中 VR 可能为空,这不是计算失败。

经验累积分布函数(eCDF)在整个取值范围比较分布。eCDF Max 是两条 eCDF 的最大垂直距离,越接近 0 越好;它能发现“均值相同但分布不同”的情况。

old_par <- par(mfrow = c(1, 2), mar = c(4.2, 4.2, 3, 1))

plot(
  ecdf(psm_design_data$age[psm_design_data$treatment_num == 0]),
  col = palette_psm["orange"],
  lwd = 2.2,
  verticals = TRUE,
  do.points = FALSE,
  main = "匹配前",
  xlab = "年龄",
  ylab = "经验累积概率",
  las = 1
)
lines(
  ecdf(psm_design_data$age[psm_design_data$treatment_num == 1]),
  col = palette_psm["teal"],
  lwd = 2.2,
  verticals = TRUE,
  do.points = FALSE
)

plot(
  ecdf(matched_design$age[matched_design$treatment_num == 0]),
  col = palette_psm["orange"],
  lwd = 2.2,
  verticals = TRUE,
  do.points = FALSE,
  main = "匹配后",
  xlab = "年龄",
  ylab = "经验累积概率",
  las = 1
)
lines(
  ecdf(matched_design$age[matched_design$treatment_num == 1]),
  col = palette_psm["teal"],
  lwd = 2.2,
  verticals = TRUE,
  do.points = FALSE
)
legend(
  "bottomright",
  legend = c("对照", "干预"),
  col = c(palette_psm["orange"], palette_psm["teal"]),
  lwd = 2.2,
  bty = "n"
)
左右两个面板展示年龄经验累积分布。匹配前干预与对照曲线明显分开;匹配后两条曲线几乎重合。

年龄在匹配前后的经验累积分布。匹配后两组阶梯曲线明显靠近,补充了均值层面的 SMD 诊断。

par(old_par)
continuous_vr_before <- balance_table$匹配前方差比[
  is.finite(balance_table$匹配前方差比)
]
continuous_vr_after <- balance_table$匹配后方差比[
  is.finite(balance_table$匹配后方差比)
]

balance_summary <- data.frame(
  诊断 = c(
    "最大协变量 |SMD|",
    "连续变量方差比范围",
    "最大协变量 eCDF Max"
  ),
  匹配前 = c(
    sprintf("%.3f", max_smd_before),
    sprintf(
      "%.3f–%.3f",
      min(continuous_vr_before),
      max(continuous_vr_before)
    ),
    sprintf("%.3f", max(balance_table$匹配前最大eCDF差))
  ),
  匹配后 = c(
    sprintf("%.3f", max_smd_after),
    sprintf(
      "%.3f–%.3f",
      min(continuous_vr_after),
      max(continuous_vr_after)
    ),
    sprintf("%.3f", max(balance_table$匹配后最大eCDF差))
  ),
  check.names = FALSE
)

knitr::kable(balance_summary, caption = "主规格的总体平衡摘要")
主规格的总体平衡摘要
诊断 匹配前 匹配后
最大协变量 |SMD| 0.566 0.066
连续变量方差比范围 0.991–1.049 0.958–1.052
最大协变量 eCDF Max 0.237 0.040

4.3 匹配后的 PS 分布

matched_probability <- plogis(matched_design$distance)
matched_control_density <- density(
  matched_probability[matched_design$treatment_num == 0],
  from = 0,
  to = 1
)
matched_treated_density <- density(
  matched_probability[matched_design$treatment_num == 1],
  from = 0,
  to = 1
)

plot(
  matched_control_density,
  col = palette_psm["orange"],
  lwd = 2.4,
  xlim = c(0, 1),
  ylim = c(0, max(matched_control_density$y, matched_treated_density$y)),
  xlab = "估计倾向评分",
  ylab = "密度",
  main = "匹配后的支持区域",
  las = 1
)
lines(
  matched_treated_density,
  col = palette_psm["teal"],
  lwd = 2.4
)
legend(
  "topright",
  legend = c("匹配对照", "保留干预者"),
  col = c(palette_psm["orange"], palette_psm["teal"]),
  lwd = 2.4,
  bty = "n"
)
匹配后两条倾向评分密度曲线几乎重合,均主要位于中等概率区域,极端尾部已明显减少。

匹配样本中干预组与对照组的估计倾向评分分布。两组分布在保留支持区域内高度接近。

检验你的理解:PS 已平衡,是否无需看原协变量? 不是。不同协变量组合可以有相似 PS。必须逐个检查所有预先选择的治疗前协变量,并根据需要检查非线性项、交互、尾部和具有特殊临床意义的分层。

5 冻结设计后再分析结局

5.1 保留 subclass 与 weights

MatchIt::match_data() 返回原变量以及:

  • distance:匹配距离;
  • weights:由匹配方案产生的分析权重;
  • subclass:匹配组编号。

主设计是 1:1、无放回,因此每个保留个体权重均为 1,每个 subclass 恰有一名干预者和一名对照。ratio、replacement 或其他匹配方式下,不能假定权重都是 1,也不能丢掉 subclass 后按普通独立样本分析。

# 设计已通过平衡诊断并冻结;现在才把结局合并进匹配样本。
matched_outcome <- MatchIt::match_data(m.out, data = psm_data)

subclass_audit <- aggregate(
  treatment_num ~ subclass,
  data = matched_outcome,
  FUN = function(z) c(n = length(z), treated = sum(z))
)

stopifnot(
  nrow(matched_outcome) == 750,
  all(matched_outcome$weights == 1),
  all(vapply(
    split(matched_outcome$treatment_num, matched_outcome$subclass),
    function(z) length(z) == 2 && sum(z) == 1,
    logical(1)
  ))
)

matched_preview <- transform(
  head(matched_outcome[order(matched_outcome$subclass), ], 8),
  treatment = ifelse(treatment == "Intervention", "干预", "对照")
)

knitr::kable(
  matched_preview[, c(
    "id", "treatment", "outcome", "distance", "weights", "subclass"
  )],
  col.names = c(
    "编号", "策略", "6 个月结局", "logit PS", "权重", "配对编号"
  ),
  digits = 3,
  caption = "冻结设计后打开的匹配结局数据"
)
冻结设计后打开的匹配结局数据
编号 策略 6 个月结局 logit PS 权重 配对编号
2 2 干预 128 -0.170 1 1
32 32 对照 134 -0.199 1 1
5 5 干预 124 -0.640 1 2
810 810 对照 128 -0.601 1 2
11 11 干预 132 -0.845 1 3
725 725 对照 140 -0.845 1 3
14 14 干预 134 0.032 1 4
791 791 对照 158 -0.031 1 4

5.2 1:1 配对 ATT 与置信区间

对第 jj 个匹配对,计算:

Dj=Y1j−Y0j,ATT̂matched=1J∑j=1JDj. D_j=Y_{1j}-Y_{0j}, \qquad \widehat{ATT}_{matched}=\frac{1}{J}\sum_{j=1}^{J}D_j.

以配对为独立单位,均值标准误为 sD/Js_D/\sqrt J。这个简洁推断适用于当前 1:1 无放回设计;不能原样复制到有放回、多对一或加权匹配。

matched_pairs <- split(matched_outcome, matched_outcome$subclass)
pair_differences <- vapply(
  matched_pairs,
  function(x) {
    x$outcome[x$treatment_num == 1] -
      x$outcome[x$treatment_num == 0]
  },
  numeric(1)
)

matched_att <- mean(pair_differences)
matched_att_se <- sd(pair_differences) / sqrt(length(pair_differences))
matched_att_ci <- matched_att +
  qt(c(0.025, 0.975), df = length(pair_differences) - 1) * matched_att_se
matched_att_p <- 2 * pt(
  -abs(matched_att / matched_att_se),
  df = length(pair_differences) - 1
)

naive_difference <- with(
  psm_data,
  mean(outcome[treatment_num == 1]) - mean(outcome[treatment_num == 0])
)

true_full_att <- mean(
  psm_data$individual_effect[psm_data$treatment_num == 1]
)
true_matched_att <- mean(
  matched_outcome$individual_effect[matched_outcome$treatment_num == 1]
)

effect_table <- data.frame(
  分析 = c(
    "未调整原始均值差",
    "1:1 配对后的 matched-sample ATT",
    "模拟真值:全部干预者 ATT",
    "模拟真值:保留干预者 ATT"
  ),
  估计或真值 = c(
    naive_difference,
    matched_att,
    true_full_att,
    true_matched_att
  ),
  `95% CI 下限` = c(NA, matched_att_ci[1], NA, NA),
  `95% CI 上限` = c(NA, matched_att_ci[2], NA, NA),
  check.names = FALSE
)

knitr::kable(
  effect_table,
  digits = 2,
  caption = "未调整比较、匹配估计与仅供教学的模拟真值"
)
未调整比较、匹配估计与仅供教学的模拟真值
分析 估计或真值 95% CI 下限 95% CI 上限
未调整原始均值差 -0.54 NA NA
1:1 配对后的 matched-sample ATT -6.27 -7.26 -5.28
模拟真值:全部干预者 ATT -6.12 NA NA
模拟真值:保留干预者 ATT -6.18 NA NA

未调整均值差只有 -0.54,因高风险者更可能接受干预而严重低估其降低结局的作用。匹配后的估计为 -6.27(95% CI:-7.26 至 -5.28;p <0.001),接近保留干预者中的模拟真值 -6.18。

在已测量变量足以实现条件可交换性、匹配后保留人群具有正值性、治疗与结局定义正确、且无干扰等假设下,接受干预使保留干预者的 6 个月结局平均降低约 6.27 个单位。因为结局越低越好,负差值有利于干预。该结论针对成功匹配者,不自动代表未匹配干预者或整个原始队列。

5.2.1 置信区间没有包含所有不确定性

这里的配对 t 区间把形成的配对视为固定,并依赖配对之间独立及均值近似。它没有自动量化:

  • PS 模型与设计选择的不确定性;
  • 未测量混杂和测量误差;
  • 因不重叠而排除人群带来的外推不确定性;
  • 机构、家庭或医生层面的额外聚类;
  • 数据驱动反复尝试匹配规格造成的选择不确定性。

不要把窄置信区间误写成“混杂已消除”。对于复杂匹配、聚类或小样本,应预先确定与设计相容的稳健/聚类推断,必要时咨询统计专家。

6 设计选择与敏感性分析

6.1 caliper、ratio 与 replacement 的权衡

选择 主要收益 主要代价与推断提醒
更窄 caliper 配对距离更近,常改善局部可比性 更多干预者无法匹配,目标人群变窄、精度下降
更宽 caliper 保留更多人 可能接受更差配对、残余不平衡增大
ratio > 1 每名干预者利用更多对照,可能提高精度 后续对照通常更远;组大小不一时权重可能不同,必须按软件输出使用
replacement 稀少的优质对照可重复匹配,常改善可比性 有效样本量下降;需处理对照重复与聚类/权重
exact matching 强制关键分类变量完全一致 可能大量损失样本;其他变量仍需诊断

不要根据结局选择这些选项。下面只比较预先列出的设计诊断,不读取任何结局。

fit_design <- function(caliper = 0.20, ratio = 1, replace = FALSE) {
  MatchIt::matchit(
    treatment_num ~ age + bmi + smoker + severity + biomarker,
    data = psm_design_data,
    method = "nearest",
    distance = "glm",
    link = "linear.logit",
    estimand = "ATT",
    ratio = ratio,
    replace = replace,
    caliper = caliper,
    std.caliper = TRUE,
    m.order = "closest"
  )
}

design_fits <- list(
  `1:1,无放回,0.10 SD` = fit_design(0.10, 1, FALSE),
  `1:1,无放回,0.20 SD(主规格)` = m.out,
  `1:1,无放回,0.30 SD` = fit_design(0.30, 1, FALSE),
  `最多 1:2,无放回,0.20 SD` = suppressWarnings(
    fit_design(0.20, 2, FALSE)
  ),
  `1:1,有放回,0.20 SD` = fit_design(0.20, 1, TRUE)
)

extract_design_diagnostics <- function(fit) {
  s <- summary(fit, un = TRUE, standardize = TRUE)
  covariate_rows <- setdiff(rownames(s$sum.matched), "distance")
  c(
    retained_treated = s$nn["Matched", "Treated"],
    used_controls = sum(
      fit$weights[psm_design_data$treatment_num == 0] > 0
    ),
    max_smd = max(
      abs(s$sum.matched[covariate_rows, "Std. Mean Diff."]),
      na.rm = TRUE
    ),
    max_weight = max(fit$weights)
  )
}

design_sensitivity_matrix <- do.call(
  rbind,
  lapply(design_fits, extract_design_diagnostics)
)
design_sensitivity_table <- data.frame(
  设计规格 = rownames(design_sensitivity_matrix),
  保留干预者 = design_sensitivity_matrix[, "retained_treated"],
  使用的不同对照 = design_sensitivity_matrix[, "used_controls"],
  最大协变量绝对SMD = design_sensitivity_matrix[, "max_smd"],
  最大匹配权重 = design_sensitivity_matrix[, "max_weight"],
  check.names = FALSE
)

knitr::kable(
  design_sensitivity_table,
  digits = 3,
  caption = "不查看结局时比较预先列出的匹配规格"
)
不查看结局时比较预先列出的匹配规格
设计规格 保留干预者 使用的不同对照 最大协变量绝对SMD 最大匹配权重
1:1,无放回,0.10 SD 1:1,无放回,0.10 SD 372 372 0.066 1.00
1:1,无放回,0.20 SD(主规格) 1:1,无放回,0.20 SD(主规格) 375 375 0.066 1.00
1:1,无放回,0.30 SD 1:1,无放回,0.30 SD 379 379 0.060 1.00
最多 1:2,无放回,0.20 SD 最多 1:2,无放回,0.20 SD 375 509 0.046 1.36
1:1,有放回,0.20 SD 1:1,有放回,0.20 SD 489 270 0.055 6.07

这些规格都可能是合理设计,选择取决于目标估计量、关键协变量、样本保留与精度。“最多 1:2”规格中,若某名干预者在 caliper 内只有一名对照,软件会保留 1:1 匹配;因此它不保证每个 subclass 都恰有两名对照。主规格在预先指定的 0.10 SMD 参考线内实现平衡,同时保留 375 名干预者;结局分析只在设计冻结后执行。

6.2 ratio 与 replacement 下如何使用权重

match_data() 中的权重编码 MatchIt 所构造的目标样本。1:k 匹配时,同一 subclass 内的多个对照通常分担对照权重;有放回时,同一原始对照可能服务于多个干预者。常见错误是对提取的数据做一次无权重 lm(),从而丢掉原设计。

# 例:最多 1:2 匹配后保留 MatchIt 权重与 subclass。
m_ratio2 <- design_fits[["最多 1:2,无放回,0.20 SD"]]
d_ratio2 <- MatchIt::match_data(m_ratio2, data = psm_data)

# 点估计可用匹配权重;标准误必须与 subclass/重复使用结构相容。
fit_ratio2 <- lm(outcome ~ treatment_num,
                 data = d_ratio2,
                 weights = weights)

# 有放回匹配时,get_matches() 可展开每次匹配使用;同一 id 可能重复。
m_replace <- design_fits[["1:1,有放回,0.20 SD"]]
d_replace <- MatchIt::get_matches(
  m_replace,
  data = psm_data,
  id = "source_row"
)

上面的 lm() 只说明如何保留点估计权重,并不自动提供有效且与设计相容的标准误。实际分析需根据匹配方法采用 subclass/个体聚类稳健方差或与匹配设计相容的推断;重复使用的对照绝不能当成彼此独立的新个体。

6.3 如果平衡仍不好怎么办

在结局仍不可见时,按以下顺序排查:

  1. 核对变量编码、单位、时间点与缺失;
  2. 检查具体哪些变量、尾部或交互不平衡;
  3. 根据领域知识为 PS 增加非线性或交互;
  4. 对关键分类变量做 exact matching,或改变 caliper/匹配方法;
  5. 若根本没有支持,缩小目标人群并坦率报告,而不是强行配对;
  6. 重新完成全部平衡与样本流诊断,再冻结设计。

不能通过把不平衡变量从表中删掉来“改善”平衡。若合理设计仍无法获得可比组,PSM 可能不适合这份数据。

7 不同结局类型如何迁移

匹配负责设计可比组,不决定结局模型。结局尺度必须回到研究问题。

7.1 连续结局

当前 1:1 配对可直接分析 pair difference。若结局严重偏斜,可报告配对差分布、稳健位置量或预先定义的变换;不要只因正态检验显著就临时换 estimand。

7.2 二分类结局

对每对计算 Y1−Y0∈{−1,0,1}Y_1-Y_0\in\{-1,0,1\},其均值就是保留干预者中的风险差。McNemar 检验关注不一致配对,但 p 值不能替代风险差和置信区间。

# 假设 binary_outcome 已在设计冻结后并入 matched_outcome。
binary_pairs <- split(matched_outcome, matched_outcome$subclass)
pair_risk_differences <- vapply(
  binary_pairs,
  function(x) {
    x$binary_outcome[x$treatment_num == 1] -
      x$binary_outcome[x$treatment_num == 0]
  },
  numeric(1)
)
mean(pair_risk_differences)  # matched-sample ATT 风险差

# 形成每对的 2×2 表后可做配对二分类检验。
paired_binary <- do.call(
  rbind,
  lapply(binary_pairs, function(x) c(
    treated = x$binary_outcome[x$treatment_num == 1],
    control = x$binary_outcome[x$treatment_num == 0]
  ))
)
mcnemar.test(table(paired_binary[, "treated"], paired_binary[, "control"]))

7.3 计数与时间结局

计数结局需要明确观察时长与过度离散;可用与匹配权重和聚类结构相容的 Poisson/负二项模型。时间结局还涉及删失、时间零点、竞争风险与比例风险;匹配之后仍要用适当的生存模型,并处理配对或个体重复。详见 生存分析详解。

# 需要 survival 包;示意 1:1 配对中的分层 Cox 模型。
survival::coxph(
  survival::Surv(time, event) ~ treatment_num + strata(subclass),
  data = matched_outcome,
  ties = "efron"
)

# 若使用匹配权重、replacement 或一个人多行,应采用相容的稳健方差。

匹配不修复信息性删失,也不让 HR 自动成为风险差。应报告与问题相符的固定时点风险、RMST、HR 或其他效应量,并检查生存分析自身假设。

8 PSM 的边界

8.1 未测量混杂

完美的 Love plot 只证明已纳入变量的分布接近。未测量疾病严重度、医生偏好、就医可及性或治疗禁忌仍可能同时影响治疗和结局。应:

  • 在设计期系统列出潜在未测量共同原因;
  • 使用负对照、定量偏倚分析或适合效应尺度的敏感性方法;
  • 比较不同可信协变量集合,而不是只报告最有利规格;
  • 清楚区分“在无未测量混杂等假设下的因果估计”与已证实因果效应。

敏感性分析不能证明没有偏倚,但能说明多强的未测量关系才足以改变结论。

8.2 缺失数据不是删掉就结束

完整案例 PSM 可能因缺失同时与治疗和结局相关而产生选择偏倚,并改变目标人群。处理步骤应包括:

  1. 区分变量未测、结构性缺失与随访结局缺失;
  2. 描述各组缺失比例和缺失模式;
  3. 在 PS 估计前实施与分析目标相容的多重插补或其他策略;
  4. 保持“设计不看结局”的原则,同时让插补模型尊重因果时间顺序;
  5. 在每个插补数据集中完成设计与估计,再用合适规则汇总,并报告匹配对象可能变化;
  6. 对 MNAR 或失访机制做敏感性分析。

简单把 NA 编成“未知”类别不一定消除偏倚;单次均值填补会扭曲方差和关系。

8.3 时间变化治疗与混杂

普通基线 PSM 假设治疗策略在共同时间零点定义。若治疗可在随访中开始、停止或切换,而当前健康状态既影响下一次治疗又受既往治疗影响,基线 PS 无法解决时间变化混杂。

此时应先模拟目标试验,建立 person-period 数据,并考虑 g-formula、边际结构模型或其他纵向因果方法。把“随访中曾治疗”倒填为基线变量会使用未来信息并产生不死时间偏倚。更多识别与纵向方法边界见 因果推断详解。

8.4 匹配不等于随机试验

PSM 不能自动解决:

  • 治疗、结局或协变量测量误差;
  • 选择进入队列或失访造成的偏倚;
  • 干预版本不一致、依从性或交叉治疗;
  • 机构与网络干扰;
  • 结局定义、随访窗口或时间零点不一致;
  • 过度外推到未匹配者、其他地区或其他年代。

9 完整应用结果与报告

9.1 结果摘要

case_summary <- data.frame(
  项目 = c(
    "原始样本",
    "主设计",
    "匹配样本",
    "最大协变量 |SMD|",
    "未调整结局均值差",
    "匹配后 matched-sample ATT",
    "模拟真值(仅供教学)"
  ),
  结果 = c(
    sprintf(
      "%d 名干预者;%d 名对照",
      sum(psm_design_data$treatment_num == 1),
      sum(psm_design_data$treatment_num == 0)
    ),
    "ATT;1:1 最近邻;无放回;logit PS;0.20 SD caliper",
    sprintf(
      "%d 对;未匹配 %d 名干预者",
      nlevels(matched_outcome$subclass),
      sum(psm_design_data$treatment_num == 1) -
        sum(matched_outcome$treatment_num == 1)
    ),
    sprintf("%.3f → %.3f", max_smd_before, max_smd_after),
    sprintf("%.2f", naive_difference),
    sprintf(
      "%.2f(95%% CI %.2f 至 %.2f)",
      matched_att,
      matched_att_ci[1],
      matched_att_ci[2]
    ),
    sprintf("保留干预者 ATT = %.2f", true_matched_att)
  ),
  check.names = FALSE
)

knitr::kable(case_summary, caption = "PSM 主分析的可审核摘要")
PSM 主分析的可审核摘要
项目 结果
原始样本 490 名干预者;710 名对照
主设计 ATT;1:1 最近邻;无放回;logit PS;0.20 SD caliper
匹配样本 375 对;未匹配 115 名干预者
最大协变量 |SMD| 0.566 → 0.066
未调整结局均值差 -0.54
匹配后 matched-sample ATT -6.27(95% CI -7.26 至 -5.28)
模拟真值(仅供教学) 保留干预者 ATT = -6.18

模拟队列共 1,200 人,其中 490 人接受干预、710 人接受对照管理。采用预先指定的 logistic 倾向评分,以 logit PS 的 0.20 个标准差为 caliper,进行 ATT 方向的 1:1 最近邻无放回匹配。375 名干预者获得对照,115 名未匹配。治疗前协变量最大绝对 SMD 从 0.566 降至 0.066。设计冻结后,375 对的平均配对结局差为 -6.27(95% CI -7.26 至 -5.28);这针对保留干预者,并依赖无未测量混杂、正值性、一致性和无干扰等假设。模拟保留人群真值为 -6.18,仅用于教学验证。

9.2 可审核报告模板

我们在[目标人群、地点与时间]中估计[ATE/ATT/ATC 或匹配后目标],治疗定义为[策略与时间零点],结局为[时点与尺度]。倾向评分使用治疗前的[变量列表与函数形式]估计;变量由[DAG/领域知识/方案]选择,设计阶段未使用结局。采用[匹配方法、方向、ratio、replacement、caliper 与 exact 条件]。原始样本包括[两组人数],匹配后包括[人数/匹配组];[人数]未匹配,因此目标人群[是否变化]。最大绝对 SMD 从[值]变为[值],并检查了[VR、eCDF、重叠、关键交互]。使用[权重、subclass 与方差方法]估计[效应量]为[估计值与 95% CI]。结果依赖[识别假设],主要局限为[未测量混杂、缺失、重叠、测量或外推]。

9.3 最低限度应保存的审计材料

  • 完整分析方案、estimand 与目标试验描述;
  • PS 公式、软件和版本、随机种子及匹配参数;
  • 匹配前后人数、未匹配原因和目标人群变化;
  • 所有预先选择协变量的 SMD、VR、eCDF 和分布图;
  • PS 重叠图、caliper 尺度与配对距离;
  • weights、subclass、原始 id 与匹配对象;
  • 结局打开的时间点、主效应代码与不确定性方法;
  • 所有设计规格与敏感性分析,而不只保留最有利结果。

10 常见错误速查

错误 为什么不对 更好的做法
把 PSM 当成自动去混杂按钮 未测量混杂与设计缺陷仍存在 先定义目标试验和识别假设
不写 estimand 就开始匹配 匹配方向和样本损失可能改变问题 预先定义 ATE/ATT/ATC 与目标人群
用结局或 p 值挑 PS 公式 污染设计并产生选择性结果 设计期隐藏结局,只按平衡与支持选择
按治疗模型 p 值删协变量 预测显著性不是混杂判断 用时间顺序、DAG 与领域知识
纳入治疗后变量 可能阻断效应或打开碰撞路径 只用时间零点前变量
只报告 PS 模型 AUC 预测治疗不等于形成平衡 报每个协变量的匹配后诊断
用基线 t 检验评价平衡 p 值强烈依赖样本量 以 SMD、VR、eCDF 和分布为主
只看平均 SMD 可掩盖某个严重失衡变量 报全部变量和最大绝对值
PS 平衡就不看协变量 相似 PS 可对应不同协变量组合 逐变量、非线性和交互诊断
caliper 尺度说不清 0.2 在概率与 logit 尺度含义不同 报 link、标准化方式和实际宽度
忽略未匹配治疗者 estimand 与外推人群已改变 报样本流并描述被排除者
丢掉 weights/subclass 点估计或标准误不再对应设计 用 match_data() 输出完整分析
replacement 后把重复对照当独立 低估不确定性 保留 id、权重与聚类结构
匹配后再做普通独立 t 检验 忽略配对依赖 1:1 无放回时分析配对差
完整案例后不报缺失 可能产生选择偏倚并改变人群 预先处理缺失并做机制敏感性分析
Love plot 好看就作确定因果结论 只覆盖已测量协变量 使用有条件的因果语言并讨论未测混杂

11 练习与答案

  1. 一项研究希望估计实际接受手术者若改用药物的平均结局差,更接近 ATE 还是 ATT?
  2. PS 模型中某个已知强结局预测因子的治疗系数 p=0.40,是否应删掉?
  3. 1:1 caliper 匹配后 30% 的治疗者未匹配,主要结果仍代表全部治疗者吗?
  4. 匹配后最大 SMD 为 0.06,但年龄 eCDF 尾部仍明显不同,应如何处理?
  5. 有放回匹配中一个对照被使用 12 次,能否当作 12 个独立对照?
  6. 随访中健康状态影响下一次用药,且又受既往用药影响,基线 PSM 是否足够?
  7. 为什么不应通过结局 p 值在 caliper 0.1、0.2、0.3 中选择一个?
  8. 二分类结局匹配分析是否只需报告 McNemar p 值?
显示练习答案
  1. ATT,因为目标人群是实际接受手术者。
  2. 不应仅凭 p 值删除。变量选择取决于时间顺序、因果结构与预后价值。
  3. 不再自动代表全部治疗者;更准确的是保留且有共同支持治疗者中的 ATT,需描述未匹配者。
  4. 在不查看结局时检查函数形式、关键交互、caliper 或匹配方法,并重新完成全部诊断;0.1 只是经验线。
  5. 不能。它仍是同一个人,必须保留 id、重复使用次数/权重并采用相容方差。
  6. 通常不够;这是受既往治疗影响的时间变化混杂,需纵向目标试验与 g-methods 等方法。
  7. 这会让随机结局差异反过来决定设计,造成选择性推断;应按平衡、支持、样本保留与方案选择。
  8. 不够。还要报告风险差、风险比或其他预先指定效应量及置信区间,并保持配对/权重结构。

12 快速参考

12.1 核心公式与 R 入口

目标 公式或代码 解释
倾向评分 e(X)=P(A=1∣X)e(X)=P(A=1\mid X) 给定治疗前变量的治疗概率
ATT E{Y(1)−Y(0)∣A=1}E\{Y(1)-Y(0)\mid A=1\} 实际治疗者中的平均因果效应
logit PS log⁡[e/(1−e)]\log[e/(1-e)] 常用匹配距离尺度
SMD (X‾1−X‾0)/s*(\bar X_1-\bar X_0)/s^* 与样本量关系较小的均值平衡指标
最近邻匹配 matchit(..., method="nearest") 从目标组寻找距离最近比较对象
0.2 SD caliper caliper=.2, std.caliper=TRUE 需同时报告 distance/link 尺度
提取设计样本 match_data(m.out, data=d) 保留 distance、weights、subclass
有放回展开 get_matches(m.out, data=d) 原对照可能重复,保留原始 id
平衡摘要 summary(m.out, un=TRUE, standardize=TRUE) 匹配前后 SMD、VR、eCDF 与人数
1:1 配对差 mean(Y_treated - Y_control) 当前主设计中的 matched-sample ATT

12.2 从原始数据到结果的最小工作流

# 1. 结局不可见的设计数据
design_data <- d[, c("A", "age", "bmi", "smoking", "severity")]

# 2. 预先指定 ATT 方向的 1:1 最近邻匹配
m <- MatchIt::matchit(
  A ~ age + bmi + smoking + severity,
  data = design_data,
  method = "nearest",
  distance = "glm",
  link = "linear.logit",
  estimand = "ATT",
  ratio = 1,
  replace = FALSE,
  caliper = 0.20,
  std.caliper = TRUE,
  m.order = "closest"
)

# 3. 在结局仍不可见时检查样本流、重叠和所有协变量平衡
summary(m, un = TRUE, standardize = TRUE)

# 4. 冻结设计,才合并结局;保留 weights 与 subclass
matched <- MatchIt::match_data(m, data = d)

# 5. 采用与匹配结构和结局类型相容的效应与方差方法

12.3 最终检查清单

  • 是否写清目标试验、时间零点、治疗策略、结局与 estimand?
  • 匹配失败后是否重新准确命名目标人群?
  • PS 是否只使用治疗前变量,且选择依据不是 p 值?
  • 设计阶段是否完全不使用结局?
  • link、距离、caliper、ratio、replacement、顺序与 exact 条件是否可复现?
  • 是否报告全部样本流和未匹配者特征?
  • 是否检查 PS 重叠及具体协变量组合的支持?
  • 是否逐变量报告 SMD,并补充 VR、eCDF、非线性与交互诊断?
  • 是否保留并正确使用 weights、subclass 和原始 id?
  • 置信区间是否与 1:1、多对一、有放回或聚类结构相容?
  • 缺失、测量误差、失访与时间变化治疗是否另行处理?
  • 未测量混杂和目标人群外推是否有敏感性分析?
  • 结果是否使用“在识别假设下”的有条件因果语言?

下一步学习方向

建议继续学习 full matching、optimal matching、coarsened exact matching、Mahalanobis within PS calipers、generalized propensity scores、overlap weighting、双重稳健结局模型、多重插补后的匹配、匹配设计的稳健方差、未测量混杂定量偏倚分析,以及纵向治疗的 g-methods。完整因果框架和其他估计器见 因果推断详解。

sessionInfo()
## R version 4.6.1 (2026-06-24)
## Platform: aarch64-apple-darwin23
## Running under: macOS Tahoe 26.5.1
##
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
##
## locale:
## [1] C.UTF-8/C.UTF-8/C.UTF-8/C/C.UTF-8/C.UTF-8
##
## time zone: America/Edmonton
## tzcode source: internal
##
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base
##
## loaded via a namespace (and not attached):
##  [1] backports_1.5.1 digest_0.6.39   R6_2.6.1        fastmap_1.2.0
##  [5] xfun_0.60       cachem_1.1.0    knitr_1.51      htmltools_0.5.9
##  [9] rmarkdown_2.31  lifecycle_1.0.5 cli_3.6.6       sass_0.4.10
## [13] jquerylib_0.1.4 compiler_4.6.1  chk_0.10.0      tools_4.6.1
## [17] evaluate_1.0.5  bslib_0.12.0    Rcpp_1.1.2      yaml_2.3.12
## [21] MatchIt_4.7.2   rlang_1.3.0     jsonlite_2.0.0