关于本教程的数据与结论 所有个体、干预、反事实结果和数值结论均由固定随机种子模拟,仅用于展示方法。实际数据中每个人只能观察一个潜在结局,真实因果效应也不可直接查阅;本教程保留“真值”只是为了检验方法是否找回已知答案。
本教程始终围绕一个问题展开:如果目标人群中的每个人都参加强化血压管理项目,与每个人都接受常规管理相比,6 个月平均收缩压会相差多少?
推荐学习顺序是“明确问题 → 模拟目标试验 → 画因果图 → 写出识别假设 → 选择估计方法 → 检查诊断 → 做敏感性分析 → 透明报告”。代码默认显示,可以逐段运行,也可通过页面工具折叠。
完成本教程后,你应能够:
同一份数据和同一个回归函数可以服务于不同目标,但它们回答的问题不同:
| 任务 | 典型问题 | 主要评价依据 |
|---|---|---|
| 描述 | 参加项目者与未参加者的平均血压相差多少? | 样本、测量与描述是否准确 |
| 预测 | 哪些人 6 个月后可能仍有高血压? | 样本外校准、判别与误差 |
| 因果 | 若同一目标人群参加项目而不是接受常规管理,结局会怎样? | 设计、时间顺序、识别假设与估计 |
“调整后的回归系数”仍可能只是条件关联。因果解释不是由
lm()、glm() 或某个显著 p
值授予的,而来自清晰的干预对比、可信的设计和足以连接观测数据与反事实结果的假设。
目标试验(target trial)不是一定要真正实施的试验,而是一份协议:如果伦理、时间和资源都允许,理想随机试验将如何回答问题。观察性研究可以尝试模拟这份协议。
| 协议要素 | 本教程中的目标试验 |
|---|---|
| 合格标准 | 基线开始接受管理、符合预先规定临床条件的目标人群 |
| 治疗策略 | 立即参加强化项目,或继续常规管理 |
| 分配方式 | 理想试验中随机;观察队列中按已记录因素调整 |
| 时间零点 | 治疗策略确定且合格标准确认的同一时点 |
| 随访 | 从时间零点到 6 个月 |
| 结局 | 6 个月收缩压(mmHg) |
| 因果对比 | 所有人参加项目与所有人接受常规管理的平均差 |
| 分析原则 | 首先估计分配策略的总体平均效应 |
目标试验能暴露很多常被软件掩盖的问题:治疗开始前后是否混在一起?必须“存活到接受治疗”的人是否获得了不死时间?纳入标准是否使用了未来信息?不同组的随访起点是否一致?
时间零点必须对齐 合格标准确认、治疗分配和随访开始若发生在不同时间,选择偏倚与不死时间偏倚可能在模型拟合前就已产生。增加协变量通常无法修复这种设计错位。
令 表示参加项目, 表示常规管理。对个体 :
个体因果效应是 。现实中只能观察其中一个:
缺失的另一个潜在结局不是普通缺失值,不能通过再次测量同一个人在同一时点获得。这就是因果推断的基本问题。研究设计与统计方法的任务,是在群体层面构造可信的反事实比较。
最常见的平均处理效应(average treatment effect,ATE)为:
本教程使用“项目减去常规管理”的方向,因此负值表示项目降低血压。另两个常见目标是:
| 估计目标 | 定义 | 目标人群 |
|---|---|---|
| ATE | 整个研究目标人群 | |
| ATT | 实际接受项目者 | |
| CATE | 具有特征 的亚组 |
若治疗效应因基线血压而异,而且项目参加者更常具有较高基线血压,ATE 与 ATT 就可能不同。选择权重或匹配方法之前必须先选择目标;不能在看到哪个结果更显著后再改目标人群。
因为模拟数据保留了不可见的两个潜在结局,我们知道当前已实现模拟队列的有限样本真值:ATE
为 -5.06 mmHg,实际参加项目者中的 ATT 为 -5.16
mmHg。后续分析只向方法提供通常能够观察到的
causal_data。
一致性要求实际接受 的人,其观察结局等于相应潜在结局:若 ,则 。它还隐含治疗版本定义足够明确。
可能威胁一致性的情形包括:
随机试验通过随机化力求使 。观察性研究通常只能主张:给定一组治疗前共同原因 后,
这常被称为“无未测量混杂”。它无法只凭数据检验证明,需要结合领域知识、测量质量、时间顺序和敏感性分析。把很多变量自动塞入模型,并不能保证所有重要共同原因都被正确测量。
对所有目标人群中有正概率出现的 ,需要:
结构性正值性违背是某类人按规则不可能接受某策略,例如绝对禁忌者不可能用药;改变模型无法创造缺失的反事实信息。实际正值性不足则是有限样本中某些组合几乎只接受一种策略,表现为倾向评分接近 0 或 1、极端权重和不稳定估计。
有向无环图(directed acyclic graph,DAG)用箭头表达被假设的直接因果关系。DAG 不由数据自动发现;它是研究者对数据生成过程的明确陈述。
draw_node <- function(x, y, label, fill = "white", width = 0.16) {
rect(x - width / 2, y - 0.055, x + width / 2, y + 0.055,
col = fill, border = palette_ci["navy"], lwd = 1.7)
text(x, y, label, cex = 0.88)
}
draw_arrow <- function(x0, y0, x1, y1, color = palette_ci["gray"]) {
arrows(x0, y0, x1, y1, length = 0.08, lwd = 1.8, col = color)
}
draw_path_arrow <- function(x, y, color = palette_ci["gray"]) {
lines(x[-length(x)], y[-length(y)], lwd = 1.8, col = color)
arrows(
x[length(x) - 1], y[length(y) - 1], x[length(x)], y[length(y)],
length = 0.08, lwd = 1.8, col = color
)
}
plot.new()
plot.window(xlim = c(0, 1), ylim = c(0, 1))
draw_node(0.13, 0.75, "L\nBaseline causes", "#E8F2F1", 0.22)
draw_node(0.39, 0.75, "A\nProgram", "#E7F1FA")
draw_node(0.64, 0.75, "M\nEngagement", "#FFF3D8")
draw_node(0.87, 0.75, "Y\n6-month SBP", "#FCE8E4")
draw_arrow(0.24, 0.75, 0.30, 0.75)
draw_arrow(0.47, 0.75, 0.55, 0.75)
draw_arrow(0.72, 0.75, 0.79, 0.75)
draw_path_arrow(c(0.13, 0.13, 0.87, 0.87),
c(0.81, 0.89, 0.89, 0.81))
draw_path_arrow(c(0.39, 0.39, 0.87, 0.87),
c(0.69, 0.61, 0.61, 0.69), palette_ci["blue"])
draw_node(0.13, 0.27, "A\nProgram", "#E7F1FA")
draw_node(0.40, 0.27, "S\nPost-treatment visit", "#F5E7F2", 0.22)
draw_node(0.67, 0.27, "U\nLatent need", "#EEEEEE", 0.20)
draw_node(0.87, 0.27, "Y\n6-month SBP", "#FCE8E4")
draw_arrow(0.21, 0.27, 0.31, 0.27)
draw_arrow(0.59, 0.27, 0.49, 0.27)
draw_arrow(0.75, 0.27, 0.79, 0.27)
text(
0.50, 0.12,
"S is the collider A -> S <- U; conditioning on S opens a noncausal path",
cex = 0.78
)
box()治疗前共同原因、中介与碰撞点的简化因果图
第一行中, 且 ,所以路径 是后门路径,需要通过设计或分析阻断。 位于 上,是项目作用的一部分;估计总效应时通常不应调整。
第二行中,治疗后的就诊 同时受到项目 和未测量健康需求 影响。若限制为“至少就诊一次者”或把 纳入模型,会在 与 之间产生条件关联,从而打开 。
一个调整集需要阻断从 指向 的所有后门路径。估计总效应时,一个安全的入门规则是不纳入治疗后的中介或碰撞点;更一般的调整准则还需逐图判断。通常优先选择最小充分集,而不是“所有能获得的变量”。本教程主分析的合理集合是治疗前的年龄、基线血压、吸烟、城乡和健康素养。
| 变量角色 | 图中结构 | 估计总效应时的通常处理 |
|---|---|---|
| 混杂因素 | 调整以阻断后门路径 | |
| 纯结局预测因素 | 可提高精度,但不是消除混杂所必需 | |
| 工具变量 | 普通结果回归中通常无需调整 | |
| 中介 | 估计总效应时不调整 | |
| 碰撞点 | 不条件化、不分层、不按其选择样本 | |
| 暴露代理或结果代理 | 测量结构取决于具体过程 | 不能仅凭相关性决定 |
“治疗前测量”不是充分理由 变量发生在治疗前,并不自动意味着它是混杂因素。共同原因、工具变量、碰撞点祖先和仅影响精度的变量具有不同角色;应先依据时间与领域机制画图,再决定如何使用。
模拟数据中的 engagement_score
是治疗后的中介,clinic_contact
是碰撞点。下面比较合理的治疗前调整、加入中介以及加入碰撞点后的项目系数。系数不是所有情况下的正式因果直接效应;这里只展示“多调变量”会改变问题或引入偏倚。
model_pre_treatment <- lm(
six_month_sbp ~ program_num + age_c10 + baseline_sbp_c10 +
smoking_num + rural_num + health_literacy,
data = causal_data
)
model_with_mediator <- update(
model_pre_treatment,
. ~ . + engagement_score
)
model_with_collider <- update(
model_pre_treatment,
. ~ . + clinic_contact_num
)
bad_control_table <- data.frame(
模型 = c("仅治疗前共同原因", "再加入治疗后中介", "再加入治疗后碰撞点"),
项目系数 = c(
coef(model_pre_treatment)["program_num"],
coef(model_with_mediator)["program_num"],
coef(model_with_collider)["program_num"]
),
回答的问题 = c(
"在识别假设下接近总效应",
"阻断部分作用路径,问题已改变",
"可能打开非因果路径"
),
check.names = FALSE
)
knitr::kable(
bad_control_table,
digits = 2,
caption = "不同调整变量下的项目回归系数"
)| 模型 | 项目系数 | 回答的问题 |
|---|---|---|
| 仅治疗前共同原因 | -5.06 | 在识别假设下接近总效应 |
| 再加入治疗后中介 | -2.65 | 阻断部分作用路径,问题已改变 |
| 再加入治疗后碰撞点 | -5.30 | 可能打开非因果路径 |
总效应真值约为 -5.06 mmHg。加入参与度后,项目系数主要保留未通过该中介的部分路径;加入就诊则可能使项目与未测健康需求发生人为关联。是否称为“直接效应”还需要明确中介干预、额外识别假设以及处理治疗—中介交互。
若在同一目标人群中以固定概率随机分配项目,治疗前特征平均而言不会系统决定分组。随机化并不保证每个有限样本完全平衡,但为随机误差的量化和因果解释提供了设计基础。
下面把同一组模拟参与者重新随机分配。为避免偷看不可观察的反事实,分析时只用随机分配下实际出现的结果。
set.seed(20260811)
random_program <- rbinom(n, 1, 0.5)
random_outcome <- ifelse(
random_program == 1,
causal_truth$y1,
causal_truth$y0
)
trial_difference <- with(
data.frame(random_program, random_outcome),
mean(random_outcome[random_program == 1]) -
mean(random_outcome[random_program == 0])
)
trial_fit <- lm(random_outcome ~ random_program)
trial_ci <- confint(trial_fit)["random_program", ]
trial_result <- data.frame(
指标 = c("随机试验均值差", "95% CI 下限", "95% CI 上限", "模拟 ATE 真值"),
mmHg = c(trial_difference, trial_ci[1], trial_ci[2], true_ate)
)
knitr::kable(
trial_result,
digits = 2,
caption = "一次模拟随机试验的意向治疗对比"
)| 指标 | mmHg |
|---|---|
| 随机试验均值差 | -5.61 |
| 95% CI 下限 | -6.51 |
| 95% CI 上限 | -4.71 |
| 模拟 ATE 真值 | -5.06 |
一次随机试验的估计不必恰好等于真值;它会受随机分配与抽样变异影响。增加治疗前的强结局预测因素可提高精度,但随机化本身才是可交换性的主要来源。这里展示的是普通线性模型近似区间;正式试验应按随机化方案、异方差、分层或集群设计采用设计一致的推断。
stopifnot(
nrow(causal_data) == nrow(causal_truth),
all(causal_data$program_num %in% c(0, 1)),
!anyNA(causal_data),
all(causal_data$baseline_sbp > 0),
all(causal_data$six_month_sbp > 0)
)
preview <- head(causal_data[, c(
"participant_id", "age", "rural", "smoking", "baseline_sbp",
"health_literacy", "program", "six_month_sbp"
)])
knitr::kable(
preview,
col.names = c(
"参与者", "年龄", "居住地", "吸烟", "基线 SBP", "健康素养",
"管理策略", "6 月 SBP"
),
digits = 1,
caption = "模拟观察队列的前 6 行"
)| 参与者 | 年龄 | 居住地 | 吸烟 | 基线 SBP | 健康素养 | 管理策略 | 6 月 SBP |
|---|---|---|---|---|---|---|---|
| C0001 | 36 | Rural | Not current | 142 | -1.9 | Usual care | 132 |
| C0002 | 42 | Urban | Not current | 149 | 0.1 | Usual care | 135 |
| C0003 | 65 | Rural | Not current | 150 | 0.4 | Usual care | 148 |
| C0004 | 42 | Urban | Not current | 128 | 0.1 | Usual care | 116 |
| C0005 | 48 | Rural | Not current | 150 | -1.6 | Usual care | 147 |
| C0006 | 69 | Urban | Not current | 137 | 3.2 | Program | 116 |
分析单位是参与者;治疗前变量在项目开始前测量,结局在 6 个月测量。中介和治疗后就诊保留在数据中用于警示,但不进入主分析的混杂调整集。
crude_summary <- aggregate(
cbind(baseline_sbp, six_month_sbp) ~ program,
data = causal_data,
FUN = mean
)
crude_difference <- with(
causal_data,
mean(six_month_sbp[program_num == 1]) -
mean(six_month_sbp[program_num == 0])
)
knitr::kable(
crude_summary,
col.names = c("管理策略", "平均基线 SBP", "平均 6 月 SBP"),
digits = 1,
caption = "按实际项目参加状态的粗描述"
)| 管理策略 | 平均基线 SBP | 平均 6 月 SBP |
|---|---|---|
| Usual care | 145 | 136 |
| Program | 150 | 136 |
未经调整的结局均值差为 -0.65 mmHg,而 ATE 真值为 -5.06 mmHg。参加者在治疗前已有更高基线血压,说明“项目组结局更高或下降不明显”不能直接说明项目无效。这里存在典型的适应证混杂:更需要干预的人更可能获得干预。
标准化均值差(standardized mean difference,SMD)不随样本量直接放大,适合描述组间基线差异:
它不是混杂检验,也没有神奇阈值。常以 作为粗略诊断参考,但还要检查分布形状、极端值、重要交互和非线性项。
balance_variables <- list(
"年龄" = causal_data$age,
"基线 SBP" = causal_data$baseline_sbp,
"当前吸烟" = causal_data$smoking_num,
"乡村居住" = causal_data$rural_num,
"健康素养" = causal_data$health_literacy
)
unadjusted_smd <- vapply(
balance_variables,
standardized_difference,
numeric(1),
z = causal_data$program_num
)
balance_unadjusted <- data.frame(
协变量 = names(unadjusted_smd),
SMD = unname(unadjusted_smd),
绝对SMD = abs(unname(unadjusted_smd)),
check.names = FALSE
)
knitr::kable(
balance_unadjusted,
digits = 2,
caption = "未调整的治疗前协变量平衡"
)| 协变量 | SMD | 绝对SMD |
|---|---|---|
| 年龄 | 0.46 | 0.46 |
| 基线 SBP | 0.64 | 0.64 |
| 当前吸烟 | 0.32 | 0.32 |
| 乡村居住 | 0.00 | 0.00 |
| 健康素养 | 0.08 | 0.08 |
不要用基线变量的 p 值筛选调整项。大样本中很小差异也可能显著,小样本中重要不平衡也可能不显著;更重要的是变量在因果结构中的角色和不平衡的实际大小。
标准化(standardization)先估计 ,再让目标人群中的每个人分别处于 和 ,最后对个体预测求平均。它得到的是目标人群中的边际效应,而不是只对应某个“平均人”的回归系数。
本例允许项目效应随基线血压线性变化,因为数据生成机制和科学问题都支持这种异质性。
outcome_model <- lm(
six_month_sbp ~
program_num * baseline_sbp_c10 +
age_c10 + smoking_num + rural_num + health_literacy,
data = causal_data
)
data_program <- transform(causal_data, program_num = 1)
data_usual <- transform(causal_data, program_num = 0)
predicted_y1 <- predict(outcome_model, newdata = data_program)
predicted_y0 <- predict(outcome_model, newdata = data_usual)
gcomp_means <- c(
program = mean(predicted_y1),
usual_care = mean(predicted_y0)
)
gcomp_ate <- unname(gcomp_means["program"] - gcomp_means["usual_care"])
gcomp_table <- data.frame(
策略 = c("所有人参加项目", "所有人接受常规管理", "ATE:项目减常规管理"),
标准化平均血压 = unname(c(gcomp_means, gcomp_ate)),
单位 = "mmHg",
row.names = NULL
)
knitr::kable(
gcomp_table,
digits = 2,
caption = "由结局模型标准化得到的反事实总体均值"
)| 策略 | 标准化平均血压 | 单位 |
|---|---|---|
| 所有人参加项目 | 133.27 | mmHg |
| 所有人接受常规管理 | 138.32 | mmHg |
| ATE:项目减常规管理 | -5.05 | mmHg |
标准化估计的 ATE 为 -5.05 mmHg,接近模拟真值 -5.06 mmHg。这里的计算步骤值得逐一核对:
data_program 与 data_usual 保留同一批人的
,只改变治疗策略;模型含有 program_num * baseline_sbp_c10 交互。此时
program_num 系数表示基线 SBP 为 145 mmHg 时的条件对比,而
ATE 将不同基线血压者的预测对比按目标人群分布平均。
即使没有交互,逻辑回归、Cox 回归等非线性模型的条件比值也通常不等于边际风险或总体平均因果效应。因此不要把一个条件 OR、HR 或特定参照值下的系数自动命名为 ATE。
conditional_program_coefficient <- coef(outcome_model)["program_num"]
coefficient_comparison <- data.frame(
数量 = c("项目主效应回归系数", "标准化 ATE"),
估计值 = unname(c(conditional_program_coefficient, gcomp_ate)),
解释 = c(
"基线 SBP=145 mmHg 时的条件效应",
"当前目标人群分布上的边际平均效应"
)
)
knitr::kable(
coefficient_comparison,
digits = 2,
caption = "条件回归系数与边际标准化效应"
)| 数量 | 估计值 | 解释 |
|---|---|---|
| 项目主效应回归系数 | -5.03 | 基线 SBP=145 mmHg 时的条件效应 |
| 标准化 ATE | -5.05 | 当前目标人群分布上的边际平均效应 |
标准误需要同时反映结局模型拟合和标准化。非参数 bootstrap 每次重抽参与者、重新拟合模型并重新标准化,是一种直观实现。
estimate_gcomp <- function(data, index) {
d <- data[index, , drop = FALSE]
fit <- lm(
six_month_sbp ~
program_num * baseline_sbp_c10 +
age_c10 + smoking_num + rural_num + health_literacy,
data = d
)
d1 <- transform(d, program_num = 1)
d0 <- transform(d, program_num = 0)
individual_contrast <-
predict(fit, newdata = d1) - predict(fit, newdata = d0)
high_baseline <- d$baseline_sbp >= 150
c(
ATE = mean(individual_contrast),
low_baseline = mean(individual_contrast[!high_baseline]),
high_baseline = mean(individual_contrast[high_baseline])
)
}
set.seed(20260812)
n_boot <- 250
gcomp_bootstrap <- replicate(
n_boot,
estimate_gcomp(causal_data, sample.int(nrow(causal_data), replace = TRUE))
)
gcomp_ci <- unname(
quantile(gcomp_bootstrap["ATE", ], c(0.025, 0.975))
)
data.frame(
方法 = "标准化",
ATE = gcomp_ate,
CI下限 = gcomp_ci[1],
CI上限 = gcomp_ci[2],
Bootstrap次数 = n_boot,
check.names = FALSE
) |>
knitr::kable(
digits = 2,
caption = "标准化 ATE 的百分位 bootstrap 区间"
)| 方法 | ATE | CI下限 | CI上限 | Bootstrap次数 |
|---|---|---|---|---|
| 标准化 | -5.05 | -5.63 | -4.43 | 250 |
为兼顾教程渲染速度,这里只运行 250 次 bootstrap;正式分析通常应使用至少 1000 次并检查 Monte Carlo 误差。该区间采用个体独立同分布抽样的超总体解释,只表达当前抽样与建模过程下的统计不确定性,不包含未测量混杂、错误 DAG、暴露误分类、选择偏倚或干预定义含糊造成的识别不确定性。
倾向评分(propensity score)定义为:
它是根据治疗前协变量预测治疗的概率,不是结局风险,也不是个体治疗效应。倾向评分的主要用途是构建协变量分布可比的伪总体、匹配集或分层。
propensity_model <- glm(
program_num ~
age_c10 + baseline_sbp_c10 + smoking_num + rural_num +
health_literacy,
family = binomial(),
data = causal_data
)
propensity_hat <- clamp_probability(
predict(propensity_model, type = "response"),
epsilon = 1e-6
)
propensity_summary <- aggregate(
propensity_hat,
by = list(策略 = causal_data$program),
FUN = function(x) c(
min = min(x), q25 = quantile(x, 0.25),
median = median(x), q75 = quantile(x, 0.75), max = max(x)
)
)
propensity_quantiles <- propensity_summary[[2]]
propensity_summary <- data.frame(
策略 = propensity_summary$策略,
propensity_quantiles,
row.names = NULL,
check.names = FALSE
)
knitr::kable(
propensity_summary,
digits = 2,
caption = "两组估计倾向评分的分布"
)| 策略 | min | q25.25% | median | q75.75% | max |
|---|---|---|---|---|---|
| Usual care | 0.05 | 0.24 | 0.34 | 0.46 | 0.88 |
| Program | 0.09 | 0.35 | 0.47 | 0.60 | 0.92 |
倾向评分模型的目标不是追求最高分类准确率或最大的 AUC。一个几乎完美区分治疗组的模型反而提示重叠不足。变量选择应来自因果结构;拟合后关注的是重叠与加权平衡。
hist(
propensity_hat[causal_data$program_num == 0],
breaks = seq(0, 1, by = 0.04), probability = TRUE,
col = grDevices::adjustcolor(palette_ci["orange"], alpha.f = 0.50),
border = "white", xlim = c(0, 1),
xlab = "Estimated propensity score", ylab = "Density",
main = "Propensity score overlap"
)
hist(
propensity_hat[causal_data$program_num == 1],
breaks = seq(0, 1, by = 0.04), probability = TRUE,
col = grDevices::adjustcolor(palette_ci["teal"], alpha.f = 0.50),
border = "white", add = TRUE
)
legend(
"topright",
legend = c("Usual care", "Program"),
fill = grDevices::adjustcolor(
c(palette_ci["orange"], palette_ci["teal"]), 0.50
),
border = NA, bty = "n"
)按实际管理策略分组的估计倾向评分分布
共同支持区域内,两组都有相似 的参与者。若某区域只有一组,ATE 需要依赖模型外推;此时可以重新定义目标人群、限制到共同支持、改变估计目标,或承认数据无法回答原问题。任何限制都要说明新的目标人群。
ATE 的稳定逆概率治疗权重为:
分母在每个协变量模式中重新平衡治疗,分子稳定权重的尺度。加权后,理想伪总体中的治疗与已测量 近似独立。
treatment_prevalence <- mean(causal_data$program_num)
stabilized_weight <- ifelse(
causal_data$program_num == 1,
treatment_prevalence / propensity_hat,
(1 - treatment_prevalence) / (1 - propensity_hat)
)
effective_sample_size <- function(w) {
sum(w)^2 / sum(w^2)
}
weight_summary <- data.frame(
指标 = c(
"最小值", "第 1 百分位", "中位数", "第 99 百分位", "最大值",
"权重均值", "总体有效样本量", "项目组有效样本量",
"常规管理组有效样本量"
),
数值 = unname(c(
min(stabilized_weight),
quantile(stabilized_weight, 0.01),
median(stabilized_weight),
quantile(stabilized_weight, 0.99),
max(stabilized_weight),
mean(stabilized_weight),
effective_sample_size(stabilized_weight),
effective_sample_size(stabilized_weight[causal_data$program_num == 1]),
effective_sample_size(stabilized_weight[causal_data$program_num == 0])
))
)
knitr::kable(
weight_summary,
digits = 2,
caption = "稳定逆概率权重诊断"
)| 指标 | 数值 |
|---|---|
| 最小值 | 0.44 |
| 第 1 百分位 | 0.50 |
| 中位数 | 0.89 |
| 第 99 百分位 | 2.78 |
| 最大值 | 4.84 |
| 权重均值 | 1.00 |
| 总体有效样本量 | 1848.07 |
| 项目组有效样本量 | 696.86 |
| 常规管理组有效样本量 | 1157.13 |
稳定权重的均值通常应接近 1。有效样本量(effective sample size,ESS)为:
ESS 明显低于原始样本量,说明估计由少数高权重观测主导。它是诊断摘要,不等于真正独立信息量,也不能取代适用于加权估计的方差方法。
weighted_smd <- vapply(
balance_variables,
standardized_difference,
numeric(1),
z = causal_data$program_num,
w = stabilized_weight
)
balance_table <- data.frame(
协变量 = names(unadjusted_smd),
调整前SMD = unname(unadjusted_smd),
加权后SMD = unname(weighted_smd),
check.names = FALSE
)
knitr::kable(
balance_table,
digits = 2,
caption = "逆概率加权前后的治疗前协变量平衡"
)| 协变量 | 调整前SMD | 加权后SMD |
|---|---|---|
| 年龄 | 0.46 | -0.02 |
| 基线 SBP | 0.64 | -0.02 |
| 当前吸烟 | 0.32 | 0.00 |
| 乡村居住 | 0.00 | -0.01 |
| 健康素养 | 0.08 | 0.00 |
old_margin <- par("mar")
par(mar = c(5.1, 8.2, 4.1, 2.1))
plot(
abs(unadjusted_smd), seq_along(unadjusted_smd),
pch = 16, col = palette_ci["orange"],
xlim = c(0, max(0.35, abs(unadjusted_smd), abs(weighted_smd))),
ylim = c(0.5, length(unadjusted_smd) + 0.5),
yaxt = "n", xlab = "Absolute SMD", ylab = "",
main = "Covariate balance"
)
balance_plot_labels <- c(
"Age", "Baseline SBP", "Current smoking", "Rural residence",
"Health literacy"
)
axis(2, at = seq_along(unadjusted_smd), labels = balance_plot_labels, las = 1)
points(abs(weighted_smd), seq_along(weighted_smd),
pch = 17, col = palette_ci["teal"])
abline(v = 0.10, lty = 2, col = palette_ci["gray"])
legend(
"topright", legend = c("Unadjusted", "Weighted", "0.10 reference"),
pch = c(16, 17, NA), lty = c(NA, NA, 2),
col = c(palette_ci["orange"], palette_ci["teal"], palette_ci["gray"]),
bty = "n"
)逆概率加权前后的绝对标准化差异
平衡图只检查已经测量并展示的变量。漂亮的平衡不能排除未测量混杂,也不能补救错误的时间零点、选择机制或测量误差。
倾向评分模型以平衡为诊断目标 若重要协变量加权后仍不平衡,应重新检查变量编码、非线性、交互、重叠和治疗机制,而不是先看哪个模型给出更理想的结局效应。
组内归一化后的加权均值避免把有限样本中的权重总和偏离目标规模直接带入均值:
ipw_y1 <- weighted_mean(
causal_data$six_month_sbp[causal_data$program_num == 1],
stabilized_weight[causal_data$program_num == 1]
)
ipw_y0 <- weighted_mean(
causal_data$six_month_sbp[causal_data$program_num == 0],
stabilized_weight[causal_data$program_num == 0]
)
ipw_ate <- ipw_y1 - ipw_y0
ipw_table <- data.frame(
数量 = c("项目策略加权均值", "常规策略加权均值", "IPW ATE"),
估计值 = c(ipw_y1, ipw_y0, ipw_ate),
单位 = "mmHg"
)
knitr::kable(
ipw_table,
digits = 2,
caption = "逆概率加权得到的边际平均结果"
)| 数量 | 估计值 | 单位 |
|---|---|---|
| 项目策略加权均值 | 133.06 | mmHg |
| 常规策略加权均值 | 138.33 | mmHg |
| IPW ATE | -5.27 | mmHg |
estimate_ipw <- function(data, index) {
d <- data[index, , drop = FALSE]
ps_fit <- glm(
program_num ~
age_c10 + baseline_sbp_c10 + smoking_num + rural_num +
health_literacy,
family = binomial(),
data = d
)
ps <- clamp_probability(predict(ps_fit, type = "response"), 1e-6)
prevalence <- mean(d$program_num)
w <- ifelse(
d$program_num == 1,
prevalence / ps,
(1 - prevalence) / (1 - ps)
)
weighted_mean(d$six_month_sbp[d$program_num == 1], w[d$program_num == 1]) -
weighted_mean(d$six_month_sbp[d$program_num == 0], w[d$program_num == 0])
}
set.seed(20260813)
ipw_bootstrap <- replicate(
n_boot,
estimate_ipw(causal_data, sample.int(nrow(causal_data), replace = TRUE))
)
ipw_ci <- unname(quantile(ipw_bootstrap, c(0.025, 0.975)))
data.frame(
方法 = "稳定 IPW",
ATE = ipw_ate,
CI下限 = ipw_ci[1],
CI上限 = ipw_ci[2],
Bootstrap次数 = n_boot,
check.names = FALSE
) |>
knitr::kable(
digits = 2,
caption = "IPW ATE 的百分位 bootstrap 区间"
)| 方法 | ATE | CI下限 | CI上限 | Bootstrap次数 |
|---|---|---|---|---|
| 稳定 IPW | -5.27 | -6.01 | -4.54 | 250 |
这里每次 bootstrap
都重新估计倾向评分和权重。把一次拟合得到的权重当作固定值、再使用普通加权
lm()
的默认标准误,通常不能正确反映估计权重带来的不确定性。
匹配为治疗者寻找治疗前特征相近的对照。常见的 1:1 最近邻匹配更自然地估计被成功匹配治疗者中的 ATT,而不是原始总体 ATE。未匹配者被排除后,必须描述新的分析人群。
下面用 base R 演示倾向评分 logit 上的无放回贪婪匹配。生产分析应使用经过验证的软件,以便明确处理顺序、并列、替换、卡钳、权重和方差。
继续学习完整 PSM 工作流
本节用于连接因果识别与匹配思想。有关 MatchIt
实作、样本流、重叠、SMD、Love plot、匹配权重、ATT
推断与设计敏感性的完整案例,请进入
倾向评分匹配详解。
greedy_match <- function(z, logit_ps, caliper, seed = 1) {
treated <- which(z == 1)
available_controls <- which(z == 0)
set.seed(seed)
treated <- sample(treated)
matched_treated <- integer(0)
matched_control <- integer(0)
for (treated_id in treated) {
if (!length(available_controls)) break
distance <- abs(logit_ps[available_controls] - logit_ps[treated_id])
nearest_position <- which.min(distance)
if (distance[nearest_position] <= caliper) {
matched_treated <- c(matched_treated, treated_id)
matched_control <- c(
matched_control, available_controls[nearest_position]
)
available_controls <- available_controls[-nearest_position]
}
}
data.frame(treated = matched_treated, control = matched_control)
}
logit_propensity <- qlogis(propensity_hat)
match_caliper <- 0.20 * sd(logit_propensity)
matched_pairs <- greedy_match(
causal_data$program_num,
logit_propensity,
caliper = match_caliper,
seed = 20260814
)
pair_difference <-
causal_data$six_month_sbp[matched_pairs$treated] -
causal_data$six_month_sbp[matched_pairs$control]
matched_att_ci <- mean_ci(pair_difference)
true_matched_att <- mean(
causal_truth$individual_effect[matched_pairs$treated]
)
matched_result <- data.frame(
指标 = c(
"原项目组人数", "成功匹配对数", "未匹配项目组人数",
"匹配 ATT(演示性)", "演示性 95% CI 下限", "演示性 95% CI 上限",
"成功匹配项目者的模拟 ATT 真值", "全部项目者的模拟 ATT 真值"
),
数值 = unname(c(
sum(causal_data$program_num == 1),
nrow(matched_pairs),
sum(causal_data$program_num == 1) - nrow(matched_pairs),
matched_att_ci["estimate"], matched_att_ci["lower"],
matched_att_ci["upper"], true_matched_att, true_att
))
)
knitr::kable(
matched_result,
digits = 2,
caption = "1:1 最近邻倾向评分匹配结果"
)| 指标 | 数值 |
|---|---|
| 原项目组人数 | 888.00 |
| 成功匹配对数 | 743.00 |
| 未匹配项目组人数 | 145.00 |
| 匹配 ATT(演示性) | -5.08 |
| 演示性 95% CI 下限 | -5.92 |
| 演示性 95% CI 上限 | -4.24 |
| 成功匹配项目者的模拟 ATT 真值 | -5.11 |
| 全部项目者的模拟 ATT 真值 | -5.16 |
matched_indices <- c(matched_pairs$treated, matched_pairs$control)
matched_z <- c(
rep(1, nrow(matched_pairs)),
rep(0, nrow(matched_pairs))
)
matched_smd <- vapply(
balance_variables,
function(x) standardized_difference(x[matched_indices], matched_z),
numeric(1)
)
data.frame(
协变量 = names(matched_smd),
匹配前SMD = unname(unadjusted_smd),
匹配后SMD = unname(matched_smd),
check.names = FALSE
) |>
knitr::kable(
digits = 2,
caption = "匹配前后的治疗前协变量平衡"
)| 协变量 | 匹配前SMD | 匹配后SMD |
|---|---|---|
| 年龄 | 0.46 | 0.03 |
| 基线 SBP | 0.64 | 0.00 |
| 当前吸烟 | 0.32 | -0.03 |
| 乡村居住 | 0.00 | 0.03 |
| 健康素养 | 0.08 | 0.02 |
匹配后的成对均值差只适用于成功匹配的人。表中的配对 t 区间把已经形成的匹配对视为固定,未完整反映倾向评分估计和匹配算法的不连续性,因此只是教学演示,不是通用的匹配方差模板。不同匹配算法可能得到不同样本;应预先说明距离、卡钳、替换、匹配比例、处理顺序、排除人数、平衡和适合设计的方差估计。
增广逆概率加权(augmented inverse probability weighting,AIPW)把标准化和 IPW 组合起来。定义 ,其 ATE 估计量为:
前半是结局模型预测差;后两项用加权残差修正预测误差。在常规条件下,倾向评分模型或结局模型有一个正确设定即可得到一致估计,因此称为“双重稳健”。
交叉拟合(cross-fitting)在一部分数据训练 nuisance 模型,在未用于拟合的折中生成倾向评分和结局预测。复杂机器学习模型尤其需要这种样本分离;本例用简单回归展示计算结构。
set.seed(20260815)
k_folds <- 5
fold_id <- integer(nrow(causal_data))
for (treatment_level in 0:1) {
level_index <- which(causal_data$program_num == treatment_level)
fold_id[level_index] <- sample(
rep(seq_len(k_folds), length.out = length(level_index))
)
}
crossfit_ps <- crossfit_m1 <- crossfit_m0 <- rep(NA_real_, nrow(causal_data))
for (fold in seq_len(k_folds)) {
test_index <- which(fold_id == fold)
train_data <- causal_data[fold_id != fold, , drop = FALSE]
test_data <- causal_data[test_index, , drop = FALSE]
ps_fit <- glm(
program_num ~
age_c10 + baseline_sbp_c10 + smoking_num + rural_num +
health_literacy,
family = binomial(),
data = train_data
)
outcome_fit_1 <- lm(
six_month_sbp ~
age_c10 + baseline_sbp_c10 + smoking_num + rural_num +
health_literacy,
data = train_data[train_data$program_num == 1, , drop = FALSE]
)
outcome_fit_0 <- lm(
six_month_sbp ~
age_c10 + baseline_sbp_c10 + smoking_num + rural_num +
health_literacy,
data = train_data[train_data$program_num == 0, , drop = FALSE]
)
crossfit_ps[test_index] <- clamp_probability(
predict(ps_fit, newdata = test_data, type = "response"),
1e-6
)
crossfit_m1[test_index] <- predict(outcome_fit_1, newdata = test_data)
crossfit_m0[test_index] <- predict(outcome_fit_0, newdata = test_data)
}
stopifnot(
!anyNA(crossfit_ps),
!anyNA(crossfit_m1),
!anyNA(crossfit_m0)
)
aipw_score <-
crossfit_m1 - crossfit_m0 +
causal_data$program_num *
(causal_data$six_month_sbp - crossfit_m1) / crossfit_ps -
(1 - causal_data$program_num) *
(causal_data$six_month_sbp - crossfit_m0) / (1 - crossfit_ps)
aipw_ate <- mean(aipw_score)
aipw_se <- sd(aipw_score) / sqrt(length(aipw_score))
aipw_ci <- aipw_ate + qnorm(c(0.025, 0.975)) * aipw_se
data.frame(
方法 = "治疗组内分层的 5 折交叉拟合 AIPW",
ATE = aipw_ate,
标准误 = aipw_se,
CI下限 = aipw_ci[1],
CI上限 = aipw_ci[2],
check.names = FALSE
) |>
knitr::kable(
digits = 2,
caption = "AIPW 对总体平均处理效应的估计"
)| 方法 | ATE | 标准误 | CI下限 | CI上限 |
|---|---|---|---|---|
| 治疗组内分层的 5 折交叉拟合 AIPW | -5.04 | 0.35 | -5.74 | -4.35 |
这里的区间是在参与者独立同分布抽样和常规正则条件下,依据交叉拟合 AIPW 经验影响函数得到的正态近似区间;聚类、重复测量或复杂抽样需要相应的方差处理。
双重稳健不是“双倍保险” AIPW 不能修复未测量混杂、错误时间顺序、一致性失败、严重正值性违背或两套模型同时错误。极端倾向评分还会放大残差项;交叉拟合也不能创造不存在的治疗对比。
crude_fit <- lm(six_month_sbp ~ program_num, data = causal_data)
crude_ci <- confint(crude_fit)["program_num", ]
ate_comparison <- data.frame(
方法 = c("粗均值差", "标准化", "稳定 IPW", "交叉拟合 AIPW", "模拟真值"),
估计值 = c(crude_difference, gcomp_ate, ipw_ate, aipw_ate, true_ate),
CI下限 = c(crude_ci[1], gcomp_ci[1], ipw_ci[1], aipw_ci[1], NA),
CI上限 = c(crude_ci[2], gcomp_ci[2], ipw_ci[2], aipw_ci[2], NA),
估计目标 = c("观察组粗差(非因果 estimand)", rep("ATE", 4)),
check.names = FALSE
)
ate_comparison_display <- ate_comparison
for (column in c("估计值", "CI下限", "CI上限")) {
values <- ate_comparison_display[[column]]
ate_comparison_display[[column]] <- ifelse(
is.na(values), "—", formatC(values, digits = 2, format = "f")
)
}
knitr::kable(
ate_comparison_display,
caption = "粗观察关联与三种混杂调整 ATE 的比较"
)| 方法 | 估计值 | CI下限 | CI上限 | 估计目标 |
|---|---|---|---|---|
| 粗均值差 | -0.65 | -1.55 | 0.25 | 观察组粗差(非因果 estimand) |
| 标准化 | -5.05 | -5.63 | -4.43 | ATE |
| 稳定 IPW | -5.27 | -6.01 | -4.54 | ATE |
| 交叉拟合 AIPW | -5.04 | -5.74 | -4.35 | ATE |
| 模拟真值 | -5.06 | — | — | ATE |
plot_rows <- 4:1
comparison_plot_labels <- c(
"Crude difference", "Standardization", "Stabilized IPW", "Cross-fit AIPW"
)
old_margin <- par("mar")
par(mar = c(5.1, 8.2, 4.1, 2.1))
plot(
ate_comparison$估计值[1:4], plot_rows,
pch = 16,
col = c(palette_ci["orange"], rep(palette_ci["teal"], 3)),
xlim = range(ate_comparison$CI下限[1:4], ate_comparison$CI上限[1:4], true_ate),
ylim = c(0.5, 4.5), yaxt = "n",
xlab = "Program minus usual care: mean SBP difference (mmHg)", ylab = "",
main = "Causal effect estimates"
)
segments(
ate_comparison$CI下限[1:4], plot_rows,
ate_comparison$CI上限[1:4], plot_rows,
col = c(palette_ci["orange"], rep(palette_ci["teal"], 3)),
lwd = 2
)
axis(2, at = plot_rows, labels = comparison_plot_labels, las = 1)
abline(v = true_ate, lty = 2, lwd = 2, col = palette_ci["navy"])
abline(v = 0, lty = 3, col = palette_ci["gray"])
legend(
"bottomright", legend = "Simulated ATE truth",
lty = 2, lwd = 2, col = palette_ci["navy"], bty = "n"
)粗估计、调整估计及模拟 ATE 真值
三种调整方法使用不同建模策略,却都依赖相同的核心识别假设。结果相近能增加对模型实现的信心,但不能证明不存在未测量混杂。粗估计偏向零,是因为高基线血压者更容易参加项目。
匹配结果没有放入这张 ATE 图,因为 1:1 匹配针对成功匹配治疗者,更接近 ATT。把不同目标人群和效应尺度的数值放在同一列比较,会制造虚假的方法冲突。
混杂因素同时影响治疗和结局;效应修饰因素则使治疗效应的大小在不同人群中不同。一个变量可以同时承担两种角色,也可以只承担其中一种。
模拟机制中,项目通过参与度产生的血压效应随基线 SBP 连续变化。下面预先用 150 mmHg 分组,仅用于展示亚组标准化;正式研究更应保留连续信息,并用曲线及区间表达异质性。
high_baseline <- causal_data$baseline_sbp >= 150
estimate_subgroup_effect <- function(index) {
mean(predicted_y1[index] - predicted_y0[index])
}
subgroup_ci <- t(apply(
gcomp_bootstrap[c("low_baseline", "high_baseline"), , drop = FALSE],
1,
quantile,
probs = c(0.025, 0.975)
))
subgroup_effects <- data.frame(
基线组 = c("基线 SBP <150", "基线 SBP ≥150"),
人数 = c(sum(!high_baseline), sum(high_baseline)),
标准化CATE = c(
estimate_subgroup_effect(!high_baseline),
estimate_subgroup_effect(high_baseline)
),
CI下限 = subgroup_ci[, 1],
CI上限 = subgroup_ci[, 2],
模拟真值 = c(
mean(causal_truth$individual_effect[!high_baseline]),
mean(causal_truth$individual_effect[high_baseline])
),
row.names = NULL,
check.names = FALSE
)
knitr::kable(
subgroup_effects,
digits = 2,
caption = "按基线血压分组的条件平均处理效应"
)| 基线组 | 人数 | 标准化CATE | CI下限 | CI上限 | 模拟真值 |
|---|---|---|---|---|---|
| 基线 SBP <150 | 1375 | -5.00 | -5.73 | -4.30 | -4.89 |
| 基线 SBP ≥150 | 825 | -5.12 | -5.88 | -4.31 | -5.34 |
比较亚组时应报告每组人数、事件或结局分布、效应与不确定性,而不是用“一个亚组显著、另一个不显著”来宣称交互。正式检验应直接评估效应差异,并预先说明尺度、函数形式和亚组。
极端权重会提高方差,并放大少数观测的测量误差。权重截尾可降低方差,却可能重新引入混杂偏倚;限制共同支持则明确改变目标人群。两者都应作为预先规定或透明报告的敏感性分析。
truncation_limits <- quantile(stabilized_weight, c(0.01, 0.99))
truncated_weight <- pmin(
pmax(stabilized_weight, truncation_limits[1]),
truncation_limits[2]
)
truncated_ipw <-
weighted_mean(
causal_data$six_month_sbp[causal_data$program_num == 1],
truncated_weight[causal_data$program_num == 1]
) -
weighted_mean(
causal_data$six_month_sbp[causal_data$program_num == 0],
truncated_weight[causal_data$program_num == 0]
)
support_index <- propensity_hat >= 0.10 & propensity_hat <= 0.90
support_data <- causal_data[support_index, , drop = FALSE]
support_ps_model <- glm(
program_num ~
age_c10 + baseline_sbp_c10 + smoking_num + rural_num + health_literacy,
family = binomial(), data = support_data
)
support_ps <- clamp_probability(
predict(support_ps_model, type = "response"), 1e-6
)
support_prevalence <- mean(support_data$program_num)
support_weight <- ifelse(
support_data$program_num == 1,
support_prevalence / support_ps,
(1 - support_prevalence) / (1 - support_ps)
)
support_ipw <-
weighted_mean(
support_data$six_month_sbp[support_data$program_num == 1],
support_weight[support_data$program_num == 1]
) -
weighted_mean(
support_data$six_month_sbp[support_data$program_num == 0],
support_weight[support_data$program_num == 0]
)
outcome_model_no_interaction <- lm(
six_month_sbp ~
program_num + age_c10 + baseline_sbp_c10 + smoking_num +
rural_num + health_literacy,
data = causal_data
)
no_interaction_ate <- coef(outcome_model_no_interaction)["program_num"]
sensitivity_table <- data.frame(
分析 = c(
"主分析:稳定 IPW",
"权重第 1/99 百分位截尾",
"限制为 0.10≤估计 PS≤0.90",
"将无交互结局模型用于标准化"
),
效应估计 = c(ipw_ate, truncated_ipw, support_ipw, no_interaction_ate),
分析人数 = c(nrow(causal_data), nrow(causal_data), nrow(support_data), nrow(causal_data)),
目标说明 = c(
"原目标人群 ATE",
"近似原目标;偏倚—方差权衡改变",
"由当前数据定义的受限经验人群 ATE",
"针对原 ATE 的错设模型敏感性估计"
),
check.names = FALSE
)
knitr::kable(
sensitivity_table,
digits = 2,
caption = "对权重、共同支持和结局模型设定的敏感性分析"
)| 分析 | 效应估计 | 分析人数 | 目标说明 |
|---|---|---|---|
| 主分析:稳定 IPW | -5.27 | 2200 | 原目标人群 ATE |
| 权重第 1/99 百分位截尾 | -4.99 | 2200 | 近似原目标;偏倚—方差权衡改变 |
| 限制为 0.10≤估计 PS≤0.90 | -5.21 | 2176 | 由当前数据定义的受限经验人群 ATE |
| 将无交互结局模型用于标准化 | -5.06 | 2200 | 针对原 ATE 的错设模型敏感性估计 |
数值接近不代表所有假设都正确,但能说明结论是否由某个任意分析选择主导。倾向评分阈值定义的是数据依赖的受限经验人群,并不等同于严格意义上的共同支持总体;若排除很多人,应单独描述被排除者,并停止把结果外推到他们。已知真实效应存在异质性时,无交互 OLS 系数一般是重叠相关的模型投影,而非原总体 ATE,因此最后一行有意展示模型错设的敏感性。
设有一个未测二元因素 ,在充分调整已测变量后,其治疗组与对照组的条件患病率差为 ,且 与结局的加性均值差为 。在没有复杂交互的简化线性情形,遗漏 造成的偏倚近似为 ,偏倚修正估计为:
delta_values <- c(-0.30, -0.15, 0, 0.15, 0.30)
gamma_values <- c(5, 10, 15)
bias_grid <- expand.grid(
暴露组患病率差 = delta_values,
U的结局均值差 = gamma_values,
KEEP.OUT.ATTRS = FALSE
)
bias_grid$偏倚修正ATE <-
aipw_ate - bias_grid$暴露组患病率差 * bias_grid$U的结局均值差
bias_display <- reshape(
bias_grid,
idvar = "暴露组患病率差",
timevar = "U的结局均值差",
direction = "wide"
)
names(bias_display) <- c("治疗组与对照组的 U 患病率差", "γ=5", "γ=10", "γ=15")
knitr::kable(
bias_display,
digits = 2,
caption = "简化未测量混杂参数下的偏倚修正 ATE(mmHg)"
)| 治疗组与对照组的 U 患病率差 | γ=5 | γ=10 | γ=15 |
|---|---|---|---|
| -0.30 | -3.54 | -2.04 | -0.54 |
| -0.15 | -4.29 | -3.54 | -2.79 |
| 0.00 | -5.04 | -5.04 | -5.04 |
| 0.15 | -5.79 | -6.54 | -7.29 |
| 0.30 | -6.54 | -8.04 | -9.54 |
这个网格不是未测混杂的检验,也不是通用校正公式。它迫使研究者说明:一个遗漏因素需要多不平衡、与结局关系多强、方向如何,才会实质改变结论。更正式的定量偏倚分析应匹配结局类型、效应尺度、交互、测量误差和已有外部信息。
当无未测量混杂不可信时,研究者可能利用政策、阈值、时间变化或外部鼓励形成的准实验结构。每种设计用一组新的强假设替换普通调整的假设。
| 方法 | 典型估计目标 | 核心识别依据 | 关键诊断或威胁 |
|---|---|---|---|
| 回归/标准化 | 目标总体 ATE 或 CATE | 给定 后无未测混杂 | 函数形式、重叠、调整集 |
| IPW | 目标总体边际效应 | 正确治疗模型及同一识别假设 | 平衡、极端权重、模型错误 |
| AIPW | 目标总体边际效应 | 治疗或结局模型之一正确及同一识别假设 | 两套模型、极端评分、交叉拟合 |
| 匹配 | 常为可匹配治疗者 ATT | 已测变量上可交换 | 匹配质量、丢弃样本、方差 |
| 双重差分 | 处理组 ATT | 无处理时满足平行趋势 | 预趋势、同期冲击、提前反应 |
| 回归不连续 | 阈值附近局部效应 | 阈值处潜在结局连续 | 操纵分数、带宽、函数形式 |
| 工具变量 | 常为依从者局部平均效应 | 相关性、独立性、排除限制等 | 弱工具、直接路径、单调性 |
| 间断时间序列 | 政策时点的水平/趋势变化 | 无同时发生且影响结局的冲击 | 季节、历史事件、自相关 |
双重差分(difference-in-differences,DiD)比较处理组与对照组从政策前到政策后的变化差:
关键平行趋势假设是:若没有干预,两组结局的平均变化趋势本应相同。政策前趋势图能发现部分反证,却不能证明政策后的反事实平行趋势。还需考虑组别构成变化、差异性同期冲击、提前反应、溢出效应和结局编码变化。
分期实施且处理效应随组别或时间变化时,传统双向固定效应回归可能混合不恰当比较。应使用适合分期处理的现代组别—时间估计方法,并明确尚未处理组或从未处理组作为比较对象。
当规则以连续评分 是否超过阈值 决定治疗,可比较阈值两侧非常接近的个体:
该效应通常只适用于阈值附近。识别要求除治疗概率外,潜在结局及重要基线特征在阈值处连续,且个体不能精确操纵分数。应展示原始散点或分箱均值、评分密度、协变量连续性,并报告局部线性模型、带宽和多项式阶数的敏感性;通常避免高阶全局多项式。
工具变量 影响治疗 ,但只能通过治疗影响结局。对二元工具和二元治疗,Wald 比值为:
在工具相关性、独立性、排除限制和单调性等条件下,该比值识别依从者中的局部平均处理效应;连续结局时是依从者均值差,二元结局时是依从者风险差,不要求另设“线性风险差模型”。第一阶段很弱会造成不稳定和严重有限样本偏倚;相关性强也不能证明排除限制。
医生偏好、距离或政策资格有时被当作工具,但它们可能通过护理质量、交通、地区资源或其他服务直接影响结局。工具的可信度来自制度与机制知识,不来自把变量放进两阶段回归。
若结局缺失同时受治疗和预后影响,仅分析结局完整者相当于条件化一个潜在碰撞点。若治疗前混杂变量缺失,完整案例也可能改变目标人群和协变量分布。
可考虑:
多重插补不会自动解决未测混杂,也不能可靠填补研究设计中从未收集的关键概念。
把“吸烟”粗分为当前/非当前、用一次测量代表长期血压、用账单代码代表疾病严重度,都可能使调整不充分。混杂变量误差、治疗误分类和结局误差的影响方向不同,差异性误差尤其难以预测。
应报告变量来源、时间、重复测量、验证研究与阈值规则。若有外部敏感度、特异度或可靠性信息,可进行概率偏倚分析,而不是只把“可能存在误差”放在局限段落。
内部效度问研究中的因果对比是否可信;外部效度问它是否适用于另一个总体。若入组 同时受效应修饰因素和结局风险影响,简单研究样本 ATE 可能不代表目标总体。
推广或运输方法可按目标总体中的协变量分布重新标准化或加权,但需要测量所有重要的选择—结局共同原因和效应修饰因素,并在目标总体中有支持。没有目标总体数据时,应清楚限定适用范围。
总体平均有效不等于公平可及 平均因果效应可能掩盖受益、伤害、可获得性和测量质量的群体差异。城乡、收入、族群或残障变量往往代表结构与资源,而不是固定生物属性。报告异质性时应说明机制假设、样本支持和潜在污名化,并把“谁能获得干预”纳入决策。
邀请领域专家、数据管理者和研究对象代表检查 DAG。列出最小充分调整集、不能调整的治疗后变量、可能未测的共同原因、选择机制和测量代理。保留不同可信 DAG 下的分析方案。
选择与 estimand 对齐的方法。标准化检查结局模型;IPW 检查治疗模型、权重与平衡;匹配检查匹配后样本;AIPW 同时检查两套 nuisance 模型。对聚类、重复测量和估计权重使用合适方差。
至少考虑:合理 DAG、非线性和交互、重叠限制、权重截尾、缺失处理、治疗/结局误差、未测混杂、负对照以及不同效应尺度。敏感性分析应有科学合理范围,而不是任意遍历到获得期望答案。
同时给出两个策略下的标准化结局、效应差、区间、目标人群、诊断与局限。把识别假设与统计模型假设分开报告,并避免把置信区间解释为包含所有不确定性。
我们在 [目标人群] 中模拟比较从 [共同时间零点] 开始实施 [策略 1] 与 [策略 0],结局为 [时间范围内的明确定义],主要 estimand 是 [ATE/ATT/CATE 及尺度]。依据 [DAG、领域知识与测量时间] 调整 [变量];识别依赖 [一致性、可交换性、正值性、无干扰及选择/缺失条件]。采用 [标准化/IPW/AIPW/设计方法] 估计,诊断显示 [平衡、重叠、权重、模型与样本支持]。策略 1 与策略 0 的标准化结局分别为 [数值] 与 [数值],效应为 [估计值,95% CI]。结果对 [敏感性分析] 为 [稳健/敏感];[未测混杂、测量、推广等具体限制] 仍可能影响解释。
| 常见说法或做法 | 问题 | 更好的做法 |
|---|---|---|
| “调整后显著,所以是因果” | 显著性不验证识别假设 | 先定义 estimand、设计、DAG 和识别条件 |
| 根据单变量 p 值选择混杂因素 | p 值不表示因果角色 | 用时间顺序、机制和 DAG 选择调整集 |
| 把所有基线和治疗后变量都调整 | 中介和碰撞点可能改变问题或制造偏倚 | 明确变量角色,只调整合适集合 |
| 倾向评分 AUC 很高就是好模型 | 高区分可能意味着重叠不足 | 检查共同支持、权重和加权平衡 |
| 匹配后不再检查协变量 | 匹配算法不保证实际平衡 | 报告匹配前后分布与 SMD |
| 删除无匹配者仍称总体 ATE | 分析目标人群已经改变 | 描述被排除者并正确命名 ATT/重叠人群效应 |
| 权重截尾后不报告阈值 | 隐藏偏倚—方差与目标变化 | 报告阈值、人数、平衡和敏感性 |
| 双重稳健等于两模型都可随意 | 两者同时错时仍偏倚 | 检查两套模型及共同识别假设 |
| 一个亚组显著、另一个不显著 | 不等于两组效应不同 | 直接估计交互或效应差及区间 |
| 只报告相对效应 | 缺少基线风险和绝对意义 | 同时报策略特异结局与绝对效应 |
| 置信区间覆盖所有不确定性 | 通常只反映抽样和指定估计过程 | 另做识别与测量敏感性分析 |
| 多种方法结果一致证明无混杂 | 方法可能共享同一错误假设 | 寻找不同偏倚结构和外部证据 |
“健康项目有用吗?”缺少哪些要素?请把它改写成一句可估计的因果问题。
基线疾病严重度影响是否接受治疗,也影响结局;治疗后的依从性受治疗影响并影响结局;复诊受治疗和症状恶化共同影响。三者分别是什么角色?估计总效应时怎样处理?
临床规则禁止妊娠者接受某药,但研究希望估计包括妊娠者在内的总体 ATE。收集更多相同规则下的数据能解决吗?
为什么标准化要为每个人生成 和 两次预测,而不是把连续协变量都设为样本均值?
某 IPW 分析的最大权重为 85,ESS 从 3000 降为 420,且重要协变量加权后绝对 SMD 为 0.24。可以直接报告效应吗?
1:1 无放回匹配排除了 35% 的治疗者和 70% 的对照者。匹配均值差还能称为原始总体 ATE 吗?
AIPW 的倾向评分模型和结局模型都拟合得很好,是否可以忽略一个未测量的疾病严重度共同原因?
城市亚组效应 p=0.03,乡村亚组 p=0.20,能否断言只有城市受益?
| 概念 | 公式 | 解释 |
|---|---|---|
| 个体效应 | 同一个体两个策略下的反事实差 | |
| ATE | 目标总体平均效应 | |
| ATT | 已治疗者平均效应 | |
| 一致性 | 观察策略对应定义明确的潜在结局 | |
| 可交换性 | 给定 后无未控制共同原因 | |
| 正值性 | 每类目标个体都有策略对比 | |
| g-formula | 条件结果在目标人群中标准化 | |
| 倾向评分 | 给定治疗前变量的治疗概率 | |
| 稳定权重 | 构建已测变量平衡的伪总体 | |
| ESS | 权重集中程度摘要 |
| 目标 | 代码模式 |
|---|---|
| 粗均值差 | with(d, mean(y[a == 1]) - mean(y[a == 0])) |
| 结局模型 | lm(y ~ a * x1 + x2, data = d) |
| 两个反事实世界 | transform(d, a = 1);transform(d, a = 0) |
| 标准化 ATE | mean(predict(fit, d1) - predict(fit, d0)) |
| 倾向评分 | glm(a ~ x1 + x2, family = binomial(), data = d) |
| ATE 权重 | ifelse(a == 1, mean(a)/ps, (1-mean(a))/(1-ps)) |
| 治疗策略加权均值 | mu1 <- sum(w[a == 1] * y[a == 1]) / sum(w[a == 1]);对
a == 0 同样计算 mu0 |
| IPW ATE | mu1 - mu0 |
| 权重 ESS | sum(w)^2 / sum(w^2) |
| 非参数 bootstrap | sample.int(nrow(d), replace = TRUE) 后重新拟合 |
| 线性模型系数区间 | confint(fit) |
| 术语 | 通俗含义 |
|---|---|
| 反事实/潜在结局 | 同一研究单位在另一个策略下本会出现的结局 |
| Estimand | 目标人群、策略、结局、时间和效应尺度共同定义的数量 |
| 混杂 | 治疗与结局共享原因,使观察组不可直接比较 |
| 后门路径 | 从指向治疗的箭头开始、连接治疗与结局的非因果路径 |
| 中介 | 位于治疗影响结局的因果路径上的变量 |
| 碰撞点 | 两个箭头共同指向的变量;条件化可能打开路径 |
| 标准化 | 在目标协变量分布上平均条件反事实预测 |
| 倾向评分 | 给定治疗前协变量时接受治疗的概率 |
| 正值性 | 目标人群中的每类个体都有接受各策略的可能 |
| 重叠 | 有限数据中两组存在相似治疗前特征 |
| 双重稳健 | 治疗或结局 nuisance 模型之一正确时的一致性性质 |
| 交叉拟合 | 在不同数据折训练模型和生成预测,降低过拟合影响 |
| 局部效应 | 只适用于阈值附近或依从者等特定子人群的效应 |
在发布因果结论前,尝试用一句话回答每个问题:
若任何答案只能写成“软件自动处理”或“因为 p<0.05”,分析尚未完成。
## 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] digest_0.6.39 R6_2.6.1 fastmap_1.2.0 xfun_0.60
## [5] cachem_1.1.0 knitr_1.51 htmltools_0.5.9 rmarkdown_2.31
## [9] lifecycle_1.0.5 cli_3.6.6 sass_0.4.10 jquerylib_0.1.4
## [13] compiler_4.6.1 tools_4.6.1 evaluate_1.0.5 bslib_0.12.0
## [17] yaml_2.3.12 rlang_1.3.0 jsonlite_2.0.0