本文定位:bridging study(桥接研究)与 follow-on CDx(follow-on companion diagnostic,后续伴随诊断)统计方法的学习笔记。前两章先铺背景——精准医学与传统医学有何不同、为什么要”搬运证据”(概念框架),以及 CDx 试验怎么设计(入组、对照、设盲三个取舍);有了背景再进入方法。方法部分分两条腿走:概念 讲清每个量是什么、公式是什么;worked example 搭一个已知真值的生成模型(DGP,data-generating process),从它派生所有下游数据集,逐节用公式估计再和已知真值对比,验证 bridging / follow-on 机制能否把被抽样偏差扭曲的疗效/一致性还原回真相。所有插图均由本文的 R 代码生成。
核心论点:bridging study 和 follow-on CDx 是同一类统计问题——用 assay 间的 concordance 把疗效证据从一个 assay 定义的人群搬运到另一个 assay 定义的人群——区别只在缺失数据的结构不同。
先分清精准医学和传统医学。 传统医学按”平均病人”给药:同一诊断下大家吃同一种药、同一剂量,疗效是全人群的平均效应,个体差异靠事后调整。精准医学换了前提——先用一个 biomarker(如某基因突变状态)把人群切成疗效/风险不同的亚群,再对亚群分别决策。药不再是”对这个病有效”,而是”对这个 biomarker 阳性的人有效”。
这一换,就逼出一个传统医学大多能绕开、精准医学却绕不开的问题:biomarker 阳性/阴性得靠一个检测工具判定,而工具不完美、还可能换。疗效证据是对”某工具筛出的人群”成立的,工具一变,人群跟着变,证据能不能接着用就成了统计问题。本文全部内容都在处理这一个问题。
精准医学的统计问题不是从名词开始的,而是从一个很实际的问题开始:医生手里拿到的是某个检测平台给出的阳性/阴性结果,治疗证据却可能来自另一个检测平台定义的人群。
两个典型例子:
(同类还有 HER2 的 IHC/FISH 分层、BRCA/HRD 与 PARP 抑制剂的 panel 差异——机制一样,不再展开。)
这些例子的共同点:biomarker 是生物学目标,assay/CDx 是测它的工具。治疗效果通常不是对”工具”本身成立,而是对这个工具筛出来的人群成立;一旦工具换了,人群也跟着变了。
那为什么偏偏是精准医学?糖尿病、心血管也测 biomarker、也换试剂啊。 差别在有没有一把公共的尺。传统病的指标是连续、可溯源的物理量——血糖 mg/dL、HbA1c、LDL 都锚定到统一参考方法,换个仪器只要校准到同一把尺,结果就一致、人群几乎不动,换工具只需重新校准,不必搬运证据。精准医学的麻烦是常常没有这把公共尺:突变有/无靠某个 assay 判,borderline 样本各 assay 会吵,而且没有”真相”可校——只能拿两个 assay 互比一致率(PPA/NPA)。
更精确地说,bridging/follow-on 问题要三条同时成立,传统病通常缺一条,精准病常常三条齐活:
反过来,传统病一旦这三条凑齐,照样撞上同样的统计:肌钙蛋白换代改了”心梗阳性”的定义、心血管风险评分换代改了该吃他汀的人群、HLA-B*5701 门控 abacavir——这些本质就是 CDx 问题。所以精准医学不是发明了新问题,而是把一个被传统病靠”连续+可溯源+药诊解耦”压住的问题,推到了台面正中央。
四个 assay 角色(贯穿全文):
换一个 assay 后,统计问题立刻变成四件事:
所以本文后面的所有公式都在回答同一个问题:如何把一个 assay 定义下的疗效证据,合理搬运到另一个 assay 定义的人群上。先把名词和场景说清楚,再用一个已知真值的 DGP 看这些搬运公式什么时候能还原真相、什么时候只能靠假设。
pivotal / bridging / follow-on 是三条开发路径,全文最重要的区分——唯一差别是”最终上市 CDx”与”试验里实际入组用的 assay”的关系。真实开发里不是”大多数项目都同时做 bridging 和 follow-on”,而是按时间顺序落到其中一条:
par(family = "cjk", mar = c(1, 1, 3, 1))
plot(NA, xlim = c(0, 12), ylim = c(0, 7), axes = FALSE, xlab = "", ylab = "",
main = "三种验证场景", cex.main = 1.6)
draw_box(1.8, 6, "Pivotal", "gray30", w = 2.6, h = 0.85, cex = 1.15)
draw_box(5.0, 6, "Pivotal trial", "gray40", w = 3.0, h = 0.85, cex = 1.05)
draw_box(9.4, 6, "最终 CDx\n直接入组", "#2E86AB", w = 4.0, h = 0.95, cex = 1.05)
draw_arr(6.6, 6, 7.3, 6)
draw_box(1.8, 4, "Bridging", "gray30", w = 2.6, h = 0.85, cex = 1.15)
draw_box(5.0, 4, "Pivotal trial", "gray40", w = 3.0, h = 0.85, cex = 1.05)
draw_box(9.4, 4, "CTA 入组\n最终 CDx 不同", "#A23B72", w = 4.0, h = 0.95, cex = 1.05)
draw_arr(6.6, 4, 7.3, 4)
draw_arr(9.4, 3.5, 9.4, 2.55, "bridging analysis", cex = 1.0)
draw_box(1.8, 2, "Follow-on", "gray30", w = 2.6, h = 0.85, cex = 1.15)
draw_box(5.0, 2, "已批准\ncomparator CDx", "gray40", w = 3.0, h = 0.95, cex = 0.95)
draw_box(9.4, 2, "新 follow-on\nCDx (FCD)", "#C73E1D", w = 4.0, h = 0.95, cex = 1.05)
draw_arr(6.6, 2, 7.3, 2)一句话:bridging 解决”试验 assay 与最终 CDx 不一致”;follow-on 解决”新 CDx 能否替代已批准 CDx”。真实临床使用还更复杂——院内 NGS panel、LDT、商业综合 panel、不同样本类型,监管批准/报销/临床实际检测不总完全重合。两者各自的定义与做法,都放在后面「从生成模型到 follow-on 与 bridging」一章里展开(同一地基、两种搬运)。
前面(§1.3)按”CDx 与入组 assay 的关系”分了三条路径。这一章换到设计层面:一个 CDx 试验具体怎么搭?
先给两组定义。伴随诊断 (companion diagnostic, CDx) 是为对应治疗产品安全有效使用提供必要信息的体外诊断设备 (U.S. Food and Drug Administration 2014)。围绕它:
主张类型决定要拿什么证据;但落到怎么搭试验,有三个彼此独立的取舍,外加一条贯穿的纪律。这几件事最容易混,先用一张表钉清楚:
常听到的概念 | 其实属于 |
|---|---|
谁入组(富集只 CDx+ / 全员含 CDx−) | 取舍一 · 入组 |
随机化、要不要并行对照臂 | 取舍二 · 对照 |
open-label、双盲/三盲、独立评估 BICR | 取舍三 · 设盲 |
揭盲前锁定 CDx 分析计划、防事后 post-hoc | 纪律 · 分析预设 |
三个取舍相互独立(一条怎么定,不限制另外两条):富集人群里既能 RCT 也能单臂;RCT 既能双盲也能 open-label;单臂则必然 open-label(但仍保留 BICR)。下面逐条讲。
理论上,predictive 主张要 marker-by-treatment 设计:CDx 检测后,CDx+ 与 CDx− 两个分层里都要做 1:1 随机化(治疗 vs 对照),这样才能估交互。可真做的时候(尤其肿瘤靶向药),CDx− 分层通常被整条筛除,既不入组也不随机(下图红叉):
par(family = "cjk", mar = c(0.5, 0.5, 3, 0.5))
plot(NA, xlim = c(0, 14), ylim = c(-0.6, 6), axes = FALSE, xlab = "", ylab = "",
main = "Predictive 设计(理论) vs 富集现实:CDx− 常被筛除", cex.main = 1.3)
draw_box(1.7, 3, "符合条件患者", "gray30", w = 3.0, h = 0.9, cex = 1.05)
draw_box(5.0, 3, "CDx 检测", "gray40", w = 2.2, h = 0.9, cex = 1.05); draw_arr(3.3, 3, 3.85, 3)
# CDx+ 支路:实际入组、随机
draw_box(8.4, 4.5, "CDx+ 1:1 随机", "#2E86AB", w = 3.2, h = 0.9, cex = 1.0)
draw_arr(6.15, 3, 6.75, 4.4)
for (yy in c(5.2, 3.8)) draw_box(12.2, yy, if (yy > 4.5) "治疗 T=1" else "对照 T=0", "gray55", w = 2.6, h = 0.62, cex = 0.95)
draw_arr(10.05, 4.6, 10.85, 5.2); draw_arr(10.05, 4.4, 10.85, 3.85)
# CDx- 支路:富集设计里筛除 —— 淡蓝(未入组)+ 划红叉
draw_box(8.4, 1.5, "CDx− 1:1 随机", "#AED6F1", w = 3.2, h = 0.9, cex = 1.0, txt = "gray20", border = "#7FB3D5")
draw_arr(6.15, 3, 6.75, 1.6)
for (yy in c(2.2, 0.8)) draw_box(12.2, yy, if (yy > 1.5) "治疗 T=1" else "对照 T=0", "#AED6F1", w = 2.6, h = 0.62, cex = 0.95, txt = "gray20", border = "#7FB3D5")
draw_arr(10.05, 1.6, 10.85, 2.2); draw_arr(10.05, 1.4, 10.85, 0.85)
segments(6.55, 2.25, 13.6, 0.45, col = "#C73E1D", lwd = 3)
segments(6.55, 0.45, 13.6, 2.25, col = "#C73E1D", lwd = 3)
text(10.0, -0.25, "富集(靶向)设计:CDx− 筛除,不入组、不随机", col = "#C73E1D", font = 2, cex = 0.95)CDx− 被筛除有两条硬理由:
于是绝大多数 CDx 试验退化成富集(靶向)设计:只入组阳性患者(实际是 CTA+),只在阳性人群里随机治疗 vs 对照。拿到的是 selection 主张(“阳性人群有效”),不是完整的 predictive/交互主张——图中下半条支路的数据从来没产生过。
这正是全文所有麻烦的根源。 因为 CDx−(以及后面会遇到的 CTA−/CDx+ 那一格)根本没被随机化、没有疗效数据,想把疗效证据搬到另一个 assay 定义的人群上,就只能对这些没观测到的格子做假设——这就是后面讲 bridging 时 \(\delta_{01}\) 不可识别的由来。
那到底什么时候能删 CDx−、什么时候不能? 取决于你要哪类主张:
还有第三条折中路:全员入组、把 CDx 留到分析阶段才劈(prospective–retrospective 设计)。 全员随机化时不看 CDx,但每人都测,等分析计划锁定后再按 CDx 分层分析。合法与否只看一点——“预设”:assay、cutoff、分析计划必须在看到 biomarker 与结局的关系之前就冻结;反面是事后 post-hoc(跑完看了数据才挑 cutoff、挑分法),等于 data dredging,监管不认。例:上面 KRAS 的故事正是这么落地的——Karapetis、Amado 等(2008)用的是已完成的全员 RCT,KRAS 状态从存档肿瘤样本回测,但 treatment × KRAS 的分析是预先设定的;所以尽管是”回溯”分析,监管仍然采信,并据此把抗 EGFR 单抗的适应症限定到 KRAS 野生型。
这里的”锁定/冻结”指分析层面的预设(对应上表的”纪律 · 分析预设”)——在主分析揭盲、看 biomarker–结局关系之前把 CDx 分析定死,防挑数据;它不是对医生/患者设盲,设盲是取舍三的事,具体到(§2.3)再展开。
取舍一确定的是”入组谁”;这一节是另一个独立的问题:入组之后,治疗组要不要配一个并行对照臂?这跟删不删 CDx− 完全是两回事——富集人群里照样可以做 RCT,也可以删掉对照臂做单臂试验(single-arm trial, SAT)(见下方单臂示意图)。精准医疗里相当一部分获批走的正是单臂。下面把这条取舍讲清:为什么能单臂、什么时候必须 RCT。
RCT:金标准,但在精准医疗里有门槛。 RCT 能估比较性获益——即”用了这个药,比不用(或用现有标准治疗)好多少”。因为治疗组和对照组来自同一批随机分配的患者、预后分布一致,两组的差异才能干净地归因于药物,而不是病情本身或人群差异。度量上通常用 PFS(progression-free survival,无进展生存期)或 OS(overall survival,总生存期)的风险比 (hazard ratio, HR):HR<1 表示试验药相对对照把”进展/死亡”的瞬时风险降低了多少(如 HR=0.6 ≈ 风险降 40%)。这种”相对某个对照”的比较性证据,是完全批准(full approval)和比较性主张的金标准——单臂试验给不出,因为它没有并行对照。
PFS/OS 具体怎么算(两臂各算一套)。 它们都是事件时间终点,从随机化那天(t=0)起算,逐患者记一个 (时间, 是否事件) 对:OS = 到任何原因死亡的时间;PFS = 到疾病进展(影像按 RECIST 判定肿瘤增大/新病灶)或死亡、谁先算谁。到数据截止还活着 / 无进展 / 失访的患者不是没结果,而是删失 (censored)——只知道”至少活/无进展到这天”,之后未知。每个臂把本臂所有患者的这些 (时间, 事件/删失) 汇总成一条 Kaplan–Meier 生存曲线(曲线每遇一个事件下一个台阶,删失只做标记不下台阶),曲线降到 0.5 处即中位 PFS/OS。两臂比较:log-rank 检验看曲线整体是否不同,Cox 模型给出 HR(两臂瞬时事件风险之比)。所以实验组、对照组各有自己的 PFS 曲线和 OS 曲线,“疗效”就是两条曲线之间的差(HR 或中位差)。
RCT 的好处(为什么是金标准):
RCT 的坏处(代价与陷阱——多数不是精准医疗独有,是所有 RCT 的通病):
crossover 为什么稀释 OS。 这不是精准医疗的特殊问题,是所有允许交叉的 RCT 的结构性代价。OS 只算到死亡:对照组病人进展后换到有效试验药,虽然晚了几个月,最终也吃上了药、也活长了 → 两臂 OS 拉不开。PFS 只算到第一次进展,而进展正是触发换药的事件、发生在换药之前 → PFS 不被污染。所以有 crossover 风险时,主要终点常从 OS 退到 PFS。下图用模拟 Kaplan–Meier 曲线对比——同一个实验组,只改对照组是否 crossover:
suppressMessages(library(survival))
set.seed(7)
par(family = "cjk", mfrow = c(1, 2), mar = c(4.4, 4.6, 3.2, 1))
ln2 <- log(2); cens <- 48; n <- 300
rmed <- function(n, m) rexp(n, ln2 / m)
mk <- function(t) data.frame(t = pmin(t, cens), ev = as.integer(t <= cens))
t_trt <- rmed(n, 30) # 实验组 OS 中位 ~30
t_c0 <- rmed(n, 18) # 对照 无 crossover 中位 ~18
t_c1 <- rmed(n, 27) # 对照 有 crossover: 换药后 OS 被抬到 ~27, 逼近实验组
kmplot <- function(dt, dc, main) {
d <- rbind(cbind(dt, arm = "trt"), cbind(dc, arm = "ctrl"))
f <- survfit(Surv(t, ev) ~ arm, data = d)
hr <- summary(coxph(Surv(t, ev) ~ (arm == "trt"), data = d))$coef[2] # exp(coef)=HR 实验 vs 对照
plot(f, col = c("#C73E1D", "#2E86AB"), lwd = 3, xlim = c(0, cens),
xlab = "随机化后月数", ylab = "生存概率 S(t)", cex.lab = 1.3, cex.axis = 1.15, mark.time = FALSE)
title(main = main, cex.main = 1.35)
legend("topright", c("实验组", "对照组"), col = c("#2E86AB", "#C73E1D"), lwd = 3, bty = "n", cex = 1.15)
abline(h = 0.5, lty = 3, col = "gray60")
text(1, 0.10, sprintf("HR = %.2f", hr), pos = 4, cex = 1.25, font = 2)
}
kmplot(mk(t_trt), mk(t_c0), "OS:无 crossover(两臂拉得开)")
kmplot(mk(t_trt), mk(t_c1), "OS:有 crossover(对照被抬高、差距抹平)")左图无 crossover,两臂拉得开、HR≈0.63 显示获益;右图对照组进展后换药,OS 曲线被抬到几乎与实验组重合、HR≈0.94,“看着没效”——但药其实一样有效,只是 OS 这把尺子被 crossover 稀释了。PFS(算到进展、在换药之前)在两种情形下都能保住获益,这就是它常被选作主要终点的原因。
RCT 更适合人群较大、有真实对照药的场景——例:曲妥珠单抗(HER2+ 乳腺癌)、维莫非尼(BRAF V600E 黑色素瘤,BRIM-3 拿到 OS 获益)、克唑替尼确证性 RCT(PROFILE 1007/1014)。
单臂试验(SAT):为什么可以,什么时候用。 单臂没有对照,人人服药,主要终点是客观缓解率(ORR)+ 缓解持续时间(DoR),与预设的历史/自然史阈值比。它站得住脚的核心统计理由是:肿瘤缩小是可归因于药的事件——肿瘤不会自己缩,所以”客观缓解”这种终点不像 PFS/OS 那样严重依赖预后和自然史,不必靠并行随机对照来归因。适用条件:marker 罕见(如 NTRK 融合 <1%,无法凑够随机样本)、预期疗效巨大、未满足的临床需求大、走加速批准(accelerated approval)。例:拉罗替尼/恩曲替尼(NTRK 融合,tumor-agnostic 篮子试验,按 ORR 获批)、帕博利珠单抗(MSI-H/dMMR,跨瘤种单臂)、奥希替尼(EGFR T790M,AURA 单臂加速批准)、克唑替尼最初的单臂加速批准(PROFILE 1001)。
par(family = "cjk", mar = c(0.5, 0.5, 3, 0.5))
plot(NA, xlim = c(0, 14), ylim = c(-0.6, 6), axes = FALSE, xlab = "", ylab = "",
main = "单臂试验(SAT):富集入组,只有治疗臂,无并行对照", cex.main = 1.3)
draw_box(1.7, 3, "符合条件患者", "gray30", w = 3.0, h = 0.9, cex = 1.05)
draw_box(5.0, 3, "CDx 检测", "gray40", w = 2.2, h = 0.9, cex = 1.05); draw_arr(3.3, 3, 3.85, 3)
draw_box(8.2, 3, "CDx+ 入组\n(富集)", "#2E86AB", w = 2.6, h = 1.0, cex = 0.95); draw_arr(6.15, 3, 6.85, 3)
# 只有治疗臂
draw_box(11.7, 4.2, "治疗 T=1", "#2E86AB", w = 2.6, h = 0.7, cex = 0.95); draw_arr(9.55, 3.2, 10.35, 4.0)
text(11.7, 5.15, "终点:ORR / DoR", cex = 0.9, col = "gray20", font = 3)
# 对照臂被删 —— 灰掉 + 红叉
draw_box(11.7, 1.8, "对照 T=0", "gray80", w = 2.6, h = 0.7, cex = 0.95); draw_arr(9.55, 2.8, 10.35, 2.0)
segments(10.3, 2.2, 13.2, 1.4, col = "#C73E1D", lwd = 3)
segments(10.3, 1.4, 13.2, 2.2, col = "#C73E1D", lwd = 3)
text(7.0, -0.3, "单臂:删去对照臂;疗效以 ORR 与预设的历史/自然史阈值比,不做并行随机化",
col = "#C73E1D", font = 2, cex = 0.92)怎么选。 现实里常见的是先后接力:先单臂拿加速批准、再补确证性 RCT(克唑替尼、奥希替尼都走这条)。一句话决策规则:marker 越罕见、效应越戏剧化、越无替代药 → 越倾向单臂;人群越大、越需要比较 PFS/OS、越有现成对照 → 越必须 RCT。
各自要盯的问题。
金标准 RCT 讲三盲:①病人盲(不知自己在治疗组还是对照组)、②医生/研究者盲、③独立评估与数据分析盲(影像/病理由独立评审判读、统计分析也盲态)。要害是:设盲得靠对照才盲得起来——你得有”治疗 vs 安慰剂/对照”这层可瞒;没有对照,前两盲根本无从谈起。所以设盲和取舍二(对照)相关,但不是同一件事:
三盲各防一种偏倚,单臂能保住哪层一目了然:
盲谁 | 防的偏倚 | 单臂能否做到 |
|---|---|---|
① 病人 | 安慰剂效应、依从/退出差异 | ✗(人人吃药) |
② 医生/研究者 | 用药、剂量调整、主观评估倾向 | ✗ |
③ 独立评估 + 分析(BICR / 统计) | 评估者、分析者偏倚 | ✓(靠 BICR) |
别和”分析预设”混。 取舍一提过的”揭盲前锁定 CDx 分析计划”,那个”揭盲”指试验末数据库/治疗码揭盲的时间点,要求 biomarker 分析在此之前冻结——是分析纪律(防 post-hoc 挑数据),和”病人/医生知不知道谁在治疗组”无关。一句话:设盲管”知不知道处理”,分析预设管”看数据的先后顺序”,两回事。
回到本文框架。 worked example 里的疗效是随机臂之差 \(\delta = \mathrm{E}[Y\mid T{=}1] - \mathrm{E}[Y\mid T{=}0]\);换成单臂时,被搬运的量就从”治疗效应差”变成”assay+ 人群的应答率”。但底层逻辑完全一样:不完美 assay 让阳性人群混进真阴性,应答率(或疗效)被 PPV 稀释,换一个 assay 就得 bridging 把它搬到新人群上。所以后文所有公式对 RCT 和单臂都成立——区别只在”疗效”这个词具体指风险比(HR)还是应答率(ORR)。
这一章是地基(DGP,Data Generating Process,数据生成过程)——后面 bridging、follow-on 两章都站在它上面。地基其实有两层,要一起搭:
两层穿插着搭(这正是本章比”光有公式”或”光有数据”更值的地方):每立一个概念,随即用模拟把它兑现成数字、当场验证。动手之前,先把整条方法线要解决的问题摆出来。
两个具体问题(都是同一件事的两种形态:疗效证据能不能跨 assay 搬运):
为什么先搭一个模拟器,而不是直接上真实数据? 因为这两个问题的要命之处是真相不可观测:真实 marker 状态 \(G\) 看不到、CTA−/CDx+ 那格没有病人、缺失格子的疗效 \(\delta_{01}\) 无从测量。在真实数据里,你永远没法验证一个 bridging 估计到底对不对——手里没有标准答案可对。所以本章的办法是:先造一个自己偷偷知道全部真相的世界(下面的”生成模型” DGP),在里面按各种真实设计抽样、跑公式,再把结果和那个只有”上帝视角”才知道的真值对撞。公式能还原真相,才敢把它用到真实数据上;还原不了的地方,恰好暴露出”哪一步只能靠假设、不能靠数据”。
顺序:先搭地基——最小符号、“已知真相的世界”、自洽验证,并把”不完美 assay 会稀释疗效”这个两种搬运都要用的通用事实立起来(结局层)→ 再讲 bridging(本文主角:自己试验有疗效,要把它从 CTA+ 人群搬到 CDx+,难在缺失格子)→ 最后讲 follow-on(换一种缺失结构:自己不做疗效试验,靠与已批准 CCD 的一致性间接搬运)。bridging 和 follow-on 是同一类问题、只是缺失结构不同(详见末章「综述」对照表);同一个生成器在末章「进阶」继续复用。
worked example 的地基是一个已知真相的世界。先搭最小的两层,全部围绕潜在真实 marker 状态 \(G\):
第三层”结局层”(连续端点 \(Y\)、疗效 \(\delta\))先不加——讲到疗效混合时再补(见 §「加结局层:疗效混合」)。好处:一切一致性和混合效应都由共同原因 \(G\) 自动自洽诱导,不用手拼一堆可能矛盾的边际概率。
上面两层、以及由它们直接导出的基础符号,严格写出来如下(Pr(·)
是概率,竖线 | 读作”在……已知的条件下”):
符号 | 严格定义 | 层 / 说明 |
|---|---|---|
G | 真实 marker 状态:G=1 真阳、G=0 真阴 | 真相层 · 潜在不可观测,需参考标准才知道 |
prevG(π) | Pr(G=1),真实患病率(1−π = Pr(G=0)) | 真相层 · 唯一的患病率参数 |
T | 治疗臂:T=1 治疗、T=0 对照 | 结局层 · §「结局层」引入结局 Y 时才用到 |
assay A | 任一检测(G 的噪声分类器);本文 A ∈ {CTA, FCD(=最终 CDx), CCD, Pre} | assay 层 · A 是占位符,代入具体检测即得该检测的量 |
sensA / | 敏感度 sensA = Pr(A=1|G=1); | assay 层 · 在 TRUTH 里直接给定(生成参数) |
PPVA / | 阳性预测值 PPVA = Pr(G=1|A=1); | assay 层 · 由 sens/spec 与患病率 π 经 Bayes 导出,非自由参数 |
这一层的量都是”标签或概率”(下一节结局层才出现可正可负的疗效实数):\(G\)、\(T\) 是二值标签 \(\{0,1\}\);\(\pi\)、\(sens/spec\)、\(PPV/NPV\) 都是 \(0\)–\(1\) 的概率。本文 DGP 的具体取值——\(\pi=0.30\);四个检测 \(sens/spec\) 依次 CTA 0.85 / 0.90、CDx(=FCD) 0.95 / 0.96、CCD 0.93 / 0.94、Pre 0.80 / 0.75;由此贝叶斯算出 \(PPV_f\approx0.91\)、\(PPV_c\approx0.87\)。
为什么 \(\pi=0.30\)? 它本质只是 DGP 的一个固定已知参数,取任何值都能验证公式;选 0.30 图两点:一是真实——很多肿瘤 biomarker 的患病率就落在 20–40%;二是教学清晰——特意避开 0.5(太对称,掩盖患病率对 PPV 的影响)和极端值(罕见 marker 会让 PPV 崩到很低、例子失真),这样”不完美 assay 稀释疗效”的效应既清楚可见、又不夸张(\(\pi=0.30\) 下 CDx 这么好的检测 \(PPV_f\) 也才 \(0.91\)、不到 1,稀释一眼看得见)。四个检测的 \(sens/spec\) 同理特意拉开档次(CDx 最好、Pre 最差),好让”assay 越好、\(PPV\) 越高、稀释越少”的对比一目了然。
下标就是”哪一个检测”,不用猜:\(A\) 只是占位符,代入具体检测就得到那个检测的量——本文固定 \(f\) = FCD(最终 / 新 CDx,是 FCD 的缩写,不是字母”f”某检测)、\(c\) = CCD(comparator CDx)。于是 \(PPV_f=\Pr(G{=}1\mid FCD{=}1)\)、\(PPV_c=\Pr(G{=}1\mid CCD{=}1)\),\(sens_A\)、\(spec_A\) 同理。
而且 \(PPV\) 不是可以随手假设的旋钮。 它由贝叶斯从三样东西钉死:该检测的 \(sens\)、\(spec\),加上这个 marker 的真实患病率 \(\pi\): \(PPV=\dfrac{sens\cdot\pi}{sens\cdot\pi+(1-spec)(1-\pi)}\)。要害在 \(\pi\):不同 marker 在人群里的分布差别极大,罕见 marker(\(\pi\) 小)即便检测很准,\(PPV\) 也可能很低(阳性里混进大量假阳)。所以 \(PPV\) 是 marker 特异的、可行范围有限——一旦锁定某个具体 marker 的 assay,它的 \(PPV\) 基本被 \((sens,spec,\pi)\) 定死,不能拍脑袋给。(这条贝叶斯关系,§「贝叶斯」那节还会用模拟验证。)
结局层的 \(Y\)、\(\mu\)、\(\delta_{\text{pos}}/\delta_{\text{neg}}\)、\(\eta\)、\(\theta\) 到 §「加结局层」再严格定义(见那里的”三级台阶”表);派生的一致性量 PPA/NPA、搬运权重 \(p\) 在 §「生成器、真值算子与自洽验证」一节就地给出;全部符号另见文末〈记号表〉。
TRUTH <- list(
prev_G = 0.30, # Pr(G=1)
assays = list(
CTA = c(sens = 0.85, spec = 0.90), # clinical trial assay (入组用)
CDx = c(sens = 0.95, spec = 0.96), # 最终 CDx / FCD (最好)
CCD = c(sens = 0.93, spec = 0.94), # comparator CDx (follow-on 参照)
Pre = c(sens = 0.80, spec = 0.75) # pre-screen (粗筛, 最差)
),
endpoint = list(mu_G1_T0 = 0, delta_pos = 0.60, # 真 marker+: 对照均值0, 治疗 +0.60
mu_G0_T0 = 0, delta_neg = 0.05, # 真 marker-: 药物几乎无效
sd = 1.0)
)角色 → 数据列映射(角色定义见
§1.1,此处只说代码里落到哪列):CDx 列 =
FCD(新检测/最终 CDx)——注意别被列名误导,FCD
在代码里就是 CDx;CCD1/CCD2 =
CCD 的两次独立重复;CTA 列 =
CTA;Pre 列 = 粗筛。
两样东西配成一对——一个造数据、一个给标准答案:
generate_population() — 模拟器:先按
prevalence 抽真状态 \(G\),再让每个
assay 当 \(G\) 的噪声分类器(sens/spec
决定翻转概率、给定 \(G\)
独立)各自出结果。它也能顺带生成结局 \(Y\)(with_endpoint=TRUE),但那一层这一节先不用。给样本量
\(n\),吐一个数据集。TRUTH
算理论真值,当”标准答案”。这一节先用三个纯 assay
层(不涉及疗效)的:
assay_prev():某 assay 的边际阳性率 \(\Pr(\text{assay}{=}1)\)。true_concordance():两 assay 的真
PPA/NPA——泛化定义 PPA = \(\Pr(B{+}\mid A{+})\)(A 判阳的人里
B 也判阳的比例)、NPA = \(\Pr(B{-}\mid
A{-})\),在共同 \(G\)
下用联合概率算,不靠样本。true_p_cta_given_cdx():\(p=\Pr(CTA{+}\mid CDx{+})\),bridging
的关键权重。第四个算子 true_delta_in_assay_pos()
属结局层,等加了疗效再定义。套路统一:模拟估计 → 和闭式真值对撞
→ 验证公式。
generate_population <- function(n, truth = TRUTH, with_endpoint = FALSE) {
G <- rbinom(n, 1, truth$prev_G)
call_assay <- function(oc) ifelse(G == 1, rbinom(n, 1, oc["sens"]), rbinom(n, 1, 1 - oc["spec"]))
df <- data.frame(G = G, CTA = call_assay(truth$assays$CTA), CDx = call_assay(truth$assays$CDx),
CCD1 = call_assay(truth$assays$CCD), CCD2 = call_assay(truth$assays$CCD),
Pre = call_assay(truth$assays$Pre))
if (with_endpoint) { # 结局层:讲疗效时才用到
ep <- truth$endpoint; T_arm <- rbinom(n, 1, 0.5)
mu <- ifelse(G == 1, ep$mu_G1_T0 + T_arm*ep$delta_pos, ep$mu_G0_T0 + T_arm*ep$delta_neg)
df$T_arm <- T_arm; df$Y <- rnorm(n, mu, ep$sd)
}
df
}
# assay 层算子:边际阳性率; 两 assay 真 PPA/NPA; true p=Pr(CTA+|CDx+)。(δ 算子在结局层再定义)
assay_prev <- function(oc, prev = TRUTH$prev_G) oc["sens"]*prev + (1-oc["spec"])*(1-prev)
true_concordance <- function(ocA, ocB, prev = TRUTH$prev_G) {
pA1 <- function(g) if (g == 1) ocA["sens"] else 1-ocA["spec"]
pB1 <- function(g) if (g == 1) ocB["sens"] else 1-ocB["spec"]
jt <- function(a,b) sum(sapply(0:1, function(g){
pa <- if(a==1) pA1(g) else 1-pA1(g); pb <- if(b==1) pB1(g) else 1-pB1(g)
pa*pb*(if(g==1) prev else 1-prev)}))
c(PPA = unname(jt(1,1)/(jt(1,1)+jt(1,0))), NPA = unname(jt(0,0)/(jt(0,0)+jt(0,1))))
}
true_p_cta_given_cdx <- function(truth = TRUTH) {
oc_c <- truth$assays$CTA; oc_d <- truth$assays$CDx; prev <- truth$prev_G
num <- sum(sapply(0:1, function(g){pc<-if(g==1)oc_c["sens"] else 1-oc_c["spec"]; pd<-if(g==1)oc_d["sens"] else 1-oc_d["spec"]; pc*pd*(if(g==1)prev else 1-prev)}))
den <- sum(sapply(0:1, function(g){pd<-if(g==1)oc_d["sens"] else 1-oc_d["spec"]; pd*(if(g==1)prev else 1-prev)}))
unname(num/den)
}算子和生成器都齐了,当场”对表”验一次自洽——但先只验 assay 层(一致性 + \(p\),还不碰疗效):同一批真值用两条独立路子各算一遍——闭式(直接拿参数套公式)vs 模拟 200 万人再数比例(经验值)。两列几乎相等(“差”列≈0)就说明公式和模拟器都没写错、DGP 自洽——之后各节才敢直接拿闭式真值当基准。
big <- generate_population(2e6)
tc <- true_concordance(TRUTH$assays$CTA, TRUTH$assays$CDx) # 命名向量 c(PPA=, NPA=)
closed <- c(tc[["PPA"]], tc[["NPA"]], true_p_cta_given_cdx()) # [[ ]] 取值、不带元素名
emp <- c(mean(big$CDx[big$CTA==1]==1), # Pr(CDx+|CTA+) = PPA
mean(big$CDx[big$CTA==0]==0), # Pr(CDx−|CTA−) = NPA
mean(big$CTA[big$CDx==1]==1)) # Pr(CTA+|CDx+) = p
ft_show(data.frame(
`量` = c("PPA_{CTA,CDx} = Pr(CDx+|CTA+)",
"NPA_{CTA,CDx} = Pr(CDx−|CTA−)",
"p = Pr(CTA+|CDx+)"),
`闭式真值` = round(closed, 4),
`模拟 2M 估计` = round(emp, 4),
`差` = round(abs(closed - emp), 4),
check.names = FALSE),
title = "概念真值表(assay 层):闭式真值 vs 模拟估计")量 | 闭式真值 | 模拟 2M 估计 | 差 |
|---|---|---|---|
PPACTA,CDx = Pr(CDx+|CTA+) | 0.7540 | 0.7532 | 0.0008 |
NPACTA,CDx = Pr(CDx−|CTA−) | 0.8993 | 0.8994 | 0.0001 |
p = Pr(CTA+|CDx+) | 0.7829 | 0.7831 | 0.0002 |
✓ DGP(assay 层)自洽,可以拿它当后面各节的”标准答案”。
问题:一个不完美的 assay 判为阳性的那群人,他们的表观疗效,比真 marker+ 人群的疗效低多少? 这是 bridging 和 follow-on 都要用的通用事实——两种搬运搬的都是”assay 定义人群的疗效”,而它天生被 assay 的不完美稀释。所以放在地基里先讲清楚,回答它要用到疗效,正好把之前搁置的第三层补上。
到这里才需要疗效,把第三层补上。结局层:连续端点
\(Y\),均值只取决于治疗臂 \(T\) 和真实 \(G\);真 marker+ 疗效 \(\delta_{\text{pos}}\)、真 marker− 疗效
\(\delta_{\text{neg}}\)
各自给定——generate_population(..., with_endpoint = TRUE)
生成的正是这一层。同时补上属结局层的第四个算子:
true_delta_in_assay_pos <- function(oc, truth = TRUTH) { # assay+ 人群真疗效 = PPV 加权 δ
prev <- truth$prev_G
ppv <- oc["sens"]*prev / (oc["sens"]*prev + (1-oc["spec"])*(1-prev))
unname(ppv*truth$endpoint$delta_pos + (1-ppv)*truth$endpoint$delta_neg)
}它一上来就讲故事:真 marker+ 疗效 0.60,但 CDx+ 人群只 0.551(不完美 assay 把真阴性混进来、稀释),CTA+ 人群更低 0.482(CTA 更差)——bridging 要把 CTA+ 观测的疗效往 CDx+ 调,正是它存在的意义。
正式写出来前,先立只有两块的”积木”。把人按 (治疗臂 \(T\), 真实 marker 状态 \(G\)) 分成四格,每格的结局均值记 \(\mu_{TG}\):
| 真 marker+ (G=1) | 真 marker− (G=0) |
|---|---|---|
治疗 T=1 | μ11 | μ10 |
对照 T=0 | μ01 | μ00 |
这四格从哪来?一张图看清——先按真 marker \(G\) 把人分两支(两支都留),每支再 1:1 随机成治疗 / 对照,就落成四格 \(\mu_{TG}\):
par(family = "cjk", mar = c(0.5, 0.5, 3, 0.5))
plot(NA, xlim = c(0, 14), ylim = c(-0.9, 6), axes = FALSE, xlab = "", ylab = "",
main = "四格 μ 从哪来:真 marker G × 治疗臂 T", cex.main = 1.3)
draw_box(1.3, 3, "患者", "gray30", w = 1.9, h = 0.85, cex = 0.95)
draw_box(3.9, 3, "看真实 marker G\n(上帝视角)", "gray45", w = 2.7, h = 1.0, cex = 0.85)
draw_arr(2.25, 3, 2.55, 3)
draw_box(6.9, 4.4, "真 marker+\nG=1", "#2E86AB", w = 2.3, h = 0.9, cex = 0.9)
draw_box(6.9, 1.4, "真 marker−\nG=0", "#AED6F1", w = 2.3, h = 0.9, cex = 0.9, txt = "gray20", border = "#7FB3D5")
draw_arr(5.25, 3.2, 5.75, 4.3); draw_arr(5.25, 2.8, 5.75, 1.5)
draw_box(10.0, 5.1, bquote(paste("T=1 → ", mu["11"])), "#2E86AB", w = 2.7, h = 0.6, cex = 0.9)
draw_box(10.0, 3.7, bquote(paste("T=0 → ", mu["01"])), "#2E86AB", w = 2.7, h = 0.6, cex = 0.9)
draw_box(10.0, 2.1, bquote(paste("T=1 → ", mu["10"])), "#AED6F1", w = 2.7, h = 0.6, cex = 0.9, txt = "gray20", border = "#7FB3D5")
draw_box(10.0, 0.7, bquote(paste("T=0 → ", mu["00"])), "#AED6F1", w = 2.7, h = 0.6, cex = 0.9, txt = "gray20", border = "#7FB3D5")
draw_arr(8.05, 4.6, 8.65, 5.1); draw_arr(8.05, 4.2, 8.65, 3.7)
draw_arr(8.05, 1.6, 8.65, 2.1); draw_arr(8.05, 1.2, 8.65, 0.7)
brace(11.55, 3.7, 5.1); text(12.5, 4.4, bquote(delta[pos]), col = "#2E86AB", cex = 1.25, font = 2)
brace(11.55, 0.7, 2.1); text(12.5, 1.4, bquote(delta[neg]), col = "#3B7DB0", cex = 1.25, font = 2)
text(7, -0.6, "μ 下标 =(治疗臂 T, 真 marker G)。每 marker 内「治疗 − 对照」= 疗效:δpos = μ11 − μ01(真+)、δneg = μ10 − μ00(真−)",
col = "gray25", cex = 0.8, font = 3)μ 的两位下标就是这么来的:第一位 = 治疗臂 \(T\)(哪一支:治疗 / 对照)、第二位 = 真 marker \(G\)(哪一列:真+ / 真−)。每一列的”治疗 − 对照”就是那个真实人群的治疗效应——全文只有这两块积木:
\[\delta_{\text{pos}}=\mu_{11}-\mu_{01}\ (\text{真 marker+ 的疗效}),\qquad \delta_{\text{neg}}=\mu_{10}-\mu_{00}\ (\text{真 marker− 的疗效}).\]
这些 \(\mu\) 是什么数? 把 \(Y\) 想成一个连续疗效读数(比如肿瘤缩小量、某评分的变化,单位随意),\(\mu_{TG}\) 就是某一格人 \(Y\) 的平均值——一个实数,可正可负,不是百分比。本文把对照臂均值设成基准 0,代入 DGP 的参数就是:\(\mu_{01}=0\)(marker+ 对照)、\(\mu_{11}=0.60\)(marker+ 用药,平均比对照高 0.60)、\(\mu_{00}=0\)(marker− 对照)、\(\mu_{10}=0.05\)(marker− 用药,几乎没动)。相减即疗效:\(\delta_{\text{pos}}=0.60-0=0.60\)、\(\delta_{\text{neg}}=0.05-0=0.05\)。个体的 \(Y\) 再在这均值上下按正态分布(均值 \(\mu\)、标准差 \(1\))抖动,所以某个 marker+ 用药者可能 \(Y=1.3\)、也可能 \(Y=-0.2\)。记住两类量别混:\(\mu\)、\(\delta\)、\(\eta\)、\(\theta\) 是疗效读数或其差,实数、可正可负;而 \(\pi\)、\(sens/spec\)、\(PPV\)、\(PPA\)、\(p\)、\(c\) 是概率或比率,取值都在 \(0\) 与 \(1\) 之间。
有真 \(G\) 时,FCD+ 人群的表观疗效 \(\eta\) 就是这两块积木的加权平均,权重是该 assay 的阳性预测值 \(PPV_f=\Pr(G{=}1\mid FCD{=}1)\)(“判为阳性的人里真的是 marker+ 的比例”):
\[\eta = PPV_f\,\delta_{\text{pos}} + (1-PPV_f)\,\delta_{\text{neg}}.\]
这条恒等式和”FCD”没什么特殊关系。 把”按真 \(G\) 分”换成”按任意 assay A 的判读分”,同一条推导照走——A 判阳人群的疗效 \(=PPV_A\,\delta_{\text{pos}}+(1-PPV_A)\,\delta_{\text{neg}}\)。两块真疗效积木 \(\delta_{\text{pos}}\)、\(\delta_{\text{neg}}\) 永远不变,变的只有权重 \(PPV_A\)。 这就是「参考标准可得框架 (reference-standard-available framework)」的全部内容:给每个 assay 一套按”臂 \(T\) × 该 assay 判读”索引的四格均值,它 A+ 人群的臂间差就落成上面这个混合。
一张表把最容易混的 \(\mu\)、\(\delta\)、\(\eta\)(\(\theta\))一次钉死——横看是”三级台阶”:层 → 相减得积木 → 按 PPV 加权;第 ③ 级那一行竖看,又正好是同一框架的三个实例(\(\mu\) 供积木,\(\eta\)、\(\theta\) 只换 PPV 权重):
台阶 | 符号 | 是什么 / 按什么索引 | 关系式 / 例 |
|---|---|---|---|
① 层 | μTG | 一格人的结局均值(和 Y 同单位);按 臂 T × 真 marker G 索引 | μ11, μ01, |
② 相减 → 得积木 | δpos, | 同一真实人群 治疗−对照;全文仅此两块真疗效积木 | δpos = μ11−μ01; |
③ 按 PPV 加权 → 混积木 | η(FCD+), | assay+ 人群的表观疗效;按 该 assay 判读 索引,权重 = 该 assay 的 PPV | η = PPVf·δpos+(1−PPVf)·δneg; |
读法:\(\eta\) 正是本文主角 bridging 要还原的量(FCD 就是最终 CDx);\(\theta\) 是 follow-on 要”借”的量(CCD 已确立疗效,\(PPV_c=\Pr(G{=}1\mid CCD{=}1)\))。两者做法完全对称——follow-on 章把 \(\theta\) 拿去用,估计、混合、稀释和这里的 \(\eta\) 一模一样,到那儿没有新东西。数下标最好记:\(\mu_{TG}\) 带两个下标(治疗臂 \(T\) × 真 marker \(G\)——要两件事才定得下一个均值);\(\delta_{\text{pos}}/\delta_{\text{neg}}\) 只剩一个下标(真 marker——臂差已把 \(T\) 吃掉);\(\eta\)、\(\theta\) 一个真-marker 下标都没有——恰恰因为看不见真 marker,才不得不用 PPV 把两块积木糊成一个。
所以 \(\eta\) 本身就是一种 \(\delta\)(assay+ 人群那一版的疗效),\(\theta\) 也是(换 CCD 而已)。后面 bridging 还是这同一招,只换了”按什么分组、用什么权重”:把 CDx+ 人群按 CTA 状态再切成两格、按占比加权——积木仍是 \(\delta_{\text{pos}}\)、\(\delta_{\text{neg}}\) 那两块。(届时会出现 \(\delta_{11}\)、\(\delta_{01}\)、\(\delta_{CDx}\) 这样的记号,两位数字指 (CTA, CDx) 格子、不是 μ 那种 (臂, 真 marker)——到 bridging 章正式定义,这里知道”同一招”即可,不用现在就懂。)吃透这里的 μ→δ→η(外加孪生 θ)就够,后文不再添新”积木”。 一张图看懂这个”加权混合”(以最好的 assay CDx 为例,\(PPV_f\approx0.91\)):
par(family = "cjk", mar = c(0.5, 0.5, 3, 0.5))
plot(NA, xlim = c(0, 1), ylim = c(0, 1), axes = FALSE, xlab = "", ylab = "",
main = "疗效混合:assay+ 人群的表观疗效被 PPV 稀释", cex.main = 1.45)
PPV <- 0.91; dpos <- 0.60; dneg <- 0.05; eta <- PPV*dpos + (1-PPV)*dneg
xof <- function(d) 0.08 + (d/0.65)*0.84
xL <- 0.08; xR <- 0.92; sp <- xL + (xR-xL)*(1-PPV) # 左=marker−、右=marker+,与下方数轴同序
text(0.5, 0.93, "① assay+ 人群的构成(按真实 marker 状态)", cex = 1.05, font = 2)
rect(xL, 0.72, sp, 0.84, col = "#AED6F1", border = "white", lwd = 2)
rect(sp, 0.72, xR, 0.84, col = "#2E86AB", border = "white", lwd = 2)
text((xL+sp)/2, 0.90, sprintf("真 marker− %.0f%%", 100*(1-PPV)), col = "#3B7DB0", font = 2, cex = 0.82)
text((sp+xR)/2, 0.78, bquote(paste("真 marker+ ", PPV[f], "≈", .(round(100*PPV)), "%")), col = "white", font = 2, cex = 0.95)
text(0.5, 0.5, "② 各自真实疗效,按上面比例加权 → 表观疗效 η", cex = 1.05, font = 2)
segments(xof(0), 0.30, xof(0.65), 0.30, lwd = 2, col = "gray50")
for (d in c(0,0.2,0.4,0.6)) { segments(xof(d),0.29,xof(d),0.31,lwd=1.5,col="gray50"); text(xof(d),0.245,d,cex=0.9,col="gray40") }
text(0.5, 0.16, "疗效 δ(治疗 vs 对照)", cex = 1.0, col = "gray30")
points(xof(dneg), 0.30, pch = 15, col = "#3B7DB0", cex = 2.4)
points(xof(dpos), 0.30, pch = 15, col = "#2E86AB", cex = 2.4)
points(xof(eta), 0.30, pch = 19, col = "#A23B72", cex = 3.0)
text(xof(dneg), 0.38, sprintf("真−\nδ=%.2f", dneg), col = "#3B7DB0", font = 2, cex = 0.9)
text(xof(dpos), 0.38, sprintf("真+\nδ=%.2f", dpos), col = "#2E86AB", font = 2, cex = 0.9)
text(xof(eta), 0.40, sprintf("η≈%.2f", eta), col = "#A23B72", font = 2, cex = 1.05)
text(0.5, 0.05, expression(eta == PPV[f] * delta[pos] + (1 - PPV[f]) * delta[neg]), cex = 1.2)PPV 越大(assay 越好),\(\eta\) 越靠近 \(\delta_{\text{pos}}\)、稀释越少;反过来,assay 越差、混进的真阴性越多,表观疗效越被拖低。
下面把这套混合用三条独立的路各算一遍,看对不对得上——而且顺手把 \(\eta\)(CDx+)和它的孪生 \(\theta\)(CCD+)一起算,当场验证”换个 assay、同一台机器”(下面以 CDx 为例讲三条路,CCD 完全一样):
mean(sub$G==1),即”CDx
判阳的人里真 \(G{=}1\) 的比例”)。delta_hat)。这正是真实试验会观测到的”表观疗效”本身。true_delta_in_assay_pos()——不抽样,先用贝叶斯从参数精确算出
\(PPV_f=\dfrac{\text{sens}\cdot\text{prev}}{\text{sens}\cdot\text{prev}+(1{-}\text{spec})(1{-}\text{prev})}\)(这里
sens/spec 取 CDx 的),再套同一条混合公式。pop <- generate_population(3e5, with_endpoint = TRUE)
mix_check <- function(assay_col, oc) {
sub <- pop[pop[[assay_col]] == 1, ]
ppv <- mean(sub$G == 1) # 从样本数出的 PPV
c(PPV = round(ppv, 4),
`混合公式` = round(ppv*TRUTH$endpoint$delta_pos + (1-ppv)*TRUTH$endpoint$delta_neg, 4),
`直接估计` = round(delta_hat(sub), 4), # 只量 Y,不预设任何模型
`闭式真值` = round(true_delta_in_assay_pos(oc), 4)) # 不抽样,从参数精确算
}
tab <- rbind(`η(FCD+ = CDx+)` = mix_check("CDx", TRUTH$assays$CDx),
`θ(CCD+)` = mix_check("CCD1", TRUTH$assays$CCD))
ft_show(tab, title = "同一台机器换个 assay:η、θ 各自三条路都对齐", rowname = "人群疗效")人群疗效 | PPV | 混合公式 | 直接估计 | 闭式真值 |
|---|---|---|---|---|
η(FCD+ = CDx+) | 0.9112 | 0.5512 | 0.5578 | 0.5508 |
θ(CCD+) | 0.8717 | 0.5295 | 0.5304 | 0.5280 |
✓ 两行、各自三路对齐:\(\eta\)(CDx+)≈0.55、\(\theta\)(CCD+)≈0.53——CCD 比 CDx
略差、\(PPV\)
稍低,混进的真阴性更多、疗效被稀释得更狠,完全在意料之中。这正是概念地基兑现成数字:\(\eta\)、\(\theta\)
不是抽象记号,而是模拟世界里数得出、对得上的真实量。有个微妙点值得点破:混合公式和闭式真值其实都用了那条混合公式(只是
\(PPV\)
一个来自样本、一个来自参数),它俩对上只说明”样本 PPV≈真
PPV”。真正独立的裁判是「直接估计」——它只老实量 \(Y\)、不预设任何混合模型;它也和闭式真值对上,才证明”assay+
疗效 = 真+/真− 按 PPV
混合”这个模型本身没说错。η 和 θ 用的是同一个
mix_check,只换了 assay
列——框架的对称性,在代码里就是同一个函数。
这里有个容易滑过、却很关键的点。 这里的混合、以及后面 bridging 的搬运,本质是同一句话:合并人群的疗效 = 各子群疗效按人数占比加权平均。当疗效用的是均值差 \(\delta\) 时,这句话是恒等式、永远成立——因为”一群人的平均值 = 各子群平均值按占比加权”本就是平均数的定义,两臂各自取平均再相减,这个加权关系原样传下来。
但“按占比加权平均”并非对任何疗效度量都成立——只在疗效是均值型(而非比值型)时才精确。 最典型的反例是 Cox 模型的风险比 HR:即便药在两个子群里 HR 都恰好等于 \(0.6\),把两个 \(0.6\) 按占比加权也得不到合并人群的 HR(合并后通常朝 \(1\) 漂)。原因是比值不是期望、不服从”按占比加权”,这个性质叫不可塌缩 (non-collapsible)。所以本文统一用连续端点的均值差,是为了让搬运恒等式精确成立、教学干净;真实试验里疗效常报成 HR,这时”看似能线性搬运、其实得先换到可塌缩的尺度(均值差 / 某时点生存率差 / RMST)“本身就是 bridging 会踩的坑。完整展开见末章 「补充:为什么用连续端点」。
整章的量,串成一张图。两条线都从真相 \(G\) 出发:往左,\(G\) 被各 assay 有噪声地测量(每个 assay 一副 \(sens/spec\)),派生出 \(PPV/NPV\)、\(PPA/NPA\)、搬运权重 \(p\);往右,\(G\) 和随机的治疗臂 \(T\) 正交地共同决定四格结局均值 \(\mu_{TG}\),同列(同一 marker、沿 \(T\))相减得两块积木 \(\delta_{\text{pos}}\)、\(\delta_{\text{neg}}\)。两条线在最底部汇合:任何 assay+ 人群的疗效,都是这两块积木、按左边那条线算出的 \(PPV\) / \(p\) 加权——这就是 \(\eta\)(FCD+)、\(\theta\)(CCD+),以及通向第 4 章 bridging 的 \(\delta_{CDx}\)。
par(family = "cjk", mar = c(0.5, 0.5, 3, 0.5))
plot(NA, xlim = c(0, 14), ylim = c(0, 11), axes = FALSE, xlab = "", ylab = "",
main = "地基全景:真相 → assay 与结局(治疗臂 T 正交),最终在疗效混合处汇合",
cex.main = 1.2)
# 多行框:每行可为普通字符串或 plotmath 表达式(下标走 X[sub],与表/正文一致的真下标)
cellbox <- function(x, y, w, h, lines, fill, tcol, cex = 0.86, border = "white", lwd = 2) {
rect(x-w/2, y-h/2, x+w/2, y+h/2, col = fill, border = border, lwd = lwd)
n <- length(lines)
ys <- y + (h/2 - 0.12) - (seq_len(n) - 0.5) * (h - 0.24) / n
fnt <- if (identical(tcol, "white")) 2 else 1
for (i in seq_len(n)) text(x, ys[i], lines[[i]], col = tcol, cex = cex, font = fnt)
}
# ① 真相层 G(根)
draw_box(7, 10.2, "真相层 G ∈ {0,1}\nπ = Pr(G=1)(真实患病率)", "#6A4C93", w = 4.4, h = 1.0, cex = 0.95)
# ② assay 层(G 的噪声分类器)+ ③ 结局层 μ(G × T 四格)
draw_box(3.1, 7.4, "assay 层 A = G 的噪声分类器\nCTA · CDx(=FCD) · CCD · Pre", "#2E86AB", w = 4.8, h = 1.1, cex = 0.88)
cellbox(10.9, 7.4, 4.8, 1.1, list(
expression(paste("结局层 ", mu[TG], " 四格结局均值")),
"行 = 治疗臂 T,列 = 真 marker G"), "#A23B72", "white", cex = 0.88)
draw_arr(5.9, 9.75, 3.9, 8.0, cex = 0.82); text(4.9, 8.95, expression(paste(sens[A], ", ", spec[A])), cex = 0.82, col = "gray30", font = 3)
draw_arr(8.1, 9.75, 9.9, 8.0, "决定真实疗效", cex = 0.82, adj_y = 0.30)
# ④ 治疗臂 T(正交轴)
draw_box(12.4, 9.9, "治疗臂 T ∈ {0,1}\n随机、与 G 独立(正交)", "#E8871E", w = 3.0, h = 0.95, cex = 0.78)
draw_arr(11.9, 9.4, 11.2, 8.0, "另一维", cex = 0.8, adj_y = 0.25)
# ⑤ assay 分支 → 闭式导出
cellbox(3.1, 4.5, 5.7, 1.6, list(
"闭式导出(算子精确算,不抽样)",
expression(paste(PPV[A], ", ", NPV[A], ":Pr(G=1|A=1)、Pr(G=0|A=0)")),
"PPA / NPA(assay 间一致性)",
"p = Pr(CTA+ | CDx+)(bridging 搬运权重)"), "#E3F1F7", "gray15", border = "#2E86AB", lwd = 1.6)
draw_arr(3.1, 6.85, 3.1, 5.35)
# ⑥ 结局分支 → 两块积木
cellbox(10.9, 4.5, 5.7, 1.6, list(
"两块积木(同列、治疗 − 对照)",
expression(paste(delta[pos], " = ", mu["11"], " − ", mu["01"], "(真 marker+ 疗效)")),
expression(paste(delta[neg], " = ", mu["10"], " − ", mu["00"], "(真 marker− 疗效)"))),
"#F7E6EF", "gray15", border = "#A23B72", lwd = 1.6)
draw_arr(10.9, 6.85, 10.9, 5.35, "沿 T 相减", cex = 0.8, adj_y = 0.25)
# ⑦ 汇合:疗效混合(PPV / p 当权重)
cellbox(7, 1.5, 10.6, 2.0, list(
"疗效混合:两块积木,按「该人群的 PPV / p」加权",
expression(paste(eta, " = ", PPV[f], " · ", delta[pos], " + (1−", PPV[f], ") · ", delta[neg], " → FCD(=CDx)+ 人群疗效")),
expression(paste(theta, " = ", PPV[c], " · ", delta[pos], " + (1−", PPV[c], ") · ", delta[neg], " → CCD+ 人群疗效")),
expression(paste(delta[CDx], " = p · ", delta["11"], " + (1−p) · ", delta["01"], " → bridging 目标(第 4 章)"))),
"#FBF3DE", "gray15", cex = 0.86, border = "#E8871E", lwd = 1.6)
draw_arr(9.9, 3.7, 8.6, 2.5) # 积木 → 混合
draw_arr(4.3, 3.7, 5.4, 2.5, "PPV / p 作权重", cex = 0.82, adj_y = 0.30) # 权重跨分支 → 混合图里的量分三种身份,正对应本章”设参数 → 算真值 →
模拟对撞”的三步:设定——\(\pi\)、各 assay 的 \(sens/spec\)、\(\delta_{\text{pos}}\)、\(\delta_{\text{neg}}\),是你在
TRUTH 里直接给的参数;闭式导出——\(PPV/NPV\)、\(PPA/NPA\)、\(p\),以及 \(\eta/\theta\)
的真值,由算子不抽样、纯从参数精确算;模拟估计——以上每个量在生成数据里数出的经验版。本章
§「生成器、真值算子与自洽验证」做的正是把后两者对撞、确认相等,DGP
才够格当”标准答案”。\(T\) 与
\(G\) 正交是关键:\(T\) 随机分配、独立于 marker,所以 \(\mu\) 才需要 \(T\)、\(G\)
两个下标,而疗效永远是在某个 marker 分组内、沿 \(T\) 相减得到的。
地基搭好,先讲本文主角 bridging:自己试验里就有疗效数据(不管是 RCT 的臂间差还是单臂的应答率),难点是把它从”入组用的 CTA 定义的人群”搬到”最终 CDx 定义的人群”,而两者不重合、还缺了一整格数据。(第五章的 follow-on 是同一套骨架换一种缺失结构——自己不做疗效试验、改靠与已批准 CCD 的一致性间接搬运。)
动机前面讲过:bridging 出现在最终 CDx 与 pivotal trial 入组 assay 不一致时。监管指导把它描述为:用原临床试验样本和预设统计分析计划比较 CTA 与候选 CDx,并据此把 CTA 的临床性能外推到最终 CDx;后续方法学工作进一步系统讨论了缺失样本、local test、pre-screening 和低阳性率 biomarker 等问题 (U.S. Food and Drug Administration 2016; Song et al. 2024)。
先说清这里 \(\delta\) 的下标,别被第三章绊到。 第三章的 \(\delta_{\text{pos}}/\delta_{\text{neg}}\) 按真实 marker 分组、只有一个标签;这里的 \(\delta\) 带两位数字,但这两位是 (CTA 状态, CDx 状态),不是 \(\mu_{TG}\) 那种 (治疗臂 \(T\), 真 marker \(G\))。读法:\(\delta_{ab}\) = 在”CTA\(=a\) 且 CDx\(=b\)“这一格里的疗效(治疗 − 对照)——所以 \(\delta_{11}\) 是 (CTA+ & CDx+)、\(\delta_{01}\) 是 (CTA− & CDx+)。两点要记住:其一,\(\delta\) 和 \(\delta_{\text{pos}}\) 一样本身已经是臂间差、不带治疗臂下标,两位数字只标”落在哪个观察得到的格子”,跟 \(T\) 无关(\(T\) 已被”治疗 − 对照”这步吃掉);其二,这里改按 CTA/CDx 分组、而非第三章的真 \(G\),是因为真实试验里真 marker \(G\) 看不到,只能按观察得到的 assay 判读来切人群。
进入”怎么做”,三步:
此外还有一个可选、触发式的分析,跟第 3 步是两回事:
(标准 bridging 的结构性缺失就第 3 步这一处、一个 \(c\);末章「进阶」里 pre-screening、多 CTA 会引入更多结构性缺失格,第 3 步那类敏感性就要做多份。第 4 步则始终是那个独立的、可选的测量缺失分析。本文 worked example 两步都走:第 3 步见「worked:三步在 DGP 上走一遍」,第 4 步——含插补的内部机制、MAR 与 MNAR 的后果——见「第 4 步:入组了,却没有 CDx 判读」及随后两节。)操作上,用最终 CDx 重测可用筛查样本,尽量含 CTA− 筛查样本和全部 CTA+ 入组样本(富集设计下尤其重要)。
问题:CDx+ 人群疗效 \(\delta_{CDx}\) 想估,可里面一整块(CTA−/CDx+)试验里根本没人——缺的这块怎么补? 富集设计只入组 CTA+;回溯性贴 CDx 后,CTA−/CDx+ 亚组未观察到:
par(family = "cjk", mar = c(0.5, 0.5, 3, 0.5))
plot(NA, xlim = c(0, 16), ylim = c(-0.5, 7.4), axes = FALSE, xlab = "", ylab = "",
main = "标准 bridging 全树:CTA × CDx × 臂 = 8 个末端,缺失打叉", cex.main = 1.2)
lb <- "#AED6F1"; lbb <- "#7FB3D5" # 未入组:淡蓝 + 深边
xm <- function(x, y, col, hw = 0.9, hh = 0.23) { # 叉:标"不存在"
segments(x-hw, y-hh, x+hw, y+hh, col = col, lwd = 2.4); segments(x-hw, y+hh, x+hw, y-hh, col = col, lwd = 2.4) }
arm <- function(y, col, tx) draw_box(10.0, y, tx, col, w = 1.8, h = 0.46, cex = 0.72,
txt = if (identical(col, lb)) "gray30" else "white", border = if (identical(col, lb)) lbb else "white")
draw_box(1.2, 3.65, "筛查\n人群", "gray30", w = 1.8, h = 0.9, cex = 0.85)
# ① 先按 CTA 分(入组用)
draw_box(3.8, 5.45, "CTA+\n入组", "#2E86AB", w = 1.9, h = 0.8, cex = 0.85)
draw_box(3.8, 1.85, "CTA−\n没入组", lb, w = 1.9, h = 0.8, cex = 0.85, txt = "gray20", border = lbb)
draw_arr(2.1, 3.8, 2.85, 5.3); draw_arr(2.1, 3.5, 2.85, 2.0)
# ② 再回贴 CDx → 四格 a,b,c,d
draw_box(6.6, 6.25, bquote(paste(bold("a"), " CTA+&CDx+")), "#2E86AB", w = 2.7, h = 0.6, cex = 0.8)
draw_box(6.6, 4.65, bquote(paste(bold("c"), " CTA+&CDx−")), "gray55", w = 2.7, h = 0.6, cex = 0.8)
draw_box(6.6, 2.65, bquote(paste(bold("b"), " CTA−&CDx+")), lb, w = 2.7, h = 0.6, cex = 0.8, txt = "gray20", border = lbb)
draw_box(6.6, 1.05, bquote(paste(bold("d"), " CTA−&CDx−")), lb, w = 2.7, h = 0.6, cex = 0.8, txt = "gray20", border = lbb)
draw_arr(4.75, 5.6, 5.25, 6.25); draw_arr(4.75, 5.3, 5.25, 4.65)
draw_arr(4.75, 2.0, 5.25, 2.65); draw_arr(4.75, 1.7, 5.25, 1.05)
# ③ 每格再分治疗 / 对照 → 8 个末端
arm(6.6, "#2E86AB", "治疗 T=1"); arm(5.9, "gray60", "对照 T=0")
arm(5.0, "gray60", "治疗 T=1"); arm(4.3, "gray72", "对照 T=0")
arm(3.0, lb, "治疗 T=1"); arm(2.3, lb, "对照 T=0")
arm(1.4, lb, "治疗 T=1"); arm(0.7, lb, "对照 T=0")
draw_arr(7.95, 6.3, 9.1, 6.6); draw_arr(7.95, 6.2, 9.1, 5.9)
draw_arr(7.95, 4.7, 9.1, 5.0); draw_arr(7.95, 4.6, 9.1, 4.3)
draw_arr(7.95, 2.7, 9.1, 3.0); draw_arr(7.95, 2.6, 9.1, 2.3)
draw_arr(7.95, 1.1, 9.1, 1.4); draw_arr(7.95, 1.0, 9.1, 0.7)
brace(11.0, 5.9, 6.6); text(12.6, 6.25, bquote(paste(delta[11], " 可估")), col = "#2E86AB", cex = 0.9, font = 2)
brace(11.0, 2.3, 3.0); text(12.6, 2.65, bquote(paste(delta["01"], " 缺失")), col = "gray30", cex = 0.9, font = 2)
text(11.5, 4.65, "c,d = CDx−(不搬)", col = "gray45", cex = 0.75, font = 3)
segments(2.9, 0.25, 14.2, 3.45, col = "#C73E1D", lwd = 3); segments(2.9, 3.45, 14.2, 0.25, col = "#C73E1D", lwd = 3) # 红叉:CTA− 整支
xm(10.0, 5.9, "#E8871E"); xm(10.0, 4.3, "#E8871E") # 橙叉:单臂才删对照(CTA+ 支的对照 T=0)
segments(0.5, 7.2, 1.0, 7.2, col = "#C73E1D", lwd = 3); text(1.15, 7.2, "红叉 = CTA− 富集未入组(取舍一)", col = "#C73E1D", cex = 0.72, font = 2, adj = 0)
xm(0.75, 6.8, "#E8871E", 0.25, 0.13); text(1.15, 6.8, "橙叉 = 单臂才删对照(取舍二;RCT 下对照在、δ11 可估)", col = "#E8871E", cex = 0.72, font = 2, adj = 0)
text(8, -0.3, expression(paste("目标 CDx+ = a+b。RCT:a 两臂都在 → ", delta[11], " = 治疗 − 对照可估;单臂:删对照(橙叉) → δ 用应答率(前提对照≈0)。b 随 CTA− 没入组 → ", delta["01"], " 缺失。p = a/(a+b),", delta[CDx], " = p·", delta[11], " + (1−p)·", delta["01"])),
col = "gray20", cex = 0.76, font = 2)\[\delta_{CDx} = p\,\delta_{11} + (1-p)\,\delta_{01},\qquad p=\Pr(CTA{+}\mid CDx{+})\]
“臂差 vs 单臂应答率” 上面 \(\delta_{11}=\) 治疗 − 对照,是个臂间差(对照差, contrast)——要有对照臂,答的是”用药比不用好多少”;对照替你扣掉了疾病自然史 / 安慰剂那一部分,所以这个差能干净地归因于药。单臂应答率(如 ORR,objective response rate,客观缓解率)没有对照,只在治疗组里量”缓解”的比例——是个绝对水平(level),答的是”治了有多少人缓解”;它扣不掉自然史,得拿历史 / 预设阈值当参照。单臂之所以立得住,靠的是”肿瘤不会自己缩、客观缓解可归因于药”(何时能单臂、历史对照的偏倚,详见 §「取舍二」)。
为什么单臂”只量治疗组”也能当疗效?把”臂”这一维显式写出来就清楚了(平时省掉、一滑而过,反而糊)。严格讲,每个格子里有两个臂均值,用一个带治疗臂下标的记号:\(\mu_{T;\,11}\) = 治疗臂 \(T\in\{1,0\}\)、在 (CTA+ & CDx+) 格里的结局均值。于是臂差 \(\delta_{11}=\mu_{1;11}-\mu_{0;11}\)(治疗 − 对照,都在这一格)。平时把 \(T\) 这维省掉、直接写 \(\delta_{11}\),是因为差分已经把它吃掉——但省掉不等于不存在,对照项 \(\mu_{0;11}\) 一直在那儿。
单臂设计里没有对照臂,量到的只有 \(\mu_{1;11}\)(治疗组的应答水平)。它能顶替疗效,靠的正是一个假设:对照项 \(\mu_{0;11}\approx 0\)——ORR 这类客观缓解终点,不治疗几乎无人自发缓解(肿瘤不自己缩),所以 \(\delta_{11}=\mu_{1;11}-\mu_{0;11}\approx\mu_{1;11}\),“治疗组应答率”就≈“臂差”。这个假设不写出来就滑过去了,其实是单臂能成立的关键前提。(本文 DGP 恰好也把对照臂均值设成 \(0\)——\(\mu_{01}=\mu_{00}=0\),见地基——所以那里 \(\delta_{\text{pos}}=\mu_{11}\)、\(\delta_{\text{neg}}=\mu_{10}\),治疗组水平直接就等于疗效,与单臂同构。)
一句话对比:臂差是”相对某对照的差值”,应答率是”治疗组里的绝对水平”(后者当疗效用,前提是对照水平≈0)。但对 bridging 而言两者是一回事:应答率 = “是否缓解”这个 \(0/1\) 变量的均值,和均值差一样属均值型、可按人数占比线性混合——所以把本章的 \(\delta\) 一律替换成 ORR,搬运恒等式 \(\delta_{CDx}=p\,\delta_{11}+(1-p)\,\delta_{01}\) 照样精确成立、公式一个都不用改。唯一不能这样换的是比值型的 HR(风险比不可塌缩,混合会偏,见末章 「补充:为什么用连续端点」)。
CDx × CTA 的 2×2 一致性表——格里 \(a,b,c,d\) 是落入该格的病人数(样本数),不是概率:如 \(a\) = 既 CTA+ 又 CDx+ 的人数、\(d\) = 既 CTA− 又 CDx− 的人数,四格加起来 = 总样本量。
| CTA+ | CTA- |
|---|---|---|
CDx+ | a | b |
CDx- | c | d |
两个一致率直接从表读出(行 = CDx,列 = CTA):
\[PPA=\Pr(CDx{+}\mid CTA{+})=\frac{a}{a+c},\qquad NPA=\Pr(CDx{-}\mid CTA{-})=\frac{d}{b+d}\]
即 PPA 是 \(CTA{+}\) 那列里 \(CDx{+}\) 的比例、NPA 是 \(CTA{-}\) 那列里 \(CDx{-}\) 的比例。还有第三个常用指标 OPA(overall percent agreement,总体一致率)——两检测判读完全一致的比例:
\[OPA=\Pr(CDx{=}CTA)=\frac{a+d}{a+b+c+d}\]
但 OPA 由患病率主导(把 PPA、NPA 按边际比例混成一个数),某一类占绝大多数时会虚高,所以监管一致性以 PPA、NPA 分开报为主、OPA 只作总览。本文的搬运公式用 PPA/NPA 加权重 \(p\),不依赖 OPA——列出只为记号完整。
把 \(a,b,c,d\) 和疗效 \(\delta\) 对上(这里最容易晕)。 是同一张 CTA×CDx 网格、两个层:\(a,b,c,d\) 数的是人数(谁落哪格),\(\delta\) 是那格里的疗效(治疗 − 对照的 \(Y\) 均值差)。把疗效层也摆成同布局的 2×2(行 = CDx、列 = CTA,和上面计数表一格对一格):
| CTA+ | CTA- |
|---|---|---|
CDx+ | δ11 | δ01 |
CDx- | δ10 | δ00 |
一格对一格:人数 \(a\) 那格的疗效是 \(\delta_{11}\)、\(b\) 格是 \(\delta_{01}\)、\(c\) 格是 \(\delta_{10}\)、\(d\) 格是 \(\delta_{00}\)。bridging 只搬 CDx+ 那一行(\(\delta_{11}\)、\(\delta_{01}\)):\(\delta_{11}\)(配人数 \(a\))观察得到、\(\delta_{01}\)(配人数 \(b\))缺失。按人数加权就是搬运公式——
\[\delta_{CDx}=\frac{a\,\delta_{11}+b\,\delta_{01}}{a+b}=p\,\delta_{11}+(1-p)\,\delta_{01},\qquad p=\frac{a}{a+b}=\Pr(CTA{+}\mid CDx{+}).\]
(CDx− 行的 \(\delta_{10}\)、\(\delta_{00}\) 不是搬运目标,本文不用。)
Bayes 估 \(p=\Pr(CTA{+}\mid CDx{+})\):\(p=\dfrac{PPA\cdot prev}{PPA\cdot prev+(1{-}NPA)(1{-}prev)}\),其中 \(prev=\Pr(CTA{+})\) 须对应意向使用人群。下面核对这条 Bayes 式与直接估计一致:
PPA_t <- 0.90; NPA_t <- 0.85; prev <- 0.30; n <- 1e4
a <- n*prev*PPA_t; b <- n*(1-prev)*(1-NPA_t) # 用设定的 PPA/NPA/prev 造一张表
ft_show(data.frame(`p(Bayes 式)` = round((PPA_t*prev)/(PPA_t*prev+(1-NPA_t)*(1-prev)), 4),
`p(直接从表)` = round(a/(a+b), 4), check.names = FALSE),
title = "Bayes 估 p = 直接从表算")p(Bayes 式) | p(直接从表) |
|---|---|
0.72 | 0.72 |
把 §4.1 的三步在已知真相的世界里逐一跑通,每步都从模拟数据估出、再和闭式真值对撞——看清楚哪些量数据能给、最终 \(\delta_{CDx}\) 到底卡在哪一步的假设上。
步骤 1 · 一致性 → 搬运权重 \(p\)。 用一份代表性筛查样本(CTA、CDx 都测、含 CTA−、未富集)估 PPA/NPA,再 Bayes 得 \(p=\Pr(CTA{+}\mid CDx{+})\):
scr <- generate_population(3e5) # 代表性筛查样本(未富集,含 CTA−)
PPA_hat <- mean(scr$CDx[scr$CTA==1]==1) # Pr(CDx+|CTA+)
NPA_hat <- mean(scr$CDx[scr$CTA==0]==0) # Pr(CDx−|CTA−)
p_hat <- mean(scr$CTA[scr$CDx==1]==1) # Pr(CTA+|CDx+) = 搬运权重
tc <- true_concordance(TRUTH$assays$CTA, TRUTH$assays$CDx)
ft_show(data.frame(
`量` = c("PPA_{CTA,CDx}", "NPA_{CTA,CDx}", "p = Pr(CTA+|CDx+)"),
`估计` = round(c(PPA_hat, NPA_hat, p_hat), 4),
`真值` = round(c(tc[["PPA"]], tc[["NPA"]], true_p_cta_given_cdx()), 4),
check.names = FALSE), title = "步骤 1:一致性与搬运权重 p —— 估计 vs 真值")量 | 估计 | 真值 |
|---|---|---|
PPACTA,CDx | 0.7558 | 0.7540 |
NPACTA,CDx | 0.9005 | 0.8993 |
p = Pr(CTA+|CDx+) | 0.7855 | 0.7829 |
步骤 2 · 疗效 → 观察格 \(\delta_{11}\)。 富集试验只入组 CTA+,在观察得到的 (CTA+ & CDx+) 格里直接估 \(\delta_{11}\)(治疗 − 对照):
trial <- generate_population(2e5, with_endpoint = TRUE) # 富集试验(分析只用 CTA+)
d11_hat <- delta_hat(subset(trial, CTA==1 & CDx==1)) # 观察格疗效
big2 <- generate_population(3e6, with_endpoint = TRUE) # god 视角:算真值用
true_d11 <- delta_hat(subset(big2, CTA==1 & CDx==1))
ft_show(data.frame(`δ_{11} 估计` = round(d11_hat,4), `δ_{11} 真值` = round(true_d11,4),
check.names = FALSE), title = "步骤 2:观察格 (CTA+ & CDx+) 疗效 δ_{11} —— 估计 vs 真值")δ11 估计 | δ11 真值 |
|---|---|
0.6103 | 0.5952 |
步骤 3 · 缺失格敏感性 → \(\delta_{CDx}\)。 (CTA− & CDx+) 那格没人、\(\delta_{01}\) 测不到,用敏感性参数 \(c=\delta_{01}/\delta_{11}\) 顶上,于是 \(\delta_{CDx}=(p+(1-p)c)\,\delta_{11}\)。把步骤 1、2 估到的 \(p\)、\(\delta_{11}\) 代进去、扫 \(c\);再用 god 视角的真 \(c\) 验证能否还原真 \(\delta_{CDx}\):
true_d01 <- delta_hat(subset(big2, CTA==0 & CDx==1)) # god 视角:缺失格真疗效
true_dCDx <- true_delta_in_assay_pos(TRUTH$assays$CDx); c_true <- true_d01/true_d11
c_seq <- seq(0, 1, 0.02)
plot(c_seq, (p_hat+(1-p_hat)*c_seq)*d11_hat, type = "l", lwd = 2, col = "#2E86AB",
xlab = expression(paste("敏感性 c = ", delta["01"], " / ", delta["11"])),
ylab = expression(paste("bridged ", delta[CDx])),
main = expression(paste("步骤 3:代入真 c 还原真 ", delta[CDx])))
abline(h = true_dCDx, lty = 2, col = "#C73E1D"); abline(v = c_true, lty = 3, col = "gray40")
points(c_true, (p_hat+(1-p_hat)*c_true)*d11_hat, pch = 19, col = "#C73E1D")
legend("topleft", legend = expression(paste("bridged(c)"), paste("真 ", delta[CDx]), paste("真 c")),
col = c("#2E86AB","#C73E1D","gray40"), lty = c(1,2,3), lwd = 2, bty = "n", cex = 0.8)ft_show(data.frame(`δ_{01} 真值` = round(true_d01,4), `c 真值` = round(c_true,4),
`δ_{CDx}(代入真 c)` = round((p_hat+(1-p_hat)*c_true)*d11_hat,4), `δ_{CDx} 真值` = round(true_dCDx,4),
check.names = FALSE), title = "步骤 3:代入真 c 还原真 δ_{CDx}")δ01 真值 | c 真值 | δCDx(代入真 c) | δCDx 真值 |
|---|---|---|---|
0.3935 | 0.6611 | 0.566 | 0.5508 |
✓ 三步都对上:步骤 1 的 PPA/NPA/\(p\)、步骤 2 的 \(\delta_{11}\) 都贴合真值(数据能给);步骤 3 把这两步估到的 \(p\)、\(\delta_{11}\) 一代,红点正落在真值线上。唯一数据给不出的,是缺失格的 \(\delta_{01}\)(即 \(c\))——它是全流程唯一不可识别、只能靠假设的量。\(\delta_{CDx}\) 一般低于富集试验直接估计的疗效,收缩程度取决于 \(p\) 和对 \(c\) 的假设;实践中 \(c\) 取小值做保守估计。
前三步处理的缺失是一整格人群缺席。这一节是另一种缺失:人在试验里,CDx 那一格却是空的——样本没采到、采到了量不足、DNA 降解让判读成 INVALID、或者根本没送测。这就是 §4.1 列的第 4 步:独立于第 3 步、可选、触发式。临床 bridging 的方法学综述也把 sample retainment / missing data 单列为核心难点 (Song et al. 2024)。
本节的疗效改用单臂应答率(记 \(R_1\) = 观察格 CTA+ & CDx+ 里的应答率,例如 CR/CRh 率),因为真实的 CDx 敏感性分析多半长这样。搬运恒等式一个字都不用改(§4.2 已说明率是均值型、可线性混合):
\[R_{CDx}=p\,R_1+(1-p)\,R_2,\qquad R_2=c\,R_1 \quad\Longrightarrow\quad R_{CDx}=\bigl(p+(1-p)c\bigr)R_1\]
par(family = "cjk", mfrow = c(1, 2), mar = c(0.6, 0.6, 3.0, 0.6))
lb <- "#AED6F1"; lbb <- "#7FB3D5"; org <- "#E8871E"
## 左:结构缺失——整格没人
plot(NA, xlim = c(0, 10), ylim = c(0, 8), axes = FALSE, xlab = "", ylab = "",
main = "结构缺失(第 3 步):整格没人", cex.main = 1.15)
text(3.9, 7.0, "CTA+", col = "gray25", font = 2, cex = 0.95) # 列头
text(7.3, 7.0, "CTA−", col = "gray25", font = 2, cex = 0.95)
text(1.95, 5.5, "CDx+", col = "gray25", font = 2, cex = 0.95, adj = 1) # 行头
text(1.95, 3.3, "CDx−", col = "gray25", font = 2, cex = 0.95, adj = 1)
rect(2.2, 4.4, 5.6, 6.6, col = "#2E86AB", border = "white", lwd = 2)
rect(5.6, 4.4, 9.0, 6.6, col = lb, border = lbb, lwd = 2)
rect(2.2, 2.2, 5.6, 4.4, col = "gray62", border = "white", lwd = 2)
rect(5.6, 2.2, 9.0, 4.4, col = "gray80", border = "white", lwd = 2)
segments(5.8, 4.6, 8.8, 6.4, col = adjustcolor("#C73E1D", 0.5), lwd = 3) # 叉先画,文字压上面
segments(5.8, 6.4, 8.8, 4.6, col = adjustcolor("#C73E1D", 0.5), lwd = 3)
text(3.9, 5.5, "a 观察到", col = "white", font = 2, cex = 0.95)
rect(6.25, 5.22, 8.35, 5.78, col = adjustcolor("white", 0.88), border = NA) # 叉上垫白底
text(7.3, 5.5, "b 没入组", col = "#C73E1D", font = 2, cex = 0.95)
text(3.9, 3.3, "c", col = "white", font = 2, cex = 0.95)
text(7.3, 3.3, "d", col = "gray25", font = 2, cex = 0.95)
text(5.6, 2.0, "c、d 是 CDx− 行,不是搬运目标", col = "gray45", cex = 0.78, font = 3)
text(5.0, 1.4, "b 格的人一行数据都没有:没有协变量、没有结局。", col = "gray20", cex = 0.9, font = 2)
text(5.0, 0.6, "→ 疗效不可识别,只能用敏感性参数 c 顶上", col = "#C73E1D", cex = 0.9, font = 2)
## 右:测量缺失——行都在,只缺一格
plot(NA, xlim = c(0, 10), ylim = c(0, 8), axes = FALSE, xlab = "", ylab = "",
main = "测量缺失(第 4 步):只缺一格", cex.main = 1.15)
hdr <- c("协变量 X", "CTA", "CDx", "结局")
xs <- c(1.0, 3.6, 5.4, 7.2); xe <- c(3.6, 5.4, 7.2, 9.2)
for (j in seq_along(hdr)) {
rect(xs[j], 6.4, xe[j], 7.2, col = "gray35", border = "white", lwd = 1.6)
text((xs[j] + xe[j])/2, 6.8, hdr[j], col = "white", font = 2, cex = 0.82)
}
cdx_val <- c("+", "?", "−", "+", "?", "+")
for (i in 1:6) {
yb <- 6.4 - i * 0.72; yt <- yb + 0.72
gone <- cdx_val[i] == "?"
for (j in seq_along(hdr)) {
fill <- if (j == 3 && gone) adjustcolor(org, 0.28) else "white"
rect(xs[j], yb, xe[j], yt, col = fill, border = "gray70", lwd = 1.2)
}
text((xs[1] + xe[1])/2, (yb + yt)/2, "年龄 / 分层 / 疾病状态", col = "gray30", cex = 0.72)
text((xs[2] + xe[2])/2, (yb + yt)/2, "+", col = "#2E86AB", font = 2, cex = 0.95)
text((xs[3] + xe[3])/2, (yb + yt)/2, cdx_val[i],
col = if (gone) org else "#2E86AB", font = 2, cex = if (gone) 1.15 else 0.95)
text((xs[4] + xe[4])/2, (yb + yt)/2, if (i %% 2) "应答" else "未应答", col = "gray30", cex = 0.72)
}
text(5.0, 1.4, "同一行的其它列都在:可以用它们学到的关系", col = "gray20", cex = 0.9, font = 2)
text(5.0, 0.6, "→ 把这一格的条件分布补出来(多重插补)", col = org, cex = 0.9, font = 2)分水岭就是”有没有可借的信息”。 结构缺失那格的人从没入组,数据里一行都没有,\(\delta_{01}\)(率版本的 \(R_2\))不可识别,只能靠 \(c\)。测量缺失的人整行都在——人口学、临床分层、CTA 判读、结局全有,只缺 CDx 一格;于是可以用”有判读的人”身上学到的关系,把这一格补上。
插补补的是分布,不是真值。 谁也不知道那个人到底是 CDx+ 还是 CDx−;能做的是按条件概率抽一个,抽很多次(多重插补),看结论在这些抽法之间晃多少。晃动幅度本身就是答案的一部分。这正是多重插补相对于单值填补的关键:让缺失值不确定性进入最终推断 (Rubin 1987; Little and Rubin 2019)。
实务顺序是:按队列量”缺失或无效”的比例 → 超过阈值(常取 10%)才启动多重插补,并在观察值 + 插补值的合并数据上重估一致性与疗效;不超阈值就用有判读的人直接分析。阈值不是统计定理,是工作量与风险的折衷;把它写进 SAP 的作用是在看到结果之前就定好做不做,避免”结果不好看才补一个敏感性分析”。
记 \(R\) 为缺失指示(\(R=1\) 表示这个人的 CDx 判读缺失),\(Z\) 为待缺失的 CDx 判读,\(X\) 为观测到的协变量:
机制 | 定义 | 读法 | 丢弃缺失者 | MAR 插补能修吗 |
|---|---|---|---|---|
MCAR | Pr(R=1 | Z, X) = Pr(R=1) | 缺失与任何东西无关 | 无偏 | 不必修 |
MAR | Pr(R=1 | Z, X) = Pr(R=1 | X) | 缺不缺可以看 X 决定;X 给定之后,不再多看一眼那个要缺的判读 | 一般有偏(X 的分布被筛过) | 能修(前提:X 进了插补模型) |
MNAR | Pr(R=1 | Z, X) 仍依赖 Z | 缺不缺还取决于判读本身(如 CDx− 的样本更容易 invalid) | 有偏 | 修不掉 |
MAR 是假设,不是可检验的事实。 缺失值那部分不出现在观测数据的似然里,所以一个 MAR 模型和某个 MNAR 模型可以对观测数据给出完全相同的拟合——数据分不开它们。这样看,第 4 步和第 3 步同属”靠假设”,区别在假设的位置和成本:\(c\) 是一个完全没有数据支撑的旋钮,MAR 则借到了 \(X\) 的信息,还能把插补带来的变异量出来。因此主分析必须陈述可识别假设,并用敏感性分析探索不可检验部分 (National Research Council 2010; Little and Rubin 2019)。
机制往哪偏也可以推:样本量不足、肿瘤含量低导致的判读无效,很可能与 marker 状态本身相关——那就落在 MNAR 一档。本节最后会把这种情形的后果量出来。
实务做法:人口学 + 临床基线(年龄、性别、种族、风险分层、疾病状态)。两个刻意的选择:
协变量本身若有零星缺失,实务里常直接填一个”未报告”类别(分类变量)或中位数(连续变量),不再单独建一层插补模型——“未报告”在基线表里本来就是一个真实类别。
缺失发生在入组的 CTA+ 人群;一致性表里的 CTA− 一列通常来自外部阴性材料(专为估 NPA 收集、判读齐全)。这些人要留在模型框里,因为 NPA 得靠它们;但不给它们插补——给一列本来齐全的判读插补,等于直接改写 NPA。真有 CTA− 个体缺 CDx 判读,做法是剔除并把剔除数报出来。
mice 的 logreg(logistic regression
插补)核心只有三步:
\[\text{① 拟合(只用有判读的人):}\quad \operatorname{logit}\Pr(\text{CDx}{=}1\mid X)=X^{\top}\hat\beta\]
\[\text{② 抽系数:}\quad \beta^{*}=\hat\beta+L^{\top}z,\qquad L^{\top}L=\widehat{\operatorname{Cov}}(\hat\beta),\quad z\sim N(0,I)\]
\[\text{③ 抽判读:}\quad \pi_i=\operatorname{expit}\!\bigl(x_i^{\top}\beta^{*}\bigr),\qquad \text{CDx}_i^{\text{imp}}=\mathbf{1}\{u_i\le\pi_i\},\quad u_i\sim U(0,1)\]
两次抽样各管一件不确定性:② 管参数不确定性——完整案例就那么多人,回归系数本身是估出来的;③ 管个体残差不确定性——同样 \(\pi=0.7\) 的两个人,一个可以是阳、一个可以是阴。少任何一步都会把不确定性做小:只做 ③(把 \(\hat\beta\) 当真值,文献里叫 improper imputation)低估插补方差;两步都不做、直接填 \(\pi>0.5\) 的那一类,则所有插补数据集完全相同,多重插补退化成单值填补,跨插补方差是 0。三种填法的差别下面会量。
每个完整数据集都要把第 1–3 步重跑一遍。插补的判读不是只改一个计数,它同时进三处:
三处的偏倚方向可以互不相同,净效应要算——这也是为什么不能只看 PPA 一处就下结论。
\[R_{CDx}(c)=p\,R_1+(1-p)\,c\,R_1 \quad\xrightarrow{\ c=1\ }\quad p\,R_1+(1-p)\,R_1=R_1\]
\(p\) 整个消掉:\(c=1\) 那一行就是双阳应答率本身,与主分析对得上,可当交叉校验读。反过来说,若敏感性分析只做 \(c=1\) 一档,一致性那一整套(PPA、NPA、\(p\) 以及 \(p\) 的方差)算了也会被代数消掉——不是算错,是这一档的必然结果。
Rubin 规则下 \(m=20\sim50\) 通常已足够(相对效率 \((1+\gamma/m)^{-1}\),\(\gamma\) 为缺失信息比例;具体所需次数仍取决于 FMI 与目标精度)(Rubin 1987)。而实务表格常用五数概括报这 \(m\) 个估计(Min/Q1/中位/均值/Q3/Max),那就得把 \(m\) 提到几百上千:四分位数很快稳住,而 Min、Max 是极值统计量,\(m\) 越大越往外走、不收敛。所以 \(m=1000\) 服务的是”极值那两列别乱跳”;即便如此,那两列的读法也只能是”这 1000 次里最坏/最好的一次”,不是界限。下面把这条漂移画出来。
在已知真相的世界里,“如果没有缺失”这个对照是能拿到的——真实数据里永远拿不到。整节的验证策略就是拿它当基准:全数据估计 vs 丢弃缺失者 vs 两种插补,看谁回得去。
先给这一节的 DGP 加两样东西:与真 marker
相关的基线协变量(插补模型要用),和一个二值应答(单臂率)。assay
参数、患病率、疗效都沿用 TRUTH,不新设。
gen_mi <- function(n, truth = TRUTH) {
G <- rbinom(n, 1, truth$prev_G)
ca <- function(oc) ifelse(G == 1, rbinom(n, 1, oc["sens"]), rbinom(n, 1, 1 - oc["spec"]))
ep <- truth$endpoint # 单臂:只有治疗组
Y <- rnorm(n, ifelse(G == 1, ep$mu_G1_T0 + ep$delta_pos, ep$mu_G0_T0 + ep$delta_neg), ep$sd)
data.frame(G = G, CTA = ca(truth$assays$CTA), CDx = ca(truth$assays$CDx),
AGE = rnorm(n, 60 + 8 * G, 7), # 协变量①:真 marker+ 年龄偏大
RISK = rbinom(n, 1, 0.25 + 0.35 * G), # 协变量②:真 marker+ 更常落在高危层
RESP = as.integer(Y > 1.0)) # 二值应答(CR/CRh 式)
}
# 一次试验 = 富集入组的 CTA+ n1 人(判读 + 结局齐全)+ 外部阴性材料 n0 人(只为估 NPA)
make_trial <- function(n1 = 150, n0 = 100) {
pool <- gen_mi(4e4)
neg <- pool[pool$CTA == 0, ][seq_len(n0), ]; neg$RESP <- NA_integer_ # 阴性材料无结局
rbind(pool[pool$CTA == 1, ][seq_len(n1), ], neg)
}
PREV_CTA <- unname(assay_prev(TRUTH$assays$CTA)) # 意向使用人群的 Pr(CTA+),Bayes 权重用把第 1–3 步压成一个函数——这就是每份插补数据集要重跑的全部:
bridged_rate <- function(cdx, cta, resp, c_values, prev = PREV_CTA, idx = seq_along(cdx)) {
cdx <- cdx[idx]; cta <- cta[idx]; resp <- resp[idx]
A <- sum(cdx == 1 & cta == 1); B <- sum(cdx == 1 & cta == 0) # 2×2 四格
C <- sum(cdx == 0 & cta == 1); D <- sum(cdx == 0 & cta == 0)
ppa <- A/(A + C); npa <- D/(B + D) # 步骤 1:一致性
R1 <- if (A > 0) mean(resp[cdx == 1 & cta == 1]) else NA_real_ # 步骤 2:观察格应答率
p <- (ppa*prev)/(ppa*prev + (1 - npa)*(1 - prev)) # Bayes 权重
c(PPA = ppa, NPA = npa, p = p, R1 = R1, A = A, # 步骤 3:扫 c
setNames((p + (1 - p)*c_values)*R1, paste0("c", c_values)))
}缺失机制两种:MAR
只看协变量(年龄大、高危层更容易缺),MNAR
额外还看判读本身(CDx−
更容易无效)。插补按上面拆的三步写,mode 控制抽几层:
zage <- function(a) (a - 60)/10
miss_mar <- function(d) d$CTA == 1 &
runif(nrow(d)) < plogis(-3.05 + 1.1*zage(d$AGE) + 0.9*d$RISK)
miss_mnar <- function(d) d$CTA == 1 &
runif(nrow(d)) < plogis(-4.3 + 1.1*zage(d$AGE) + 0.9*d$RISK + 3.0*(d$CDx == 0))
impute_once <- function(d, miss, use_cta, mode = "proper") {
f <- if (use_cta) CDx ~ AGE + RISK + CTA else CDx ~ AGE + RISK
fit <- glm(f, data = d[!miss, ], family = binomial) # ① 只用有判读的人
b <- coef(fit)
bst <- if (mode == "proper") # ② 抽系数 β*
b + t(chol(summary(fit)$cov.unscaled)) %*% rnorm(length(b)) else b
pr <- plogis(as.vector(model.matrix(delete.response(terms(fit)), d[miss, ]) %*% bst))
if (mode == "deterministic") as.integer(pr > 0.5) # 确定性填补:不抽
else as.integer(runif(length(pr)) <= pr) # ③ 抽判读
}
draw_imp <- function(d, miss, use_cta, m, c_values, mode = "proper")
t(vapply(seq_len(m), function(i) {
cdx <- d$CDx; cdx[miss] <- impute_once(d, miss, use_cta, mode)
bridged_rate(cdx, d$CTA, d$RESP, c_values) # 每份完整数据集重跑整条链
}, numeric(5 + length(c_values))))set.seed(7)
d_one <- make_trial(); miss_one <- miss_mar(d_one)
M <- 1000; C_V <- c(1, 0.5, 0)
mi_pro <- draw_imp(d_one, miss_one, TRUE, M, C_V, "proper")
mi_imp <- draw_imp(d_one, miss_one, TRUE, M, C_V, "improper")
mi_det <- draw_imp(d_one, miss_one, TRUE, 20, C_V, "deterministic")
par(family = "cjk", mfrow = c(1, 3), mar = c(4.2, 4.2, 3.4, 0.8))
## (a) 抽系数:β* 曲线族
fit1 <- glm(CDx ~ AGE + RISK + CTA, data = d_one[!miss_one, ], family = binomial)
ag <- seq(38, 88, length.out = 120)
Xg <- cbind(1, ag, 0, 1) # RISK=0、CTA=1 的切片
V <- t(chol(summary(fit1)$cov.unscaled))
plot(NA, xlim = range(ag), ylim = c(0, 1), main = "① 拟合 + ② 抽系数 β*",
xlab = "年龄(RISK=0、CTA+ 切片)", ylab = "Pr(CDx+)", cex.main = 1.1, cex.lab = 1.05)
for (k in 1:40)
lines(ag, plogis(as.vector(Xg %*% (coef(fit1) + V %*% rnorm(4)))),
col = adjustcolor("gray55", 0.45), lwd = 1)
lines(ag, plogis(as.vector(Xg %*% coef(fit1))), col = "#2E86AB", lwd = 3)
obs <- d_one[!miss_one & d_one$CTA == 1, ]
points(obs$AGE, jitter(obs$CDx, amount = 0.04), pch = 16, cex = 0.5,
col = adjustcolor("#2E86AB", 0.5))
rug(d_one$AGE[miss_one], col = "#E8871E", lwd = 2, side = 1)
legend("bottomright", c("完整案例拟合", "40 条 β* 抽样", "待插补者年龄"),
col = c("#2E86AB", "gray55", "#E8871E"), lwd = c(3, 1, 2), cex = 0.85,
bg = adjustcolor("white", 0.88), box.col = NA)
## (b) 三种填法的跨插补散布
xr <- range(c(mi_pro[, "c0.5"], mi_imp[, "c0.5"], mi_det[, "c0.5"]))
hist(mi_pro[, "c0.5"], breaks = 30, col = adjustcolor("#2E86AB", 0.55), border = "white",
freq = FALSE, xlim = xr, main = "三种填法:1000 次插补的桥接率",
xlab = expression(paste("桥接应答率 ", R[CDx], " (c = 0.5)")),
ylab = "密度", cex.main = 1.1, cex.lab = 1.05)
lines(density(mi_imp[, "c0.5"]), col = "forestgreen", lwd = 2.4)
abline(v = mi_det[1, "c0.5"], col = "#E8871E", lwd = 3)
legend("topright", c(sprintf("proper(抽 β*+判读)SD %.4f", sd(mi_pro[, "c0.5"])),
sprintf("improper(只抽判读)SD %.4f", sd(mi_imp[, "c0.5"])),
"确定性填补 SD = 0"),
col = c("#2E86AB", "forestgreen", "#E8871E"), lwd = c(6, 2.4, 3), cex = 0.8,
bg = adjustcolor("white", 0.88), box.col = NA)
## (c) 五数概括随 m 漂移
ms <- unique(round(10^seq(log10(10), log10(M), length.out = 40)))
v <- mi_pro[, "c0.5"]
fv <- t(vapply(ms, function(mm) c(min(v[1:mm]), quantile(v[1:mm], c(.25, .5, .75), names = FALSE),
max(v[1:mm])), numeric(5)))
matplot(ms, fv, type = "l", log = "x", lty = c(2, 1, 1, 1, 2), lwd = c(2, 1.8, 2.6, 1.8, 2),
col = c("#C73E1D", "gray45", "#2E86AB", "gray45", "#C73E1D"),
ylim = range(fv) + c(-0.001, 0.008), # 顶部留出 legend 的位置
main = "五数概括随 m 的走势", xlab = "插补次数 m(对数轴)",
ylab = expression(paste("桥接应答率 ", R[CDx], " (c = 0.5)")),
cex.main = 1.1, cex.lab = 1.05)
legend("topleft", c("Min / Max(一直往外爬)", "Q1 / Q3", "中位数"),
col = c("#C73E1D", "gray45", "#2E86AB"), lty = c(2, 1, 1), lwd = c(2, 1.8, 2.6),
cex = 0.8, bg = adjustcolor("white", 0.9), box.col = NA)左图是”抽系数”这一步的样子:完整案例给一条拟合曲线(蓝),\(\beta^{*}\) 每抽一次就换一条(灰)——灰带的宽度就是参数不确定性,待插补者(橙色刻度)的 \(\pi_i\) 每轮都落在不同曲线上。
中图是三种填法的后果:proper 的散布最宽,improper 窄一点(这份数据上方差小 12%,缺失者更多、协变量更多时差距会更大),确定性填补是一根竖线——不确定性被抹成 0,多重插补名存实亡。
右图是 \(m\) 的作用:中位数和四分位数在 \(m\approx 50\) 之后就基本不动了,而 Min/Max 两条虚线一直在往外爬。要它们稳,就得把 \(m\) 提到上千;即便如此,稳的也只是”抖动小一些”,不是”到界了”。
单次试验的差异会被抽样噪声吃掉(\(n=150\) 时 PPA 的抽样标准差约 0.035,比要看的偏倚还大),所以重复 150 次试验、每次都和同一次试验的全数据估计配对相减,看平均偏倚:
mi_rep <- function(mech, R = 150, m = 20, cf = 0.5) {
keys <- c("PPA", "p", "R1", paste0("c", cf))
t(vapply(seq_len(R), function(i) {
d <- make_trial(); mi <- mech(d)
full <- bridged_rate(d$CDx, d$CTA, d$RESP, cf)[keys] # 基准:没有缺失
cc <- bridged_rate(d$CDx[!mi], d$CTA[!mi], d$RESP[!mi], cf)[keys] # 丢弃缺失者
mx <- colMeans(draw_imp(d, mi, FALSE, m, cf))[keys] # 插补:只用协变量
mc <- colMeans(draw_imp(d, mi, TRUE, m, cf))[keys] # 插补:+CTA 判读
c(rate = mean(mi[d$CTA == 1]), full = full, cc = cc - full,
mx = mx - full, mc = mc - full)
}, numeric(1 + 4*4)))
}
set.seed(11)
rep_mar <- mi_rep(miss_mar)
rep_mnar <- mi_rep(miss_mnar)
bias_tbl <- function(res, cf = "c0.5") {
g <- function(k) sapply(c("PPA", "p", "R1", cf), function(q) mean(res[, paste0(k, ".", q)]))
se <- function(k) sd(res[, paste0(k, ".", cf)])/sqrt(nrow(res))
data.frame(
`做法` = c("丢弃缺失者(complete case)", "插补:仅协变量", "插补:协变量 + CTA 判读"),
`ΔPPA` = sprintf("%+.4f", c(g("cc")[1], g("mx")[1], g("mc")[1])),
`Δp` = sprintf("%+.4f", c(g("cc")[2], g("mx")[2], g("mc")[2])),
`ΔR_{1}` = sprintf("%+.4f", c(g("cc")[3], g("mx")[3], g("mc")[3])),
`Δ桥接率 (c=0.5)` = sprintf("%+.5f", c(g("cc")[4], g("mx")[4], g("mc")[4])),
`MC 标准误` = sprintf("%.5f", c(se("cc"), se("mx"), se("mc"))),
check.names = FALSE)
}
ft_show(bias_tbl(rep_mar), title = "MAR 机制下的配对偏倚(150 次重复试验,每次 m = 20)",
footers = sprintf(paste("基准 = 同一次试验的全数据估计:PPA %.4f、桥接率 %.4f;",
"平均缺失率 %.1f%%(入组 CTA+ 中)。Δ = 该做法 − 全数据。"),
mean(rep_mar[, "full.PPA"]), mean(rep_mar[, "full.c0.5"]),
100*mean(rep_mar[, "rate"])))做法 | ΔPPA | Δp | ΔR1 | Δ桥接率 (c=0.5) | MC 标准误 |
|---|---|---|---|---|---|
丢弃缺失者(complete case) | -0.0204 | -0.0047 | -0.0032 | -0.00364 | 0.00149 |
插补:仅协变量 | -0.0208 | -0.0047 | -0.0020 | -0.00261 | 0.00056 |
插补:协变量 + CTA 判读 | +0.0009 | +0.0000 | -0.0024 | -0.00211 | 0.00048 |
基准 = 同一次试验的全数据估计:PPA 0.7523、桥接率 0.3030; 平均缺失率 16.6%(入组 CTA+ 中)。Δ = 该做法 − 全数据。 | |||||
三行读下来:
最值得留意的是这四列之间的传递。 PPA 偏 0.020,经 Bayes 换成权重 \(p\) 只剩 0.005,再乘 \((1-c)R_1\) 进桥接率——PPA 这一路对最终偏倚的贡献很小。写成一行链式导数就清楚了:
\[\frac{\partial R_{CDx}}{\partial\,\text{PPA}}=\underbrace{(1-c)\,R_1}_{\text{结局那一路}}\times\underbrace{\frac{\partial p}{\partial\,\text{PPA}}}_{\text{Bayes 那一路}},\qquad \frac{\partial p}{\partial\,\text{PPA}}=\frac{prev\,(1-NPA)(1-prev)}{\bigl[\text{PPA}\cdot prev+(1-NPA)(1-prev)\bigr]^{2}}\]
b0 <- bridged_rate(d_one$CDx, d_one$CTA, d_one$RESP, 0.5) # 这份数据的各量
dp_dppa <- PREV_CTA*(1 - b0[["NPA"]])*(1 - PREV_CTA) /
(b0[["PPA"]]*PREV_CTA + (1 - b0[["NPA"]])*(1 - PREV_CTA))^2
slope <- (1 - 0.5)*b0[["R1"]] * dp_dppa
cat(sprintf("dp/dPPA = %.3f ; (1-c)*R1 = %.3f ; 合起来 dR_CDx/dPPA = %.4f\n",
dp_dppa, (1 - 0.5)*b0[["R1"]], slope))## dp/dPPA = 0.203 ; (1-c)*R1 = 0.140 ; 合起来 dR_CDx/dPPA = 0.0284
w_R1 <- b0[["p"]] + (1 - b0[["p"]])*0.5 # R1 那一路的系数
cat(sprintf("→ PPA 偏 %.3f 只搬来 %.4f;R1 偏 %.4f 搬来 %.4f;合计 %.4f,实测 %.4f\n",
abs(mean(rep_mar[, "cc.PPA"])), abs(mean(rep_mar[, "cc.PPA"]))*slope,
abs(mean(rep_mar[, "cc.R1"])), abs(mean(rep_mar[, "cc.R1"]))*w_R1,
abs(mean(rep_mar[, "cc.PPA"]))*slope + abs(mean(rep_mar[, "cc.R1"]))*w_R1,
abs(mean(rep_mar[, "cc.c0.5"]))))## → PPA 偏 0.020 只搬来 0.0006;R1 偏 0.0032 搬来 0.0028;合计 0.0034,实测 0.0036
两级衰减都在这条式子里:\(p\) 是 PPA 的一个平缓函数(斜率 0.20),\((1-c)R_1\) 又把它再压一次(0.14),合起来把 PPA 的扰动缩到约 1/35。这条链天生对一致性的偏倚不敏感——所以这类敏感性分析常常得出”缺失带来的偏倚可忽略”,不是插补神奇,而是估计量的结构把这一路的扰动衰减掉了。
另一路没有这层保护。 \(R_1\) 的系数是 \(p+(1-p)c\approx0.89\)——\(R_1\) 偏多少,桥接率几乎照搬。上面那行输出把实测的桥接率偏倚 0.0036 拆开了:PPA 那一路只占 0.0006,其余来自 \(R_1\)。看一致性表判断”缺失影响大不大”会看错重点,真正要盯的是缺失有没有动 \(R_1\)(即有没有把系统性偏应答/偏不应答的人补进双阳格)。
那 \(m\) 个估计怎么变成一个结论?两种口径,回答的问题不同:
set.seed(13)
m_r <- 200; B_r <- 200; n_one <- nrow(d_one)
th <- numeric(m_r); U <- numeric(m_r)
for (i in seq_len(m_r)) {
cdx <- d_one$CDx; cdx[miss_one] <- impute_once(d_one, miss_one, TRUE, "proper")
th[i] <- bridged_rate(cdx, d_one$CTA, d_one$RESP, 0.5)[["c0.5"]] # 第 i 份的点估计
U[i] <- var(vapply(seq_len(B_r), function(b) # 第 i 份的组内方差
bridged_rate(cdx, d_one$CTA, d_one$RESP, 0.5,
idx = sample.int(n_one, n_one, TRUE))[["c0.5"]], numeric(1)), na.rm = TRUE)
}
Wbar <- mean(U); Bv <- var(th); Tot <- Wbar + (1 + 1/m_r)*Bv # Rubin 规则
ft_show(data.frame(
`量` = c("θ̄(m 个估计的均值)", "W̄(组内方差:抽样不确定性)",
"B(组间方差:插补不确定性)", "T = W̄ + (1+1/m)·B(总方差)",
"缺失信息比例 FMI = (1+1/m)B / T", "Rubin 95% 区间", "五数概括的 Min–Max 跨度"),
`值` = c(sprintf("%.4f", mean(th)), sprintf("%.3e", Wbar), sprintf("%.3e", Bv),
sprintf("%.3e", Tot), sprintf("%.1f%%", 100*(1 + 1/m_r)*Bv/Tot),
sprintf("[%.4f, %.4f](宽 %.4f)", mean(th) - 1.96*sqrt(Tot),
mean(th) + 1.96*sqrt(Tot), 2*1.96*sqrt(Tot)),
sprintf("[%.4f, %.4f](宽 %.4f)", min(th), max(th), max(th) - min(th))),
check.names = FALSE),
title = "同一份数据的两种汇总:Rubin 规则 vs 五数概括",
footers = sprintf("m = %d 次插补;每份插补数据集用 %d 次自助重抽算它自己的组内方差。", m_r, B_r))量 | 值 |
|---|---|
θ̄(m 个估计的均值) | 0.2450 |
W̄(组内方差:抽样不确定性) | 1.336e-03 |
B(组间方差:插补不确定性) | 3.462e-05 |
T = W̄ + (1+1/m)·B(总方差) | 1.371e-03 |
缺失信息比例 FMI = (1+1/m)B / T | 2.5% |
Rubin 95% 区间 | [0.1724, 0.3176](宽 0.1451) |
五数概括的 Min–Max 跨度 | [0.2301, 0.2573](宽 0.0272) |
m = 200 次插补;每份插补数据集用 200 次自助重抽算它自己的组内方差。 | |
两者答的不是同一个问题。 \(\bar W\) 是”这 150 个人本来就有的抽样不确定性”,\(B\) 是”插补法引入的额外不确定性”;这里 \(B\) 只占总方差的 2.5%。所以:
把缺失改成还依赖判读本身(CDx− 更容易无效),同一套方法再跑一遍:
做法 | ΔPPA | Δp | ΔR1 | Δ桥接率 (c=0.5) | MC 标准误 |
|---|---|---|---|---|---|
丢弃缺失者(complete case) | +0.0542 | +0.0113 | +0.0014 | +0.00319 | 0.00089 |
插补:仅协变量 | +0.0343 | +0.0074 | -0.0056 | -0.00380 | 0.00061 |
插补:协变量 + CTA 判读 | +0.0593 | +0.0123 | -0.0088 | -0.00585 | 0.00070 |
缺失概率额外依赖 CDx 判读本身。基准 = 全数据估计:PPA 0.7537、 桥接率 0.3065;平均缺失率 13.4%。 | |||||
PPA 这次偏高(缺的多是 CDx−,剩下的人阳性比例虚高),而且插补没有修掉它——正统做法(协变量 + CTA)的偏倚 +0.0593 和丢弃缺失者 +0.0542 一个量级。原因直白:插补模型是从观测到的那部分学关系的,而 MNAR 下这部分关系本身已经被筛过,学过来再套到缺失者身上,等于把偏倚照搬一遍。
桥接率那一列还出现了符号翻转(PPA 偏正、桥接率偏负):插补进入三处,\(R_1\) 那两处(分母、分子)把偏阴的、应答更少的人补进双阳格,压低了 \(R_1\),方向与 \(p\) 那一路相反。这正是前面说”净效应要算、不能凭一处推断”的实例。
要处理 MNAR,只能再加一个假设——例如给插补的 logit 加一个偏移量 \(\Delta\)、扫它的取值(\(\delta\)-adjustment / pattern-mixture)。那就又回到了第 3 步 \(c\) 的位置上:一个数据说不出话的旋钮。
# c=1 恒等式的自检:p 必须整个消掉,否则表格会与主分析的双阳应答率不一致。
chk <- bridged_rate(d_one$CDx, d_one$CTA, d_one$RESP, c(1, 0.5, 0))
stopifnot(isTRUE(all.equal(chk[["c1"]], chk[["R1"]])), chk[["c0"]] < chk[["c1"]])
cat(sprintf("✓ c=1 时桥接率 %.6f = 双阳应答率 R1 %.6f(p 消掉);c=0 时降到 %.6f\n",
chk[["c1"]], chk[["R1"]], chk[["c0"]]))## ✓ c=1 时桥接率 0.280000 = 双阳应答率 R1 0.280000(p 消掉);c=0 时降到 0.219754
能给:在 MAR 假设下,把”这些人若都有判读、结论会怎样”量出来,并给出结论对缺失的敏感度;顺带得到一个 \(c=1\) 的交叉校验。
不能给:MAR 本身的检验(数据分不开 MAR 与 MNAR);MNAR 下的无偏估计;以及——它替代不了把缺失率降下来(采样流程、复测机制)。第 3 步的 \(c\) 和第 4 步的 MAR 是同一类东西的两个位置:统计能做的是把假设摆到明处、把它的后果算清楚,不是把假设消掉。
bridging 讲完,换 follow-on——同一套骨架、又一种缺失结构。区别:bridging 自己试验里有疗效数据、只是人群错位;follow-on 连疗效试验都不做,药和 CCD 都已上市,新检测 FCD 只想证明”我和已批准的 CCD 判人足够一致”,从而间接继承 CCD 那套疗效证据。所以 follow-on 的主战场从”补疗效缺失格”变成”量一致性”——疗效那层用的正是地基里已讲的稀释事实。用外部 concordance study 支持 follow-on CDx 临床验证的统计框架可参见 Li (2016)。
问题:FCD 和 CCD 判人到底有多一致(follow-on 目标 1)? 要回答它,先得有一把量”两个检测一致不一致”的尺子,并把符号定清楚。
承接 §1.3 的 follow-on 定位:要证 目标 1——FCD 与已批准 CCD 分类足够一致;目标 2——借 CCD 的疗效证据说明 FCD+ 人群疗效可接受(用地基里的疗效混合)。路径分直接(用 FCD 重做试验,或重测原 pivotal 样本)与间接(CCD 已确立疗效 + 外部 FCD-vs-CCD 一致性研究)。本章顺序:先把符号定清楚 → 一致性估计 + NI 检验(目标 1)→ 无参考的 \(p_c\) 重加权 → 样本量 → 3-way 补充。
目标 2 里被”借”的量,正是地基里立过的 \(\theta\)——当时已和 \(\eta\) 一起用模拟验证过三路对齐,这里正式派上用场,没有新符号:\(\theta=PPV_c\,\delta_{\text{pos}}+(1{-}PPV_c)\,\delta_{\text{neg}}\),\(PPV_c=\Pr(G{=}1\mid CCD{=}1)\)——还是地基那两块积木,只是权重换成 CCD 的 PPV。\(\theta\) 是 CCD 已经确立、可以借用的疗效;follow-on 的逻辑就是:若 FCD 与 CCD 判人足够一致(目标 1),则 \(\eta\approx\theta\),于是 FCD+ 人群可继承 CCD 的疗效结论。(记号统一,地基也说过:\(\mu\) 按真 \(G\)、\(\eta\) 按 FCD、\(\theta\) 按 CCD 索引疗效,三者都是同两块积木的混合,只是分组不同。)
先把符号定清楚。comparator CDx (CCD) 测两次(CCD1、CCD2)+ follow-on CDx (FCD),可把”FCD-vs-CCD 一致性”和”CCD 自身重复一致性”做基准比较。
记号约定:每个检测的结果编码成 0/1——“= 1” 表示该检测把样本判为阳性(biomarker+),“= 0” 判为阴性。所以 \(CCD1{=}1\) 是”第一次 CCD 判这个样本阳性”,\(FCD{=}0\) 是”新检测判它阴性”。
PPA / NPA 是什么:两个检测对同一批样本的一致率。PPA(阳性一致率)= 一个检测判阳的样本里、另一个也判阳的比例,写成 \(\Pr(\text{乙}{=}1\mid \text{甲}{=}1)\);NPA(阴性一致率)= 判阴里另一个也判阴的比例 \(\Pr(\text{乙}{=}0\mid \text{甲}{=}0)\)。数越大,两个检测越像。注意它们有方向性:交换条件检测与被比较检测,数值一般会变 (U.S. Food and Drug Administration 2007)。
下标读法:下标两个字母 = “以谁为参照 + 跟谁比”,其中 \(C1\) = CCD1、\(C2\) = CCD2、\(F\) = FCD;第一个字母是参照(条件放在它上面),第二个是被比较的检测。于是:
\[PPA_{C1F}=\Pr(FCD{=}1\mid CCD1{=}1),\quad PPA_{C1C2}=\Pr(CCD2{=}1\mid CCD1{=}1)\] \[NPA_{C1F}=\Pr(FCD{=}0\mid CCD1{=}0),\quad NPA_{C1C2}=\Pr(CCD2{=}0\mid CCD1{=}0)\] \[PPA_{C2F}=\Pr(FCD{=}1\mid CCD2{=}1),\quad PPA_{C2C1}=\Pr(CCD1{=}1\mid CCD2{=}1)\] \[NPA_{C2F}=\Pr(FCD{=}0\mid CCD2{=}0),\quad NPA_{C2C1}=\Pr(CCD1{=}0\mid CCD2{=}0)\]
逻辑:FCD 相对 CCD(\(C1F\)、\(C2F\))的一致性,不应明显差于 CCD 自己两次重复(\(C1C2\)、\(C2C1\))。这就是 NI(非劣效性)式一致性检验的立意——拿”CCD 的重复性”当及格线。
正式写成四个单侧非劣效性假设(PPA、NPA 各两个,分别以 CCD1、CCD2 为参照)。每个 \(\xi\) 是”CCD 自一致 − FCD-对-CCD 一致”的差距,\(\delta_1\)(PPA 侧)、\(\delta_0\)(NPA 侧)是可接受 margin:
\[H_0:\ \xi_{ppa1}=PPA_{C1C2}-PPA_{C1F}\ \ge\ \delta_1 \quad\text{vs}\quad H_a:\ \xi_{ppa1}<\delta_1\] \[H_0:\ \xi_{ppa2}=PPA_{C2C1}-PPA_{C2F}\ \ge\ \delta_1 \quad\text{vs}\quad H_a:\ \xi_{ppa2}<\delta_1\] \[H_0:\ \xi_{npa1}=NPA_{C1C2}-NPA_{C1F}\ \ge\ \delta_0 \quad\text{vs}\quad H_a:\ \xi_{npa1}<\delta_0\] \[H_0:\ \xi_{npa2}=NPA_{C2C1}-NPA_{C2F}\ \ge\ \delta_0 \quad\text{vs}\quad H_a:\ \xi_{npa2}<\delta_0\]
差距落到 margin 之下(拒绝 \(H_0\))= FCD 不比 CCD 自身重复性明显更差。
问题:现实里没有金标准(真 \(G\) 看不到),而且入组还按 CCD1 富集过——这时候怎么估一致性才不被富集带偏? 尤其是 CCD2 一侧的量,直接从富集样本里算会有系统性偏差。
先摆计数记号,再给 CCD1-条件化估计量,最后是富集下要的 \(p_c\) 重加权。
计数表(符号记号):
CCD1+ | CCD1− | |||||
|---|---|---|---|---|---|---|
CCD2+ | CCD2− | Total | CCD2+ | CCD2− | Total | |
FCD+ | a₁ | b₁ | m₁ | a₀ | b₀ | r₁ |
FCD− | c₁ | d₁ | m₀ | c₀ | d₀ | r₀ |
Total | n₁₁ | n₁₀ | n₁ | n₀₁ | n₀₀ | n₀ |
CCD1+ 分层内按 (CCD2, FCD) 记 \(a_1,b_1,c_1,d_1\)(行 = FCD、列 = CCD2),边缘计数:
\[m_1 = a_1 + b_1 \quad(\text{FCD+ 数})\] \[n_{11} = a_1 + c_1 \quad(\text{CCD2+ 数})\] \[n_1 = n_{11} + n_{10} \quad(\text{CCD1+ 总数})\]
CCD1− 分层内同理记 \(a_0,b_0,c_0,d_0\),边缘计数:
\[r_0 = c_0 + d_0 \quad(\text{FCD− 数})\] \[n_{00} = b_0 + d_0 \quad(\text{CCD2− 数}),\qquad n_0 = n_{01} + n_{00} \quad(\text{CCD1− 总数})\]
CCD1-条件化的估计量可直接从计数算,不受分层入组偏差影响,每个一行:
\[\hat{PPA}_{C1F}=m_1/n_1\quad(\text{CCD1+ 里 FCD 也判阳的比例})\] \[\hat{PPA}_{C1C2}=n_{11}/n_1\quad(\text{CCD1+ 里 CCD2 也判阳的比例})\] \[\hat{NPA}_{C1F}=r_0/n_0\quad(\text{CCD1− 里 FCD 也判阴的比例})\] \[\hat{NPA}_{C1C2}=n_{00}/n_0\quad(\text{CCD1− 里 CCD2 也判阴的比例})\]
富集入组(按 CCD1 抽)扭曲样本内 CCD1 分层比例。\(p_c=\Pr(CCD1{=}1)\)(真实人群 prevalence,非样本比例)重加权可还原 CCD2-条件化的四个量。用计数表的记号(\(a,b,c,d\) 见上),四式为:
\[\hat{PPA}_{C2F}=\frac{p_c\,a_1 n_0+(1{-}p_c)a_0 n_1}{p_c(a_1{+}c_1)n_0+(1{-}p_c)(a_0{+}c_0)n_1}\] \[\hat{PPA}_{C2C1}=\frac{p_c(a_1{+}c_1)n_0}{p_c(a_1{+}c_1)n_0+(1{-}p_c)(a_0{+}c_0)n_1}\] \[\hat{NPA}_{C2F}=\frac{(1{-}p_c)d_0 n_1+p_c\,d_1 n_0}{(1{-}p_c)(b_0{+}d_0)n_1+p_c(b_1{+}d_1)n_0}\] \[\hat{NPA}_{C2C1}=\frac{(1{-}p_c)(b_0{+}d_0)n_1}{(1{-}p_c)(b_0{+}d_0)n_1+p_c(b_1{+}d_1)n_0}\]
分子分母同除 \(n_1 n_0\) 即看出:这四式都是用真实 CCD1 prevalence \(p_c=\Pr(CCD1{=}1)\)(来自外部流行病学数据或假设,不是样本里的比例)去重新加权两个 CCD1 分层内的样本比例——把富集扭曲的分层权重换回真实权重。下面用 worked example 把这四式与已知真值对撞,验证它们成立:
pc <- assay_prev(TRUTH$assays$CCD) # p_c = Pr(CCD1=1)
tc_fcd <- true_concordance(TRUTH$assays$CCD, TRUTH$assays$CDx) # FCD vs CCD 真值
tc_c2c1 <- true_concordance(TRUTH$assays$CCD, TRUTH$assays$CCD) # CCD 自重复真值
pool <- generate_population(3e5)
enr <- rbind(pool[pool$CCD1==1,][1:2000,], pool[pool$CCD1==0,][1:2000,]) # 富集 1:1
e1 <- subset(enr, CCD1==1); e0 <- subset(enr, CCD1==0); n1 <- nrow(e1); n0 <- nrow(e0)
a1<-sum(e1$CDx==1&e1$CCD2==1); b1<-sum(e1$CDx==1&e1$CCD2==0); c1<-sum(e1$CDx==0&e1$CCD2==1); d1<-sum(e1$CDx==0&e1$CCD2==0)
a0<-sum(e0$CDx==1&e0$CCD2==1); b0<-sum(e0$CDx==1&e0$CCD2==0); c0<-sum(e0$CDx==0&e0$CCD2==1); d0<-sum(e0$CDx==0&e0$CCD2==0)
rw <- c(
PPA_C2F = (pc*a1*n0+(1-pc)*a0*n1)/(pc*(a1+c1)*n0+(1-pc)*(a0+c0)*n1),
PPA_C2C1 = (pc*(a1+c1)*n0)/(pc*(a1+c1)*n0+(1-pc)*(a0+c0)*n1),
NPA_C2F = ((1-pc)*d0*n1+pc*d1*n0)/((1-pc)*(b0+d0)*n1+pc*(b1+d1)*n0),
NPA_C2C1 = ((1-pc)*(b0+d0)*n1)/((1-pc)*(b0+d0)*n1+pc*(b1+d1)*n0))
naive <- c(
PPA_C2F = with(subset(enr, CCD2==1), mean(CDx==1)),
PPA_C2C1 = with(subset(enr, CCD2==1), mean(CCD1==1)),
NPA_C2F = with(subset(enr, CCD2==0), mean(CDx==0)),
NPA_C2C1 = with(subset(enr, CCD2==0), mean(CCD1==0)))
truth <- c(tc_fcd["PPA"], tc_c2c1["PPA"], tc_fcd["NPA"], tc_c2c1["NPA"])
ft_show(data.frame(
`量` = c("PPA_{C2F}", "PPA_{C2C1}", "NPA_{C2F}", "NPA_{C2C1}"),
`真值` = round(unname(truth),4),
`naive(富集偏差)` = round(unname(naive),4),
`p_{c} 重加权` = round(unname(rw),4),
check.names = FALSE),
title = "p_{c} 重加权(4 个 CCD2 条件化量):naive 偏离、重加权贴回真值")量 | 真值 | naive(富集偏差) | pc 重加权 |
|---|---|---|---|
PPAC2F | 0.8309 | 0.8639 | 0.8050 |
PPAC2C1 | 0.8162 | 0.8953 | 0.8017 |
NPAC2F | 0.9319 | 0.9048 | 0.9306 |
NPAC2C1 | 0.9131 | 0.8284 | 0.9108 |
✓ 四式都成立:naive(直接在富集样本里条件化)因富集偏离真值,\(p_c\) 重加权都贴回真值。CI(置信区间)用 Wilson 或 bootstrap;检验按接受准则取单/双侧。
问题:FCD 到底够不够一致、够到能替代 CCD 吗? 工具齐了,用一份代表性样本把四个一致性量估出来、对撞真值,再跑 NI 检验(判”FCD-对-CCD”不比”CCD 自身重复”差):
conc <- generate_population(1200) # 代表性一致性研究
c1pos <- subset(conc, CCD1 == 1); c1neg <- subset(conc, CCD1 == 0)
est <- c(mean(c1pos$CDx==1), mean(c1pos$CCD2==1),
mean(c1neg$CDx==0), mean(c1neg$CCD2==0))
truth_conc <- c(true_concordance(TRUTH$assays$CCD, TRUTH$assays$CDx)[["PPA"]],
true_concordance(TRUTH$assays$CCD, TRUTH$assays$CCD)[["PPA"]],
true_concordance(TRUTH$assays$CCD, TRUTH$assays$CDx)[["NPA"]],
true_concordance(TRUTH$assays$CCD, TRUTH$assays$CCD)[["NPA"]])
ft_show(data.frame(
`一致性量` = c("PPA_{C1F}", "PPA_{C1C2}", "NPA_{C1F}", "NPA_{C1C2}"),
`估计` = round(est, 4), `真值` = round(truth_conc, 4),
check.names = FALSE),
title = "一致性估计 vs 真值(CCD1 条件化)")一致性量 | 估计 | 真值 |
|---|---|---|
PPAC1F | 0.8197 | 0.8309 |
PPAC1C2 | 0.8388 | 0.8162 |
NPAC1F | 0.9329 | 0.9319 |
NPAC1C2 | 0.9209 | 0.9131 |
四个量的估计都逼近真值。按 §NI 假设框架跑检验,以 \(\xi_{ppa1}\) 为代表(其余三个同套路,bootstrap 上界 \(<\delta_1\) 则拒绝 \(H_0\)):
xi_boot <- boot_diff_paired(c1pos$CCD2==1, c1pos$CDx==1); delta1 <- 0.10
ft_show(round(c(xi_point = mean(c1pos$CCD2==1) - mean(c1pos$CDx==1),
ci_upper_95 = unname(quantile(xi_boot, 0.95)), margin = delta1,
reject_H0 = unname(quantile(xi_boot, 0.95) < delta1)), 4),
title = "单侧 NI 检验(PPA)")量 | 值 |
|---|---|
xi_point | 0.0191 |
ci_upper_95 | 0.0492 |
margin | 0.1000 |
reject_H0 | 1.0000 |
✓ 估计逼近真值;FCD(最好的 assay)对 CCD 正确拒绝 \(H_0\)。
问题:这个一致性研究要招多少人,才有足够把握(power)断定 FCD 合格? 意向使用人群随机抽样可公式推导,但实际是非随机抽样(无闭式),用模拟:
par(family = "cjk", mar = c(0.5, 0.5, 3, 0.5))
plot(NA, xlim = c(0, 10), ylim = c(0, 7), axes = FALSE, xlab = "", ylab = "",
main = "样本量模拟流程", cex.main = 1.5)
steps <- c("指定各分层样本量", "假设 PPA/NPA 与 phi", "模拟 CCD2、FCD 结果",
"估 PPA/NPA + bootstrap CI", "单侧 NI 检验", "重复 N 次 → 估 power")
ys <- 6.4 - (seq_along(steps) - 1) * 1.12
for (i in seq_along(steps)) draw_box(5, ys[i], steps[i], "#2E86AB", w = 6.0, h = 0.72, cex = 1.05)
for (i in 1:(length(steps) - 1)) draw_arr(5, ys[i] - 0.36, 5, ys[i + 1] + 0.36)power_at <- function(n_per, n_sim = 200, delta1 = 0.10, alpha = 0.05, B = 800) {
rej <- logical(n_sim)
for (s in seq_len(n_sim)) {
d <- generate_population(2*n_per); cp <- subset(d, CCD1==1)
if (nrow(cp) < 5) { rej[s] <- NA; next }
rej[s] <- quantile(boot_diff_paired(cp$CCD2==1, cp$CDx==1, B = B), 1-alpha) < delta1
}
mean(rej, na.rm = TRUE)
}
n_grid <- c(100, 200, 400, 800, 1600); pw <- sapply(n_grid, power_at)
plot(n_grid, pw, type = "b", pch = 19, col = "#2E86AB", lwd = 2, ylim = c(0, 1),
xlab = "每分层目标 n", ylab = "power", main = "PPA NI 检验 power vs 样本量")
abline(h = 0.8, lty = 3, col = "gray50")power 随 \(n\) 单调上升,达 0.8 所需 \(n\) 一目了然。
最后补一个 follow-on 一致性的技术点:给定 \(G\) 条件独立,但 CCD1+ 分层内 FCD 与 CCD2 仍有残差正相关(CCD1+ 不能完全确定 \(G\))。用 phi 量化,验证下面这个锚点 \(P_{a1}=\varphi_1\sqrt{PPA_{C1C2}PPA_{C1F}(1{-}PPA_{C1C2})(1{-}PPA_{C1F})}+PPA_{C1C2}PPA_{C1F}\):
3-way 联合表(FCD 一次 + CCD 两次),用一致性量和两个锚点 \(P_{a1},P_{d0}\) 参数化:
| CCD1+/CCD2+ | CCD1+/CCD2- | CCD1-/CCD2+ | CCD1-/CCD2- |
|---|---|---|---|---|
FCD+ | Pa1 | PPAC1F − Pa1 | 1 − NPAC1C2 − NPAC1F + Pd0 | NPAC1C2 − Pd0 |
FCD- | PPAC1C2 − Pa1 | 1 − PPAC1C2 − PPAC1F + Pa1 | NPAC1F − Pd0 | Pd0 |
合计 | PPAC1C2 | 1 − PPAC1C2 | 1 − NPAC1C2 | NPAC1C2 |
a1p <- mean(c1pos$CCD2==1 & c1pos$CDx==1); pC2 <- mean(c1pos$CCD2==1); pF <- mean(c1pos$CDx==1)
phi1 <- (a1p - pC2*pF) / sqrt(pC2*(1-pC2)*pF*(1-pF))
ft_show(c(`φ_{1}` = round(phi1, 4), `P_{a1} 经验值` = round(a1p, 4),
`P_{a1} 公式值` = round(phi1*sqrt(pC2*pF*(1-pC2)*(1-pF)) + pC2*pF, 4)),
title = "φ 与锚点 P_{a1}:经验值 = 公式值")量 | 值 |
|---|---|
φ1 | 0.5868 |
Pa1 经验值 | 0.7705 |
Pa1 公式值 | 0.7705 |
✓ 联合表公式自洽。phi>0 提醒:“给定 \(G\) 条件独立” ≠ “分层内无关”——任何 follow-on 的样本量模拟都得带上这个 phi(见前面「样本量 / power」)。
统一结论:只要给对参数(真 \(p\)、真 \(c\)、正确权重),bridging / follow-on 机制能把被抽样偏差扭曲的疗效和一致性还原回 DGP 的已知真相。风险全在那几个不可识别的参数——\(\delta_{01}\)(缺失亚组疗效)、\(c\)、真实 prevalence——它们是假设出来的,不是估出来的。
bridging 与 follow-on:数据和方法论一模一样吗? 不一样。一句话:底层生成机制一样,但观测到的数据不同,方法论共用一套骨架、估计对象却不同。 分维度对照:
维度 | bridging | follow-on |
|---|---|---|
数据来源 | 药的 pivotal 试验本身 | 单独的一致性研究 + 已批准 CCD 的历史疗效 |
有无自带疗效数据 | 有(试验里 CTA+ 人群的 T、Y) | 无(FCD 上没有 T、Y) |
观测变量 | assay 结果 + 治疗臂 T + 结局 Y | 只有 assay 结果(CCD1/CCD2/FCD) |
缺失结构 | CTA−/CDx+ 那格没入组 → δ01 缺 | 富集抽样扭曲 CCD1 分层比例 + 无金标准 |
疗效从哪来 | 在自己试验里搬(CTA+ → CDx+) | 从 CCD 借(外推到 FCD+ 人群) |
核心公式 | δCDx = p·δ11 + (1−p)·δ01 | PPA/NPA 的 NI 检验 + pc 重加权 |
不可识别参数 | δ01(用敏感性参数 c 假设) | 真实 prevalence pc、分层内残差相关 φ |
共用的骨架:都是「assay 间 concordance + PPV 混合 + 一个不可识别参数」,把疗效/一致性从一个 assay 定义的人群搬到另一个。真正的分水岭是数据里有没有随机化疗效——bridging 有,只需搬运;follow-on 没有,只能借 CCD 的疗效并测自己与 CCD 的一致性。每加一层 assay 选择(多 CTA、pre-screening,见末章「进阶」),就多依赖一个猜测——这是反对多层 pre-screening 的根本原因。
提醒:本文 worked example 里两者共用同一个 DGP(同一批 \(G\)、assay、
TRUTH),那是教学简化——好让”同一类问题”一眼可见、并能用同一套真值对撞验证;真实世界里两者的数据集并不相同(如上表)。
位置 | 结论 |
|---|---|
§ 混合疗效 η | ✓ 混合公式 = 直接估计 = 真值 |
§ NPA/PPA | ✓ NPA = d/(b+d) 直接从表读出 |
§ 贝叶斯 p | ✓ Bayes 估 p = 直接估计 |
§ 联合表参数化 | ✓ 边际自洽 (φ>0) |
§ pc 重加权 | ✓ pc 重加权还原真值 (pc = 真实 CCD1 prevalence) |
§ standard bridging | ✓ 真 c 处还原真 δCDx |
§ 第 4 步 MAR 插补 | ✓ 正统 MAR 插补(含 CTA 判读)回到全数据估计;只用协变量偏保守;MNAR 下修不掉 |
§ c=1 恒等式 | ✓ c=1 时 p 代数消掉,桥接率 = 双阳应答率(stopifnot 自检) |
§ double bridging | ✓ 全程限定 CDx+ 后还原真值 |
§ multi-CTA 一致性权重 | ✓ 按阳性/阴性人数加权,汇总一致性还原真值 |
核验表最后两行(double bridging、multi-CTA)属末章「进阶」的两种额外情形。
全文疗效统一用连续端点 \(Y\) 的均值差 \(\delta\),是一个刻意的选择。原因:贯穿全文的两条搬运恒等式——疗效混合 \(\eta=PPV\,\delta_{\text{pos}}+(1{-}PPV)\,\delta_{\text{neg}}\)、bridging 的 \(\delta_{CDx}=p\,\delta_{11}+(1{-}p)\,\delta_{01}\)——都是全期望,只有当 \(\delta\) 是一个可塌缩 (collapsible) 的期望型量时才精确成立:均值差、某固定时点的生存概率差、或 RMST(restricted mean survival time,限制平均生存时间)差。
而现实肿瘤试验汇报疗效的通用货币是 Cox 模型给的 HR(hazard ratio)——HR 不可塌缩:两个亚组各自的 HR 按 \(p\) 线性加权 \(\neq\) 合并人群的 HR(风险比是比值不是期望,混合会系统性偏)。所以真实 bridging 里 \(\delta_{11}\) 常常就是一个 HR,但线性混合不能直接架在 HR 上——要么换到可塌缩尺度(均值差 / 生存概率差 / RMST),要么放弃精确、做数值近似。
一句话:本文选连续端点是为了让搬运恒等式精确成立、教学干净;换成 HR 时,“看似能线性搬运、其实要挑对尺度”本身就是 bridging 会踩的坑。(具体某篇 bridging 方法论文用哪种尺度,以该文为准;Cox 的内部估计 partial likelihood 与本文主题无关,略。)
调参入口:改「统一生成模型」里的
TRUTH(prevalence、各 assay sens/spec、marker+/−
真疗效),全文所有真值和估计会一致联动。
标准 bridging 只有一个缺失亚组 (CTA−/CDx+)。两种情形引入更多:Case I pre-screening(双重 bridging);Case II 多个 CTA。共同教训:每加一层 assay 选择,CDx 疗效估计离直接观测更远、不确定性更大。
为加快入组/降成本,CTA 前加 biomarker 粗筛 (pre-screen)。多一个上游缺失:本该 CTA+ 却被粗筛排除的患者。
par(family = "cjk", mar = c(0.5, 0.5, 3, 0.5))
plot(NA, xlim = c(0, 13.5), ylim = c(0, 9), axes = FALSE, xlab = "", ylab = "",
main = "Pre-screen → CTA → CDx:双重 bridging", cex.main = 1.45)
draw_box(1.2, 4.5, "Pre-screen", "gray30", w = 2.3, h = 0.8, cex = 0.95)
draw_box(4.1, 6.6, "Pre+", "#2E86AB", w = 1.9, h = 0.8, cex = 1.0)
draw_box(4.1, 2.2, "Pre-\n(未入组)", "gray60", w = 2.4, h = 0.9, cex = 0.9)
draw_arr(2.35, 4.6, 3.0, 6.2); draw_arr(2.35, 4.4, 3.0, 2.6)
draw_box(7.0, 7.7, "CTA+ (入组)", "#2E86AB", w = 2.5, h = 0.75, cex = 0.92)
draw_box(7.0, 5.4, "CTA-\n(未入组)", "#C73E1D", w = 2.5, h = 0.9, cex = 0.88)
draw_arr(5.3, 6.7, 5.85, 7.4); draw_arr(5.3, 6.5, 5.85, 5.7)
draw_box(7.0, 2.2, "CTA+", "#A23B72", w = 1.9, h = 0.75, cex = 0.92)
draw_arr(5.3, 2.2, 6.0, 2.2)
draw_box(10.7, 8.1, "CDx+ 观察 (delta_11)", "#2E86AB", w = 3.9, h = 0.7, cex = 0.9)
draw_box(10.7, 7.2, "CDx-", "gray55", w = 3.8, h = 0.6, cex = 0.85)
draw_arr(8.25, 7.85, 8.75, 8.1); draw_arr(8.25, 7.6, 8.75, 7.25)
draw_box(10.7, 5.4, "CDx+ 缺失 (stage-1)", "#C73E1D", w = 3.9, h = 0.7, cex = 0.9)
draw_arr(8.25, 5.4, 8.65, 5.4)
draw_box(10.7, 2.2, "CDx+ 缺失 (stage-2)", "#A23B72", w = 3.9, h = 0.7, cex = 0.9)
draw_arr(7.95, 2.2, 8.65, 2.2)
text(6.75, 0.5, "stage-1: Pre+&CTA+ → 完整 CTA+ stage-2: CTA+ → CDx+",
cex = 0.95, col = "gray30", font = 3)调整变两阶段:Stage 1 从观测的 Pre+/CTA+ 还原完整 CTA+ 队列;Stage 2 从 CTA+ 还原 CDx+ 人群。
\[\delta_{CTA+}=(p_1+(1{-}p_1)c_1)\,\delta'_{11},\qquad \delta_{CDx+}=(p+(1{-}p)c_2)\,\delta_{CTA+}\]
关键:两阶段所有子群都要先限定在 CDx+ 内,再按 Pre / CTA 细分。因为 estimand(\(\delta_{CDx+}\))始终活在 CDx+ 人群里,\(\delta'_{11}\) 指的是 (Pre+ & CTA+ & CDx+) 的疗效,不是整个 (Pre+ & CTA+) 的疗效——若不限定 CDx+,会用整个 CTA+ 的疗效冒充 CTA+&CDx+ 的疗效、系统性低估。下面严格限定 CDx+:
big3 <- generate_population(4e6, with_endpoint = TRUE); cdxpos <- subset(big3, CDx == 1)
# stage 1: 在 CTA+&CDx+ 内按 Pre 还原
d11p <- delta_hat(subset(cdxpos, Pre==1 & CTA==1)); d01p <- delta_hat(subset(cdxpos, Pre==0 & CTA==1))
p1 <- mean(subset(cdxpos, CTA==1)$Pre == 1)
delta_CTApos_s1 <- p1*d11p + (1-p1)*d01p; true_d11c <- delta_hat(subset(cdxpos, CTA==1))
# stage 2: 在 CDx+ 内按 CTA 还原
p_true <- true_p_cta_given_cdx(); d01_s2 <- delta_hat(subset(cdxpos, CTA==0))
delta_CDx_s2 <- p_true*delta_CTApos_s1 + (1-p_true)*d01_s2
ft_show(data.frame(`p_{1}` = round(p1,4), `c_{1}` = round(d01p/d11p,4),
`阶段1 还原` = round(delta_CTApos_s1,4), `阶段1 真值` = round(true_d11c,4),
`阶段2 还原` = round(delta_CDx_s2,4), `δ_{CDx} 真值` = round(true_delta_in_assay_pos(TRUTH$assays$CDx),4),
check.names = FALSE),
title = "Double bridging:两阶段依次还原真值")p1 | c1 | 阶段1 还原 | 阶段1 真值 | 阶段2 还原 | δCDx 真值 |
|---|---|---|---|---|---|
0.7942 | 0.9592 | 0.5928 | 0.5928 | 0.5494 | 0.5508 |
✓ 两阶段依次还原真值。每阶段一个缺口、一个敏感性参数——两阶段的双重收缩。变体:部分 pre-screening(只部分人群走粗筛,只有受影响子集需 double-bridging)、患者层面 pre-screening 信息(保留则能显式识别 Pre+/Pre− 分支,提高第一阶段透明度)。实践结论直白:尽量不用 pre-screening——它让最终 CDx 疗效估计更低更不精确。
保险/site 偏好/国别监管导致多个 CTA,各自与 CDx 一致性不同、偏差程度不同。假设:每人只按一个 CTA 入组;各 CTA 患者选择表现类似。
par(family = "cjk", mar = c(0.5, 0.5, 3, 0.5))
plot(NA, xlim = c(0, 13), ylim = c(0, 8), axes = FALSE, xlab = "", ylab = "",
main = "两个 CTA:并行 bridging", cex.main = 1.5)
draw_box(1.2, 4, "样本", "gray30", w = 1.9, h = 0.8, cex = 1.0)
draw_box(4.0, 6, "CTA1", "#2E86AB", w = 1.9, h = 0.78, cex = 1.0)
draw_box(4.0, 2, "CTA2", "#3AA0C0", w = 1.9, h = 0.78, cex = 1.0)
draw_arr(2.2, 4.2, 2.95, 5.7); draw_arr(2.2, 3.8, 2.95, 2.3)
draw_box(7.0, 7, "CTA1+", "#2E86AB", w = 1.9, h = 0.7, cex = 0.95); draw_box(7.0, 5, "CTA1-", "#C73E1D", w = 1.9, h = 0.7, cex = 0.95)
draw_arr(5.0, 6.1, 5.95, 6.9); draw_arr(5.0, 5.9, 5.95, 5.1)
draw_box(10.6, 7, "CDx+ 观察", "#2E86AB", w = 2.8, h = 0.66, cex = 0.9)
draw_box(10.6, 5, "CDx+ 缺失 → bridge", "#C73E1D", w = 3.3, h = 0.66, cex = 0.82)
draw_arr(8.0, 7, 9.0, 7); draw_arr(8.0, 5, 8.9, 5)
draw_box(7.0, 3, "CTA2+", "#3AA0C0", w = 1.9, h = 0.7, cex = 0.95); draw_box(7.0, 1, "CTA2-", "#C73E1D", w = 1.9, h = 0.7, cex = 0.95)
draw_arr(5.0, 2.1, 5.95, 2.9); draw_arr(5.0, 1.9, 5.95, 1.1)
draw_box(10.6, 3, "CDx+ 观察", "#3AA0C0", w = 2.8, h = 0.66, cex = 0.9)
draw_box(10.6, 1, "CDx+ 缺失 → bridge", "#C73E1D", w = 3.3, h = 0.66, cex = 0.82)
draw_arr(8.0, 3, 9.0, 3); draw_arr(8.0, 1, 8.9, 1)方法 A(加权一致性):先汇总 PPA/NPA/prevalence → 汇总 \(p\) → 一次 bridging。 方法 B(加权疗效):各 CTA 内分别 bridging → 按样本量合并。
truth2 <- TRUTH
truth2$assays$CTA1 <- c(sens = 0.85, spec = 0.90) # 阳性率约 0.33
truth2$assays$CTA2 <- c(sens = 0.90, spec = 0.50) # 特异度差, 阳性率约 0.62 (强对比)
gen_cta <- function(n, oc) { d <- generate_population(n, truth = truth2, with_endpoint = TRUE)
d$CTAx <- ifelse(d$G==1, rbinom(n,1,oc["sens"]), rbinom(n,1,1-oc["spec"])); d }
n1c <- 40000; n2c <- 40000; d1 <- gen_cta(n1c, truth2$assays$CTA1); d2 <- gen_cta(n2c, truth2$assays$CTA2)
bridge_one <- function(d, oc) {
prev <- truth2$prev_G; ocd <- truth2$assays$CDx
num <- sum(sapply(0:1, function(g){pc<-if(g==1)oc["sens"] else 1-oc["spec"]; pd<-if(g==1)ocd["sens"] else 1-ocd["spec"]; pc*pd*(if(g==1)prev else 1-prev)}))
den <- sum(sapply(0:1, function(g){pd<-if(g==1)ocd["sens"] else 1-ocd["spec"]; pd*(if(g==1)prev else 1-prev)}))
p <- num/den
p*delta_hat(subset(d, CTAx==1 & CDx==1)) + (1-p)*delta_hat(subset(d, CTAx==0 & CDx==1))
}
dB1 <- bridge_one(d1, truth2$assays$CTA1); dB2 <- bridge_one(d2, truth2$assays$CTA2)
ft_show(data.frame(`方法 B(加权疗效)` = round(dB1*n1c/(n1c+n2c) + dB2*n2c/(n1c+n2c), 4),
`δ_{CDx} 真值` = round(true_delta_in_assay_pos(truth2$assays$CDx, truth2), 4),
check.names = FALSE),
title = "多 CTA 方法 B(加权疗效)还原真值")方法 B(加权疗效) | δCDx 真值 |
|---|---|
0.5557 | 0.5508 |
汇总一致性的加权(一个易错点):跨 CTA 汇总 PPA/NPA 时,PPA 要按各队列的 CTA 阳性人数加权、NPA 按阴性人数加权,不能按总样本量 \(n_i/n\)——因为 PPA 是”阳性里判阳的比例”,权重自然是阳性人数。下面对比两种权重:
ppa1 <- true_concordance(truth2$assays$CTA1, truth2$assays$CDx)["PPA"]
ppa2 <- true_concordance(truth2$assays$CTA2, truth2$assays$CDx)["PPA"]
pos1 <- sum(d1$CTAx==1); pos2 <- sum(d2$CTAx==1)
ft_show(data.frame(`按阳性人数(对)` = round(unname((pos1*ppa1+pos2*ppa2)/(pos1+pos2)), 4),
`按总样本量(错)` = round(unname(ppa1*n1c/(n1c+n2c) + ppa2*n2c/(n1c+n2c)), 4),
check.names = FALSE),
title = "汇总一致性:按阳性人数 vs 按总样本量")按阳性人数(对) | 按总样本量(错) |
|---|---|
0.5454 | 0.5951 |
✓ 方法 B 还原真值;汇总一致性按阳性人数加权得到正确值,按总样本量加权则明显偏离。(注意区分:方法 B 最后按 \(n_i/n\) 合并疗效是另一回事——那是”各 CTA 使用份额”的建模选择、需业务判断,不是这里的加权问题。)
查阅用。\(\delta_{\text{pos}}/\delta_{\text{neg}}\) 是本文为”两块积木”起的简写(等价于两个 \(\mu\) 相减)。
符号 | 定义 | 备注 |
|---|---|---|
T | 治疗臂 =1 / 对照臂 =0 | |
G | 真实 marker 状态:1 阳 / 0 阴 | 需参考标准才可观测 |
μTG | 结局均值,按 (治疗臂 T, 真 marker G) 索引的四格 | 最底层原料 |
δpos = μ11 − μ01 | 真 marker+ 人群的治疗效应(积木一) | 本文简写 |
δneg = μ10 − μ00 | 真 marker− 人群的治疗效应(积木二) | 本文简写 |
η | FCD+ 人群疗效 = PPVf·δpos + (1−PPVf)·δneg | 按 FCD 索引 |
θ | CCD+ 人群疗效 = PPVc·δpos + (1−PPVc)·δneg | 按 CCD 索引;follow-on 借用的量 |
δCDx | CDx+ 人群疗效(bridging 估计目标) | |
δ11, δ01 | (CTA+ & CDx+) / (CTA− & CDx+) 人群疗效 | 观察到 / 缺失 |
δ'11, δ'01, δCTA+ | double bridging 第一阶段:(Pre± & CTA+ & CDx+) 疗效、还原出的 CTA+ 队列疗效 | 进阶 |
c, c1, c2 | 敏感性参数:δ01 = c·δ11(及两阶段版本),c ∈ (0,1) | 缺失格不可识别 |
p | Pr(CTA+ | CDx+)——bridging 搬运权重 | |
p1 | Pr(Pre+ | CTA+)——double bridging 第一阶段权重 | 进阶 |
pc | Pr(CCD1=1)——真实人群 CCD1 prevalence(重加权用) | 非样本比例 |
prev, π | Pr(CTA+)——意向使用人群的 marker prevalence | Bayes 估 p 用 |
PPVf, NPVf | FCD 的阳性/阴性预测值 Pr(G=1|FCD=1)、Pr(G=0|FCD=0) | |
PPVc, NPVc | CCD 的阳性/阴性预测值(同构定义,assay 换成 CCD) | |
PPA, NPA, OPA | 阳性/阴性/总体一致率(OPA=(a+d)/n 判读完全一致的比例,本文只列不用);下标两字母 = 参照 + 被比(C1=CCD1、C2=CCD2、F=FCD) | |
ξppa1, … | NI 检验的差距,如 ξppa1 = PPAC1C2 − PPAC1F | |
δ1, δ0 | NI 检验 PPA 侧 / NPA 侧的可接受 margin | 与疗效 δ 无关,是检验边界 |
φ1, φ2 | φ 系数:CCD1+ / CCD1− 分层内 FCD 与 CCD2 的相关 | |
Pa1, Pd0 | 3-way 联合表的两个概率锚点 | |
R1, R2, RCDx | 率版本的疗效:观察格 (CTA+ & CDx+) 应答率、缺失格应答率 (=c·R1)、桥接后 CDx+ 人群应答率 | 与 δ 版本同构,公式不变 |
R(缺失指示) | 缺失指示:R=1 表示该个体的 CDx 判读缺失(第 4 步用) | 测量层缺失 |
X, Z | X = 观测协变量(插补模型的自变量);Z = 待插补的 CDx 判读 | MAR 定义里的两个角色 |
m, β* | m = 插补次数;β* = 从 Cov(β̂) 抽出的一套系数(插补的第二次抽样) | 插补内部 |
W̄, B, T, FMI | Rubin 规则:组内方差均值、组间方差、总方差 T = W̄+(1+1/m)B、缺失信息比例 | 汇总口径:五数概括只含 B 那一份 |
(完整框架里还有按 FCD 索引的分量 \(\eta_{11},\eta_{01},\dots\) 和按 CCD 索引的 \(\theta_{11},\theta_{01},\dots\)——本文只用它们的差 \(\eta=\eta_{11}-\eta_{01}\)、\(\theta=\theta_{11}-\theta_{01}\),分量本身不必单独记。)
缩写 | 英文全称 | 中文含义 | 用在本文哪里 |
|---|---|---|---|
PPA / NPA / OPA | positive / negative / overall percent agreement | 阳性/阴性/总体一致率 | assay 间一致性评价(OPA 仅列出、不用) |
PPV / NPV | positive / negative predictive value | 阳性/阴性预测值 | assay+ 人群疗效混合 |
NI | non-inferiority | 非劣效性 | FCD 相对 CCD 的一致性检验 |
CI | confidence interval | 置信区间 | 一致性估计的不确定性 |
RCT | randomized controlled trial | 随机对照试验 | 试验设计语境 |
MI | multiple imputation | 多重插补 | 第 4 步:缺失 CDx 判读的插补 |
MCAR / MAR / MNAR | missing completely at random / at random / not at random | 完全随机缺失 / 随机缺失 / 非随机缺失 | 第 4 步:缺失机制三档 |
FMI | fraction of missing information | 缺失信息比例 | Rubin 规则里插补方差占总方差的比例 |
作者:胡杨博士 | 首次发布:2026-08-25 | 最后更新:2026-08-26 16:40 EDT