Scope of this document: These notes introduce the statistical methods used in bridging studies (or bridging trials) and follow-on companion diagnostic (CDx) development. The first two chapters establish the background—how precision medicine differs from conventional medicine, why evidence must sometimes be transported between assay-defined populations, and how enrollment, control, and blinding choices shape a CDx trial. The methods then proceed along two parallel tracks: the Concepts track defines each estimand and formula, while the Worked Example constructs a data-generating process (DGP) whose truth is known by design, derives every downstream dataset from it, and compares each estimate with its known target. This lets us test whether the bridging and follow-on methods recover treatment effects and assay agreement distorted by selective sampling. Every figure is generated from the R code in this document.
Core thesis: Bridging studies and follow-on CDx development address the same class of statistical problem: using agreement between assays to transport efficacy evidence from a population defined by one assay to a population defined by another. Their main difference is the structure of the missing data.
First, distinguish precision medicine from the conventional one-size-fits-most paradigm. Conventional development usually targets the average effect in a diagnostic population and manages individual variation after treatment begins. Precision medicine reverses that order: first use a biomarker (for example, mutation status) to divide patients into subgroups with different benefit–risk profiles, then make treatment decisions within each subgroup. The claim is no longer simply “the drug works for this disease,” but “the drug works for patients positive for this biomarker.”
This shift creates a problem that conventional development often avoids but precision medicine cannot: biomarker status must be assigned by an assay, and assays are imperfect and may change. Efficacy evidence therefore applies to a population selected by a particular assay. Change the assay and the selected population changes with it; whether the original evidence still applies becomes a statistical question. That is the problem studied throughout this document.
The statistical problem begins not with terminology but with a practical mismatch: a clinician has a positive or negative result from one testing platform, while the treatment evidence may come from a population defined by another.
Two typical examples:
(Similar examples include HER2 stratification via IHC/FISH, and BRCA/HRD panel variation with PARP inhibitors — the mechanism is identical, so we will not elaborate further.)
The common thread is that the biomarker is the biological feature of interest, whereas the assay or CDx is the tool used to measure it. A treatment effect is established in the population selected by that tool. When the tool changes, the selected population may change as well.
Why is this especially important in precision medicine? Diabetes and cardiovascular medicine also measure biomarkers and change instruments or reagents. The difference is often the availability of a common reference standard. Many conventional biomarkers are continuous, traceable quantities: blood glucose, HbA1c, and LDL measurements can be anchored to common reference methods. A new instrument can therefore be calibrated to the same standard, with little change in the population crossing a clinical threshold. That problem calls for recalibration, not transport of efficacy evidence. In precision medicine, a common reference standard is often unavailable: mutation calls depend on the assay, borderline samples may be classified differently, and performance must be characterized through inter-assay agreement (PPA/NPA) rather than calibration to an observed ground truth.
More precisely, the bridging/follow-on problem arises when three conditions hold simultaneously—a combination uncommon in conventional measurement settings but frequent in precision medicine:
Conversely, conventional clinical settings face the same statistical problem whenever these conditions are met: a new troponin assay can change who is classified as having myocardial infarction, an updated cardiovascular risk score can change who is recommended statin therapy, and HLA-B*5701 status determines eligibility for abacavir. Precision medicine did not invent the problem; it made prominent a problem that continuous, traceable measurements and looser assay–drug coupling often keep under control elsewhere.
Four assay roles used throughout this document:
Changing assays immediately raises four statistical issues:
All formulas that follow address the same question: how can efficacy evidence established under one assay be transported to a population defined by another? After fixing the terminology and context, we use a DGP with known truth to determine when the transport formulas recover their targets and where they necessarily depend on assumptions.
Pivotal co-development, bridging, and follow-on development are three distinct pathways. Their defining distinction here is the relationship between the final marketed CDx and the assay used to enroll the pivotal trial. A program generally follows one pathway at a given stage of development rather than conducting bridging and follow-on work simultaneously:
par(family = "cjk", mar = c(1, 1, 3, 1))
plot(NA, xlim = c(0, 12), ylim = c(0, 7), axes = FALSE, xlab = "", ylab = "",
main = "Three Validation Scenarios", 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, "Final CDx\nenrolled directly", "#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 enrolled\nfinal CDx differs", "#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, "Already approved\ncomparator CDx", "gray40", w = 3.0, h = 0.95, cex = 0.95)
draw_box(9.4, 2, "New follow-on\nCDx (FCD)", "#C73E1D", w = 4.0, h = 0.95, cex = 1.05)
draw_arr(6.6, 2, 7.3, 2)In one sentence: bridging addresses disagreement between the trial assay and the final CDx; follow-on development asks whether a new CDx can replace an approved one. Clinical practice is more complicated still: in-house NGS panels, LDTs, commercial comprehensive panels, and different specimen types coexist, while approval, reimbursement, and actual test use do not always align. The later bridging and follow-on chapters develop both pathways from the same statistical foundation.
The previous section (§1.3) distinguished three pathways by the relationship between the CDx and the enrollment assay. We now move from development pathway to trial design: how should a CDx trial be constructed?
Start with two distinctions. A companion diagnostic (CDx) is an in vitro diagnostic device that provides information essential to the safe and effective use of a corresponding therapeutic product (U.S. Food and Drug Administration 2014). CDx development involves two claim types and two forms of validation:
The claim determines the required evidence. Trial design then involves three largely separate choices, plus one overarching discipline. Because these ideas are often conflated, the table separates them explicitly:
Concept or practice | Design dimension |
|---|---|
Who enrolls (enrichment CDx+ only / unselected including CDx−) | Trade-off One · Enrollment |
Randomization, with or without parallel control arm | Trade-off Two · Control |
Open-label, double-blind/triple-blind, independent BICR assessment | Trade-off Three · Blinding |
Lock CDx analysis plan before unblinding, prevent post-hoc | Discipline · Prespecified analysis |
The three dimensions are conceptually distinct, although some combinations constrain others. An enriched population can be studied in either an RCT or a single-arm trial; an RCT can be blinded or open-label; and a single-arm trial is necessarily open-label to patients and investigators but can still use BICR. We address each dimension in turn.
In theory, a predictive claim requires a marker-by-treatment design: after CDx testing, both strata—CDx+ and CDx−—should undergo 1:1 randomization (treatment vs. control) to estimate the interaction. But in practice (especially with targeted oncology drugs), the CDx− stratum is usually entirely screened out, neither enrolled nor randomized (red X in the diagram below):
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 design (theory) vs. enrichment reality: CDx− often screened out", cex.main = 1.3)
draw_box(1.7, 3, "Eligible patients", "gray30", w = 3.0, h = 0.9, cex = 1.05)
draw_box(5.0, 3, "CDx testing", "gray40", w = 2.2, h = 0.9, cex = 1.05); draw_arr(3.3, 3, 3.85, 3)
# CDx+ pathway: actually enrolled, randomized
draw_box(8.4, 4.5, "CDx+ 1:1 randomization", "#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) "Treatment T=1" else "Control 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− pathway: screened out in enrichment design—light blue (not enrolled) + red X
draw_box(8.4, 1.5, "CDx− 1:1 randomization", "#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) "Treatment T=1" else "Control 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, "Enrichment (targeted) design: CDx− screened out, not enrolled, not randomized", col = "#C73E1D", font = 2, cex = 0.95)Excluding CDx− patients is often justified on two grounds:
Most CDx trials therefore use an enrichment (targeted) design: only assay-positive patients (in practice, CTA+) are enrolled, and treatment is randomized only within that population. Such a design supports a selection claim (“the positive population benefits”), not a full predictive claim based on a treatment-by-marker interaction—the lower branch in the diagram contributes no data.
This is the source of the central difficulty. CDx− patients—and later the CTA−/CDx+ cell—were never randomized and have no outcome data. Transporting efficacy evidence to a population defined by another assay therefore requires assumptions about unobserved cells. This is why \(\delta_{01}\) is not identifiable in the bridging analysis.
When is exclusion appropriate? The answer depends on the intended claim:
A third, intermediate option is to enroll an unselected population and use CDx only for analysis (a prospective–retrospective design). Patients are randomized without using CDx status for enrollment; archived specimens are tested, and the analysis is stratified by CDx according to a locked plan. Credibility depends on prespecification: the assay, cutoff, and analysis must be fixed before the biomarker–outcome relationship is examined. Choosing them after inspecting the data is post-hoc data dredging and is not regulatorily persuasive. Example: Karapetis, Amado, and colleagues (2008) studied KRAS in completed, unselected RCTs using archived tumor samples, but the treatment-by-KRAS analyses were prespecified. The retrospective specimen testing was therefore still capable of supporting a regulatory restriction to KRAS wild-type disease.
Here “locking” means prespecifying the analysis before primary unblinding and before examining the biomarker–outcome association. It prevents selective analysis; it is not the same as blinding clinicians or patients, which is Trade-off Three (§2.3).
Trade-off One determines who enrolls. This section asks a separate question: should enrolled patients be assigned to a concurrent control arm? This choice is distinct from enrichment: an enriched population can be studied in either an RCT or a single-arm trial (SAT). Many precision-medicine indications have initially been supported by single-arm evidence. The key issue is when that design is credible and when an RCT is necessary.
RCT: the evidentiary standard, but sometimes difficult in precision medicine. An RCT estimates comparative benefit—how much better the experimental treatment performs than no treatment or the current standard of care. Randomization balances prognostic factors in expectation, allowing between-arm differences to be attributed to treatment rather than baseline disease or population composition. Common measures are the hazard ratio (HR) for progression-free survival (PFS) or overall survival (OS). An HR below 1 indicates a lower instantaneous event hazard in the experimental arm (for example, HR = 0.6 corresponds to an estimated 40% lower hazard under the proportional-hazards interpretation). Such concurrent comparative evidence is the standard for full approval and comparative claims; a single-arm trial cannot provide it.
How PFS and OS are calculated. Both are time-to-event endpoints measured from randomization (\(t=0\)). Each patient contributes a time and an event indicator. OS is time to death from any cause; PFS is time to radiographic disease progression (for example, under RECIST) or death, whichever occurs first. A patient who remains alive and progression-free at the data cutoff, or is lost to follow-up, is censored: follow-up is known through the censoring time but not afterward. Within each arm, these observations form a Kaplan–Meier curve. Events produce downward steps; censoring times are marked but do not themselves lower the curve. The time at which the curve reaches 0.5 is the median PFS or OS. A log-rank test compares the curves, and a Cox model estimates the HR. Thus each arm has its own PFS and OS curve, and efficacy is expressed through a between-arm contrast such as an HR or a difference in medians.
Advantages of an RCT:
Costs and limitations of an RCT (most are not unique to precision medicine):
Why crossover dilutes OS. This is a structural feature of any RCT that permits crossover, not a problem unique to precision medicine. OS follows patients until death. If control-arm patients receive the effective experimental treatment after progression, their survival may improve and the OS curves may converge. PFS ends at the first progression, which occurs before treatment switching, so it is not affected in the same way. When substantial crossover is expected, PFS is therefore often favored over OS as the primary endpoint. The simulated Kaplan–Meier curves below keep the experimental arm fixed and vary only whether the control arm crosses over:
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) # treatment arm OS median ~30
t_c0 <- rmed(n, 18) # control no crossover median ~18
t_c1 <- rmed(n, 27) # control with crossover: after switch OS raised to ~27, approaching treatment arm
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 treatment vs control
plot(f, col = c("#C73E1D", "#2E86AB"), lwd = 3, xlim = c(0, cens),
xlab = "Months post-randomization", ylab = "Survival probability S(t)", cex.lab = 1.3, cex.axis = 1.15, mark.time = FALSE)
title(main = main, cex.main = 1.35)
legend("topright", c("Treatment", "Control"), 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: no crossover (arms separate)")
kmplot(mk(t_trt), mk(t_c1), "OS: with crossover (control raised)")Without crossover (left), the curves separate and HR≈0.63 indicates benefit. With crossover (right), the control-arm OS curve rises toward the experimental arm and HR≈0.94 makes the effect appear small. The simulated treatment effect has not changed; crossover has attenuated the OS contrast. Because PFS is measured before the switch, it preserves the original contrast in both scenarios.
RCTs are more feasible in larger populations with an appropriate active comparator. Examples include trastuzumab in HER2+ breast cancer, vemurafenib in BRAF V600E melanoma (BRIM-3), and the confirmatory crizotinib trials PROFILE 1007 and 1014.
Single-arm trial (SAT): when it can be credible. A single-arm trial has no concurrent control; all participants receive treatment. Common endpoints are objective response rate (ORR) and duration of response (DoR), assessed against a prespecified historical benchmark. The rationale is that a confirmed objective tumor response is often much less likely to arise from natural history alone than a PFS or OS outcome. Single-arm evidence is most persuasive when the marker is rare (for example, an NTRK fusion prevalence below 1%), the effect is large, unmet need is substantial, and accelerated approval is being pursued. Examples include larotrectinib and entrectinib in tumor-agnostic NTRK-fusion basket trials, pembrolizumab in MSI-H/dMMR tumors, osimertinib in EGFR T790M disease, and the initial accelerated approval of crizotinib.
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 = "Single-arm trial (SAT): enriched enrollment, treatment arm only, no concurrent control", cex.main = 1.3)
draw_box(1.7, 3, "Eligible patients", "gray30", w = 3.0, h = 0.9, cex = 1.05)
draw_box(5.0, 3, "CDx testing", "gray40", w = 2.2, h = 0.9, cex = 1.05); draw_arr(3.3, 3, 3.85, 3)
draw_box(8.2, 3, "CDx+ enrollment\n(enriched)", "#2E86AB", w = 2.6, h = 1.0, cex = 0.95); draw_arr(6.15, 3, 6.85, 3)
# treatment arm only
draw_box(11.7, 4.2, "Treatment 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, "Endpoints: ORR / DoR", cex = 0.9, col = "gray20", font = 3)
# control arm deleted—grayed out + red X
draw_box(11.7, 1.8, "Control 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, "Single-arm: no control; efficacy = ORR vs. a prespecified historical threshold (no concurrent randomization)",
col = "#C73E1D", font = 2, cex = 0.72)Decision rule. A common sequence is single-arm evidence for accelerated approval followed by a confirmatory RCT, as with crizotinib and osimertinib. In broad terms: the rarer the marker, the larger the anticipated effect, and the fewer the alternatives, the stronger the case for a single-arm design; the larger the population, the greater the need for a comparative PFS or OS claim, and the more suitable the available comparator, the stronger the case for an RCT.
Design-specific cautions.
When feasible, the most bias-resistant RCT can blind three groups: ① patients to treatment assignment, ② investigators to assignment, and ③ independent outcome assessors and analysts (for example, centralized imaging or pathology review and blinded statistical analysis). The first two forms require a treatment contrast that can be masked; without a control, they are impossible. Blinding is therefore related to, but distinct from, the choice of control group:
Each layer of blinding addresses a different source of bias; the table shows which layer a single-arm trial can retain:
Blind whom | Reduces bias from | Single-arm feasible |
|---|---|---|
① Patient | Placebo effects and differential adherence/dropout | ✗ (everyone receives drug) |
② Investigator/researcher | Treatment administration, dose adjustment, and subjective assessment | ✗ |
③ Independent assessment + analysis (BICR / statistician) | Outcome assessment and analysis | ✓ (via BICR) |
Do not confuse blinding with analysis prespecification. “Lock the CDx analysis plan before unblinding” refers to database or treatment-code unblinding at the end of the trial. The biomarker analysis must be fixed before that point to prevent post-hoc selection. Blinding governs who knows treatment assignment; prespecification governs what is decided before the data are examined.
Back to the framework. In the worked example, efficacy is an arm difference, \(\delta = \mathrm{E}[Y\mid T{=}1] - \mathrm{E}[Y\mid T{=}0]\). In a single-arm study, the transported quantity is instead the response rate in the assay-positive population. The mixing logic is the same: assay error introduces truly marker-negative patients into the positive group, and changing assays changes that mixture. The linear transport formulas that follow therefore apply to collapsible mean-type estimands such as mean differences and response rates. They do not apply directly to a hazard ratio, a caveat developed later in “Why Use Continuous Endpoints”.
This chapter builds the data-generating process (DGP) that supports the later bridging and follow-on analyses. The foundation has two parts that must be developed together:
The two parts are developed together: each concept is immediately translated into a simulated quantity and checked numerically. Before building the model, we state the two problems it must solve.
Two concrete problems (both ask whether treatment evidence can be transported across assays):
Why simulate before using real data? The central difficulty is that the relevant truth is unobserved: latent marker status \(G\) is unavailable, the CTA−/CDx+ cell contains no randomized patients, and its treatment effect \(\delta_{01}\) cannot be measured. Real data therefore provide no definitive benchmark for a bridging estimate. In the DGP below, the truth is known by construction. We can sample under realistic designs, apply each formula, and compare the result with its true target. Recovery of the target validates the implementation under the stated assumptions; failure to recover it reveals exactly where identification depends on an assumption rather than observed data.
Road map: first define the notation, construct the known-truth DGP, verify its self-consistency, and show how assay error dilutes treatment effects in assay-positive populations. We then study bridging, which transports evidence from CTA+ to CDx+ despite a missing stratum, followed by follow-on development, which borrows efficacy evidence through agreement with an approved CCD. The two problems share a statistical structure but differ in their observed and missing data, as summarized later in “Synthesis and Verification Status”. The same generator is reused in the advanced examples.
The worked example begins with a simulation in which truth is known by construction. Its first two layers are organized around the latent true marker status \(G\):
The third, outcome layer—continuous endpoint \(Y\) and treatment effect \(\delta\)—is added later in “Adding the Outcome Layer”. Deferring it keeps the assay model transparent: all agreement patterns are induced coherently by the common latent cause \(G\), rather than assembled from potentially incompatible marginal probabilities.
The table gives the precise notation for these layers
(Pr(·) denotes probability, and | is read as
“given”):
Symbol | Strict Definition | Layer / Remark |
|---|---|---|
G | True marker status: G=1 marker-positive, G=0 marker-negative | Truth layer · Latent and unobserved without a reference standard |
prevG (π) | Pr(G=1), true prevalence (1−π = Pr(G=0)) | Truth layer · The unique prevalence parameter |
T | Treatment arm: T=1 treatment, T=0 control | Outcome layer · Used when § "Outcome Layer" introduces endpoint Y |
assay A | Any assay (noisy classifier of G); in this text A ∈ {CTA, FCD(=final CDx), CCD, Pre} | Assay layer · A is placeholder, substitute concrete assay to get that assay's quantity |
sensA / | Sensitivity sensA = Pr(A=1|G=1); | Assay layer · Directly specified in TRUTH (generation parameter) |
PPVA / | Positive predictive value PPVA = Pr(G=1|A=1); | Assay layer · Derived from sens/spec and prevalence π via Bayes, not a free parameter |
Every quantity introduced so far is either a binary indicator or a probability. \(G\) and \(T\) take values in \(\{0,1\}\); \(\pi\), sensitivity, specificity, PPV, and NPV lie between 0 and 1. The DGP sets \(\pi=0.30\) and uses sensitivity/specificity pairs of 0.85/0.90 for CTA, 0.95/0.96 for CDx (= FCD), 0.93/0.94 for CCD, and 0.80/0.75 for Pre. Bayes’ theorem then gives \(PPV_f\approx0.91\) and \(PPV_c\approx0.87\). The outcome layer introduced next contains real-valued treatment effects, which may be positive or negative.
Why \(\pi=0.30\)? Any fixed prevalence could be used to verify the formulas, but 0.30 offers both plausibility and pedagogical clarity. It lies within the 20–40% range seen for many tumor biomarkers, avoids the artificial symmetry of 0.50, and is not so extreme that a rare marker drives PPV to very low values. Even the high-performing CDx then has \(PPV_f\approx0.91\) rather than 1, making dilution visible without exaggerating it. The assays likewise span a deliberate performance range—from CDx as the best to Pre as the worst—so that higher PPV and less dilution can be seen directly.
The subscript identifies the assay. \(A\) is a placeholder that can be replaced by a specific assay. Throughout this document, \(f\) denotes FCD (the final or new CDx) and \(c\) denotes CCD (the comparator CDx). Thus \(PPV_f=\Pr(G{=}1\mid FCD{=}1)\) and \(PPV_c=\Pr(G{=}1\mid CCD{=}1)\); the same convention applies to \(sens_A\) and \(spec_A\).
PPV is not a free sensitivity parameter. Bayes’ theorem determines it from the assay’s sensitivity and specificity and the marker prevalence \(\pi\) in the target population:
\[PPV=\frac{sens\cdot\pi}{sens\cdot\pi+(1-spec)(1-\pi)}.\]
Prevalence is crucial. Even an accurate assay can have a modest PPV when the marker is rare, because false positives may comprise a substantial share of all positive calls. PPV is therefore marker- and population-specific; once \((sens,spec,\pi)\) are fixed, it is determined rather than guessed. The simulation checks this Bayes relationship again below.
The outcome-layer quantities \(Y\), \(\mu\), \(\delta_{\text{pos}}\), \(\delta_{\text{neg}}\), \(\eta\), and \(\theta\) are defined in “Adding the Outcome Layer”. PPA, NPA, and the transport weight \(p\) are defined in the next section. A consolidated notation table appears at the end.
TRUTH <- list(
prev_G = 0.30, # Pr(G=1)
assays = list(
CTA = c(sens = 0.85, spec = 0.90), # clinical trial assay (for enrollment)
CDx = c(sens = 0.95, spec = 0.96), # final CDx / FCD (best)
CCD = c(sens = 0.93, spec = 0.94), # comparator CDx (follow-on reference)
Pre = c(sens = 0.80, spec = 0.75) # pre-screen (coarse screen, worst)
),
endpoint = list(mu_G1_T0 = 0, delta_pos = 0.60, # true marker+: control mean 0, treatment +0.60
mu_G0_T0 = 0, delta_neg = 0.05, # true marker−: drug effect nearly nil
sd = 1.0)
)Mapping assay roles to data columns: the code column
CDx represents the FCD (the new or final
CDx); CCD1 and CCD2 are two independent
replicates of the CCD; CTA represents the
clinical trial assay; and Pre represents the coarse
prescreen.
The implementation has two complementary parts: a data generator and closed-form truth functions.
generate_population() samples \(G\) from its prevalence and then generates
each assay call as a conditionally independent, imperfect classification
of \(G\). With
with_endpoint=TRUE, it also generates \(Y\), although the outcome layer is not used
in this section. The function returns a dataset of size \(n\).TRUTH, without sampling. This section
uses three assay-layer functions:
assay_prev(): the marginal assay-positive probability
\(\Pr(\text{assay}{=}1)\).true_concordance(): the PPA and NPA of two assays, with
assay A as the reference: \(PPA=\Pr(B{+}\mid
A{+})\) and \(NPA=\Pr(B{-}\mid
A{-})\).true_p_cta_given_cdx(): the bridging weight \(p=\Pr(CTA{+}\mid CDx{+})\).The fourth function, true_delta_in_assay_pos(), belongs
to the outcome layer and is introduced later. The same validation
pattern is used throughout: simulate an estimate → compare it
with the closed-form target → verify the implementation.
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) { # outcome layer: used when discussing efficacy
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 layer operators: marginal positive rate; true PPA/NPA of two assays; true p=Pr(CTA+|CDx+). (δ operator defined in outcome layer)
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)
}We now check self-consistency at the assay layer only. The same quantities are computed in two independent ways: analytically from the parameters and empirically from a simulated population of two million. Near equality—and a difference near zero—shows that the formulas and generator implement the same DGP. The closed-form values can then serve as benchmarks in later sections.
big <- generate_population(2e6)
tc <- true_concordance(TRUTH$assays$CTA, TRUTH$assays$CDx) # named vector c(PPA=, NPA=)
closed <- c(tc[["PPA"]], tc[["NPA"]], true_p_cta_given_cdx()) # [[ ]] extract value, no element name
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(
`Quantity` = c("PPA_{CTA,CDx} = Pr(CDx+|CTA+)",
"NPA_{CTA,CDx} = Pr(CDx−|CTA−)",
"p = Pr(CTA+|CDx+)"),
`Closed-form truth` = round(closed, 4),
`Simulated 2M estimate` = round(emp, 4),
`Difference` = round(abs(closed - emp), 4),
check.names = FALSE),
title = "Assay-layer validation: closed-form targets vs simulated estimates")Quantity | Closed-form truth | Simulated 2M estimate | Difference |
|---|---|---|---|
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 |
✓ The assay layer is self-consistent. Its closed-form targets can be used to validate the later analyses.
Question: when an imperfect assay defines a positive population, how much is the observed treatment effect diluted relative to the truly marker-positive population? Both bridging and follow-on analyses transport effects between assay-defined populations, so both depend on this mixing relationship. We can now add the outcome layer that was deferred above.
In the outcome layer, the continuous endpoint \(Y\) has a mean determined by treatment arm
\(T\) and true marker status \(G\). The DGP specifies a treatment effect
\(\delta_{\text{pos}}\) for truly
marker-positive patients and \(\delta_{\text{neg}}\) for truly
marker-negative patients. This is exactly the layer produced by
generate_population(..., with_endpoint = TRUE). Its
closed-form truth function is:
true_delta_in_assay_pos <- function(oc, truth = TRUTH) { # efficacy in assay+ population = PPV-weighted δ
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)
}The effect is visible immediately. The treatment effect among truly marker-positive patients is 0.60, but it is only 0.551 in the CDx+ population because false-positive calls introduce truly marker-negative patients. It is lower still—0.482—in the CTA+ population because CTA performs less well. Bridging exists to transport the effect observed in CTA+ toward the effect relevant to CDx+.
The formal derivation uses two treatment-effect building blocks. Partition patients by treatment arm \(T\) and true marker status \(G\) into four cells, with outcome mean \(\mu_{TG}\) in each cell:
| True marker+ (G=1) | True marker− (G=0) |
|---|---|---|
Treatment T=1 | μ11 | μ10 |
Control T=0 | μ01 | μ00 |
The figure shows how these cells arise: first partition patients by true marker status \(G\), then randomize treatment within each stratum, producing the four means \(\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 = "Four outcome means: true marker G × treatment arm T", cex.main = 1.3)
draw_box(1.3, 3, "Patients", "gray30", w = 1.9, h = 0.85, cex = 0.95)
draw_box(3.9, 3, "Latent true marker G\n(known in simulation)", "gray45", w = 2.7, h = 1.0, cex = 0.85)
draw_arr(2.25, 3, 2.55, 3)
draw_box(6.9, 4.4, "True marker+\nG=1", "#2E86AB", w = 2.3, h = 0.9, cex = 0.9)
draw_box(6.9, 1.4, "True 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, "μ subscript = (treatment arm T, true marker G). Within each marker stratum, treatment minus control gives the effect: δpos = μ11 − μ01; δneg = μ10 − μ00.",
col = "gray25", cex = 0.8, font = 3)The subscripts of \(\mu\) follow this order: the first indexes treatment arm \(T\); the second indexes true marker status \(G\). Within each marker stratum, treatment minus control gives the stratum-specific treatment effect. The document uses only these two true-marker-specific building blocks:
\[\delta_{\text{pos}}=\mu_{11}-\mu_{01}\ (\text{effect in truly marker-positive patients}),\qquad \delta_{\text{neg}}=\mu_{10}-\mu_{00}\ (\text{effect in truly marker-negative patients}).\]
What do the \(\mu\) values represent? Let \(Y\) be a continuous endpoint—for example, tumor shrinkage or change in a clinical score. Then \(\mu_{TG}\) is the mean of \(Y\) in one cell: a real-valued quantity, not a percentage. The DGP sets both control means to zero, so \(\mu_{01}=0\) for marker-positive controls and \(\mu_{00}=0\) for marker-negative controls. The treatment means are \(\mu_{11}=0.60\) and \(\mu_{10}=0.05\), giving \(\delta_{\text{pos}}=0.60\) and \(\delta_{\text{neg}}=0.05\). Individual outcomes vary normally around these means with standard deviation 1. Keep the scales distinct: \(\mu\), \(\delta\), \(\eta\), and \(\theta\) are real-valued outcome means or contrasts, whereas \(\pi\), sensitivity, specificity, PPV, PPA, \(p\), and \(c\) are probabilities or ratios between 0 and 1.
The treatment effect in the FCD+ population, \(\eta\), is a weighted average of these two building blocks. The weight is the assay’s positive predictive value, \(PPV_f=\Pr(G{=}1\mid FCD{=}1)\), the proportion of FCD+ patients who are truly marker-positive:
\[\eta = PPV_f\,\delta_{\text{pos}} + (1-PPV_f)\,\delta_{\text{neg}}.\]
Nothing in this identity is unique to FCD. For any assay \(A\), the treatment effect in the \(A+\) population is \(PPV_A\,\delta_{\text{pos}}+(1-PPV_A)\,\delta_{\text{neg}}\). The two true-marker-specific treatment effects remain fixed; only the assay-specific PPV changes. This is the reference-standard-available framework in its simplest form.
The table separates the easily confused roles of \(\mu\), \(\delta\), \(\eta\), and \(\theta\): cell means form treatment-effect building blocks, and PPV weights mix those building blocks within an assay-positive population.
Step | Symbol | What it is / indexed by | Relationship / Example |
|---|---|---|---|
① Layer | μTG | Outcome mean of one cell (same unit as Y); indexed by arm T × true marker G | μ11, μ01, |
② Subtract → get building block | δpos, | Treatment minus control within the same true-marker stratum; the two building blocks used throughout | δpos = μ11−μ01; |
③ Weight by PPV → mix building blocks | η(FCD+), | Treatment effect in an assay-positive population; the weight is that assay's PPV | η = PPVf·δpos+(1−PPVf)·δneg; |
How to read the notation. \(\eta\) is the FCD+ population effect that bridging seeks to recover when FCD is the final CDx. \(\theta\) is the CCD+ population effect that follow-on development seeks to borrow. The construction is symmetric: the assay changes, and therefore the PPV weight changes, but the two true-marker-specific effects do not. The subscripts provide a useful reminder. \(\mu_{TG}\) needs two subscripts because a cell mean is indexed by both treatment arm and true marker status. \(\delta_{\text{pos}}\) and \(\delta_{\text{neg}}\) need only the marker stratum because differencing has already removed the treatment-arm index. \(\eta\) and \(\theta\) carry no true-marker subscript because each averages over the latent marker strata.
Thus \(\eta\) and \(\theta\) are both treatment effects in assay-positive populations. Bridging later applies the same law of total expectation after partitioning the CDx+ population by CTA status. Its symbols \(\delta_{11}\), \(\delta_{01}\), and \(\delta_{CDx}\) index CTA/CDx cells rather than the treatment/true-marker cells indexed by \(\mu\). The figure illustrates the weighted mixture for the best-performing assay, CDx, with \(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 = "Effect mixing: assay error dilutes the effect in the assay-positive population", cex.main = 1.30)
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) # left=marker−, right=marker+, same order as axis below
text(0.5, 0.93, "① Composition of assay+ population (by true marker status)", 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("True marker− %.0f%%", 100*(1-PPV)), col = "#3B7DB0", font = 2, cex = 0.82)
text((sp+xR)/2, 0.78, bquote(paste("True marker+ ", PPV[f], "≈", .(round(100*PPV)), "%")), col = "white", font = 2, cex = 0.95)
text(0.5, 0.5, "② Weight the two true-marker effects by these proportions → population effect η", cex = 1.00, 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, "Treatment effect δ (treatment minus control)", 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("True−\nδ=%.2f", dneg), col = "#3B7DB0", font = 2, cex = 0.9)
text(xof(dpos), 0.38, sprintf("True+\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)As PPV increases, \(\eta\) approaches \(\delta_{\text{pos}}\) and dilution decreases. Lower PPV introduces more truly marker-negative patients and pulls the observed effect downward.
We now compute the mixture three ways for both \(\eta\) (CDx+) and \(\theta\) (CCD+), showing that the same calculation applies after changing the assay:
mean(sub$G==1)).delta_hat). This is the population
contrast a trial would observe.true_delta_in_assay_pos() to calculate PPV exactly from the
assay parameters and prevalence, then apply the mixing identity without
sampling.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 counted from sample
c(PPV = round(ppv, 4),
`Mixing formula` = round(ppv*TRUTH$endpoint$delta_pos + (1-ppv)*TRUTH$endpoint$delta_neg, 4),
`Direct estimate` = round(delta_hat(sub), 4), # measure Y only, no model assumed
`Closed-form truth` = round(true_delta_in_assay_pos(oc), 4)) # no sampling, compute exactly from parameters
}
tab <- rbind(`η (FCD+ = CDx+)` = mix_check("CDx", TRUTH$assays$CDx),
`θ (CCD+)` = mix_check("CCD1", TRUTH$assays$CCD))
ft_show(tab, title = "The same identity for two assays: η and θ agree across three calculations", rowname = "Population effect")Population effect | PPV | Mixing formula | Direct estimate | Closed-form truth |
|---|---|---|---|---|
η (FCD+ = CDx+) | 0.9112 | 0.5512 | 0.5578 | 0.5508 |
θ (CCD+) | 0.8717 | 0.5295 | 0.5304 | 0.5280 |
✓ All three calculations agree within simulation
error. \(\eta\) (CDx+) is
about 0.55 and \(\theta\) (CCD+) about
0.53. Because CCD has a slightly lower PPV, its positive population
contains more truly marker-negative patients and its treatment effect is
more diluted. The mixing-formula and closed-form calculations both use
the same identity, differing only in whether PPV is estimated or
calculated from parameters. The direct estimate is the independent
check: it computes the outcome contrast without invoking the identity
and still agrees with the closed-form target. The same
mix_check function is used for \(\eta\) and \(\theta\); only the assay column
changes.
The key mathematical point is easy to miss: a treatment effect in a combined population is the prevalence-weighted average of subgroup effects only on an appropriate effect scale. For a mean difference \(\delta\), the relationship follows from the law of total expectation: each arm mean is a weighted average of its subgroup means, and differencing preserves the weights under randomization.
The relationship does not hold for every effect measure. A Cox-model hazard ratio is non-collapsible: even if each subgroup has HR = 0.6, a prevalence-weighted average of the subgroup HRs does not in general equal the marginal HR. Ratios are not expectations and cannot be transported by this linear identity. The examples therefore use a continuous-endpoint mean difference, for which the identity is exact. In practice, an analysis may need a collapsible scale such as a mean difference, a fixed-time survival-probability difference, or a restricted mean survival time (RMST) difference. See “Why Use Continuous Endpoints”.
The figure connects the chapter’s quantities. Both branches begin with latent truth \(G\). On the assay branch, each imperfect classifier is defined by sensitivity and specificity, which induce PPV/NPV, PPA/NPA, and the bridging weight \(p\). On the outcome branch, \(G\) and independently randomized treatment \(T\) determine the four means \(\mu_{TG}\); treatment-minus-control contrasts within marker strata yield \(\delta_{\text{pos}}\) and \(\delta_{\text{neg}}\). The branches meet in the population mixtures: assay-specific PPVs weight the two true-marker effects to produce \(\eta\) and \(\theta\), while \(p\) later weights CTA-defined cells to produce \(\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 = "Foundation map: truth → assay and outcome → treatment-effect mixing",
cex.main = 1.1)
# Multi-line boxes: each line can be plain string or plotmath expression (subscript via X[sub], matching table/text subscripts)
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)
}
# ① Truth layer G (root)
draw_box(7, 10.2, "Truth layer G ∈ {0,1}\nπ = Pr(G=1) (true prevalence)", "#6A4C93", w = 4.4, h = 1.0, cex = 0.95)
# ② Assay layer (noisy classifier of G) + ③ Outcome layer μ (G × T four cells)
draw_box(3.1, 7.4, "Assay layer A = noisy classifier of G\nCTA · CDx(=FCD) · CCD · Pre", "#2E86AB", w = 4.8, h = 1.1, cex = 0.82)
cellbox(10.9, 7.4, 4.8, 1.1, list(
expression(paste("Outcome layer ", mu[TG], " (4 cells)")),
"row = arm T, col = true marker G"), "#A23B72", "white", cex = 0.82)
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, "determines outcomes", cex = 0.82, adj_y = 0.30)
# ④ Treatment arm T (orthogonal axis)
draw_box(12.2, 9.9, "Treatment arm T ∈ {0,1}\nrandom, independent of G", "#E8871E", w = 3.6, h = 0.95, cex = 0.74)
draw_arr(11.9, 9.4, 11.2, 8.0, "another dimension", cex = 0.8, adj_y = 0.25)
# ⑤ Assay branch → closed-form derivation
cellbox(3.1, 4.5, 5.7, 1.6, list(
"Closed-form (computed exactly, no sampling)",
expression(paste(PPV[A], ", ", NPV[A], ": Pr(G=1|A=1), Pr(G=0|A=0)")),
"PPA / NPA (inter-assay agreement)",
"p = Pr(CTA+ | CDx+) (transport weight)"), "#E3F1F7", "gray15", cex = 0.8, border = "#2E86AB", lwd = 1.6)
draw_arr(3.1, 6.85, 3.1, 5.35)
# ⑥ Outcome branch → two building blocks
cellbox(10.9, 4.5, 5.7, 1.6, list(
"Two building blocks (within column: treat − control)",
expression(paste(delta[pos], " = ", mu["11"], " − ", mu["01"], " (true marker+ effect)")),
expression(paste(delta[neg], " = ", mu["10"], " − ", mu["00"], " (true marker− effect)"))),
"#F7E6EF", "gray15", cex = 0.8, border = "#A23B72", lwd = 1.6)
draw_arr(10.9, 6.85, 10.9, 5.35, "subtract along T", cex = 0.8, adj_y = 0.25)
# ⑦ Convergence: efficacy mixing (PPV / p as weights)
cellbox(7, 1.5, 10.6, 2.0, list(
"Treatment-effect mixing: two building blocks weighted by the relevant PPV or p",
expression(paste(eta, " = ", PPV[f], " · ", delta[pos], " + (1−", PPV[f], ") · ", delta[neg], " → FCD(=CDx)+ population effect")),
expression(paste(theta, " = ", PPV[c], " · ", delta[pos], " + (1−", PPV[c], ") · ", delta[neg], " → CCD+ population effect")),
expression(paste(delta[CDx], " = p · ", delta["11"], " + (1−p) · ", delta["01"], " → bridging target (chapter 4)"))),
"#FBF3DE", "gray15", cex = 0.86, border = "#E8871E", lwd = 1.6)
draw_arr(9.9, 3.7, 8.6, 2.5) # building blocks → mixing
draw_arr(4.3, 3.7, 5.4, 2.5, "PPV / p as weights", cex = 0.82, adj_y = 0.30) # weights across branches → mixingThe quantities have three roles. Specified
parameters—\(\pi\), each
assay’s sensitivity and specificity, and \(\delta_{\text{pos}}\) and \(\delta_{\text{neg}}\)—are entered directly
in TRUTH. Closed-form targets—PPV/NPV,
PPA/NPA, \(p\), \(\eta\), and \(\theta\)—are derived from those parameters
without sampling. Simulation estimates are empirical
counterparts computed from generated data. Comparing the last two
validates the DGP implementation. Randomization makes \(T\) independent of \(G\), which is why each \(\mu\) is indexed by both variables and each
treatment effect is a within-marker contrast across \(T\). # Bridging: Missing Cells and
Treatment Effect Transport
With the foundation in place, we turn to bridging. The trial already contains outcome data—either an arm contrast from an RCT or a response rate from a single-arm study—but those data apply to the CTA-defined enrollment population. The task is to transport that evidence to the population defined by the final CDx. Because the two assays do not select identical populations, one clinically relevant cell has no outcome data. Follow-on development uses the same broad logic with a different missing-data structure: it has no new efficacy trial and instead borrows evidence through agreement with an approved CCD.
As discussed earlier, bridging is needed when the final CDx differs from the assay used to enroll the pivotal trial. Regulatory guidance describes the use of original trial specimens and a prespecified analysis to compare the CTA with the candidate CDx and extrapolate clinical performance to the final assay. Later methodological work addresses practical complications such as missing specimens, local testing, prescreening, and low-prevalence biomarkers (U.S. Food and Drug Administration 2016; Song et al. 2024).
First fix the meaning of the subscripts. In the foundation, \(\delta_{\text{pos}}\) and \(\delta_{\text{neg}}\) index strata of the true marker. Here the two digits in \(\delta_{ab}\) index CTA status first and CDx status second. They do not have the same meaning as the subscripts of \(\mu_{TG}\), which index treatment arm and true marker status. Thus \(\delta_{11}\) is the treatment-minus-control contrast in the CTA+/CDx+ cell, and \(\delta_{01}\) is the corresponding contrast in the CTA−/CDx+ cell. The treatment-arm index does not appear because it has already been absorbed by the contrast. Unlike the foundation, this analysis partitions patients by observed assay calls rather than by the unobserved \(G\).
The core analysis has three steps:
Additionally there is one optional, trigger-based analysis, separate from step 3:
Standard bridging has one structurally missing cell and therefore one \(c\). The advanced examples add prescreening or multiple CTAs, creating additional structurally missing cells and additional sensitivity parameters. Step 4 remains a separate, optional analysis of missing assay measurements. The worked example covers both: Step 3 in the three-step DGP analysis, and Step 4—including the mechanics of imputation and the consequences of MAR versus MNAR—in the subsequent sections. Operationally, the final CDx should be run on all available CTA+ trial specimens and as many CTA− screening specimens as possible.
Problem: We want the treatment effect \(\delta_{CDx}\) in the CDx+ population, but CTA−/CDx+ patients were not enrolled. Under an enrichment design, retrospective CDx testing reveals the observed CTA+ cells but cannot create outcome data for the unobserved CTA−/CDx+ subgroup:
par(family = "cjk", mar = c(0.5, 0.5, 3, 0.5))
plot(NA, xlim = c(0, 16), ylim = c(-1.1, 7.4), axes = FALSE, xlab = "", ylab = "",
main = "Standard bridging full tree: CTA × CDx × arm = 8 leaves, missing marked with X", cex.main = 1.2)
lb <- "#AED6F1"; lbb <- "#7FB3D5" # Not enrolled: light blue + dark border
xm <- function(x, y, col, hw = 0.9, hh = 0.23) { # X: mark "does not exist"
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, "Screening\nPopulation", "gray30", w = 1.8, h = 0.9, cex = 0.85)
# ① First partition by CTA (enrollment)
draw_box(3.8, 5.45, "CTA+\nEnrolled", "#2E86AB", w = 1.9, h = 0.8, cex = 0.85)
draw_box(3.8, 1.85, "CTA−\nNot Enrolled", 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)
# ② Then backfill CDx → four cells 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)
# ③ Each cell further split into treatment/control → 8 leaves
arm(6.6, "#2E86AB", "Treatment T=1"); arm(5.9, "gray60", "Control T=0")
arm(5.0, "gray60", "Treatment T=1"); arm(4.3, "gray72", "Control T=0")
arm(3.0, lb, "Treatment T=1"); arm(2.3, lb, "Control T=0")
arm(1.4, lb, "Treatment T=1"); arm(0.7, lb, "Control 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], " Estimable")), col = "#2E86AB", cex = 0.9, font = 2)
brace(11.0, 2.3, 3.0); text(12.6, 2.65, bquote(paste(delta["01"], " Missing")), col = "gray30", cex = 0.9, font = 2)
text(11.5, 4.65, "c,d = CDx−(not transported)", 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) # Red X: entire CTA− branch
xm(10.0, 5.9, "#E8871E"); xm(10.0, 4.3, "#E8871E") # Orange X: single-arm only deletes control (CTA+ branch control T=0)
segments(0.5, 7.2, 1.0, 7.2, col = "#C73E1D", lwd = 3); text(1.15, 7.2, "Red X = CTA− enrichment not enrolled (trade-off #1)", col = "#C73E1D", cex = 0.72, font = 2, adj = 0)
xm(0.75, 6.8, "#E8871E", 0.25, 0.13); text(1.15, 6.8, "Orange X = single-arm drops control (trade-off #2)", col = "#E8871E", cex = 0.72, font = 2, adj = 0)
text(8, -0.4, expression(paste("Target CDx+ = a+b. RCT: cell a has both arms → ", delta[11], " estimable; single-arm drops control → δ = response rate. b (CTA− not enrolled) → ", delta["01"], " missing.")),
col = "gray20", cex = 0.74, font = 2)
text(8, -0.85, expression(paste(delta[CDx], " = p·", delta[11], " + (1−p)·", delta["01"], ", p = a/(a+b)")), col = "gray20", cex = 0.74, font = 2)\[\delta_{CDx} = p\,\delta_{11} + (1-p)\,\delta_{01},\qquad p=\Pr(\text{CTA}{+}\mid \text{CDx}{+})\]
Arm contrast versus single-arm response rate. \(\delta_{11}\) is an arm contrast and requires a control group; it answers how much better the experimental treatment performs than the comparator. A single-arm ORR instead measures the proportion of treated patients who respond. It is an absolute level, not a comparative effect, and must be judged against a prespecified historical benchmark. The design considerations and limitations of that argument were discussed under Trade-off Two.
Writing the treatment-arm dimension explicitly clarifies the relationship. Let \(\mu_{T;\,11}\) denote the outcome mean in treatment arm \(T\in\{0,1\}\) within the CTA+/CDx+ cell. Then \(\delta_{11}=\mu_{1;11}-\mu_{0;11}\). The treatment-arm index is suppressed in \(\delta_{11}\) because differencing has absorbed it, but the control mean \(\mu_{0;11}\) remains part of the estimand.
A single-arm design observes only \(\mu_{1;11}\). Treating that response level as an effect requires the assumption that the relevant control response level is negligible, \(\mu_{0;11}\approx0\). For an ORR-type endpoint, this is the premise that spontaneous objective responses are rare, so \(\delta_{11}=\mu_{1;11}-\mu_{0;11}\approx\mu_{1;11}\). The DGP makes the same simplifying choice by setting the control-arm means to zero, so \(\delta_{\text{pos}}=\mu_{11}\) and \(\delta_{\text{neg}}=\mu_{10}\).
In short, an arm difference is relative to a control, whereas a response rate is an absolute level in treated patients. Both are mean-type quantities: a response rate is the mean of a binary response indicator. The transport identity therefore remains exact if every \(\delta\) is replaced by an ORR. A hazard ratio cannot be substituted in this way because it is non-collapsible; see “Why Use Continuous Endpoints”.
In the CDx × CTA 2×2 agreement table, \(a,b,c,d\) are patient counts, not probabilities. For example, \(a\) is the number positive on both assays and \(d\) the number negative on both; the four cells sum to the total sample size.
| CTA+ | CTA- |
|---|---|---|
CDx+ | a | b |
CDx- | c | d |
Two directional agreement rates can be read directly from the table (rows = CDx, columns = CTA):
\[PPA=\Pr(\text{CDx}{+}\mid \text{CTA}{+})=\frac{a}{a+c},\qquad NPA=\Pr(\text{CDx}{-}\mid \text{CTA}{-})=\frac{d}{b+d}\]
PPA is the proportion of CDx+ results within the CTA+ column, and NPA is the proportion of CDx− results within the CTA− column. A third metric, overall percent agreement (OPA), is the proportion for which the two assay calls agree:
\[OPA=\Pr(\text{CDx}{=}\text{CTA})=\frac{a+d}{a+b+c+d}\]
OPA depends strongly on the marginal prevalence of positive and negative calls. When one class dominates, OPA can look high even if agreement in the smaller class is poor. Regulatory reports therefore emphasize PPA and NPA separately and use OPA only as a summary. The transport analysis derives \(p\) from PPA, NPA, and prevalence; OPA is included only for completeness.
The same CTA×CDx grid carries two kinds of quantities. \(a,b,c,d\) count patients; \(\delta\) denotes the treatment effect within a cell. The effect table below uses the same row and column arrangement as the count table:
| CTA+ | CTA- |
|---|---|---|
CDx+ | δ11 | δ01 |
CDx- | δ10 | δ00 |
Cell \(a\) has effect \(\delta_{11}\), cell \(b\) has \(\delta_{01}\), cell \(c\) has \(\delta_{10}\), and cell \(d\) has \(\delta_{00}\). Bridging targets only the CDx+ row: \(\delta_{11}\) is observed and \(\delta_{01}\) is missing. Weighting the two effects by their cell counts gives
\[\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(\text{CTA}{+}\mid \text{CDx}{+}).\]
(The CDx− row \(\delta_{10}\), \(\delta_{00}\) are not transport targets, not used here.)
Bayes-derived weight \(p=\Pr(\text{CTA}{+}\mid \text{CDx}{+})\):
\[p=\frac{PPA\cdot prev}{PPA\cdot prev+(1{-}NPA)(1{-}prev)},\]
where \(prev=\Pr(\text{CTA}{+})\) must refer to the intended-use population. The calculation below matches this expression with the direct count \(a/(a+b)\).
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) # Construct table from given PPA/NPA/prev
ft_show(data.frame(`p (Bayes formula)` = round((PPA_t*prev)/(PPA_t*prev+(1-NPA_t)*(1-prev)), 4),
`p (direct from table)` = round(a/(a+b), 4), check.names = FALSE),
title = "Bayes-derived p matches the direct 2×2-table estimate")p (Bayes formula) | p (direct from table) |
|---|---|
0.72 | 0.72 |
We now apply the three steps to the known-truth DGP. Each estimate is compared with its closed-form or simulation target, separating quantities identified by data from the assumption needed to identify \(\delta_{CDx}\).
Step 1 · Agreement → transport weight \(p\). Use a representative, unenriched screening sample containing both CTA and CDx results, including CTA− specimens, to estimate PPA and NPA. Then derive \(p=\Pr(\text{CTA}{+}\mid \text{CDx}{+})\) by Bayes’ theorem.
scr <- generate_population(3e5) # Representative screening sample (not enriched, has 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+) = transport weight
tc <- true_concordance(TRUTH$assays$CTA, TRUTH$assays$CDx)
ft_show(data.frame(
`Quantity` = c("PPA_{CTA,CDx}", "NPA_{CTA,CDx}", "p = Pr(CTA+|CDx+)"),
`Estimate` = round(c(PPA_hat, NPA_hat, p_hat), 4),
`Truth` = round(c(tc[["PPA"]], tc[["NPA"]], true_p_cta_given_cdx()), 4),
check.names = FALSE), title = "Step 1: agreement and transport weight p — estimate vs target")Quantity | Estimate | Truth |
|---|---|---|
PPACTA,CDx | 0.7558 | 0.7540 |
NPACTA,CDx | 0.9005 | 0.8993 |
p = Pr(CTA+|CDx+) | 0.7855 | 0.7829 |
Step 2 · Treatment effect → observed cell \(\delta_{11}\). The enriched trial enrolls only CTA+ patients. Estimate \(\delta_{11}\) directly within the observed CTA+/CDx+ cell as treatment minus control.
trial <- generate_population(2e5, with_endpoint = TRUE) # Enriched trial (analysis uses only CTA+)
d11_hat <- delta_hat(subset(trial, CTA==1 & CDx==1)) # Observed cell efficacy
big2 <- generate_population(3e6, with_endpoint = TRUE) # God's-eye view: compute truth
true_d11 <- delta_hat(subset(big2, CTA==1 & CDx==1))
ft_show(data.frame(`δ_{11} estimate` = round(d11_hat,4), `δ_{11} truth` = round(true_d11,4),
check.names = FALSE), title = "Step 2: CTA+/CDx+ treatment effect δ_{11} — estimate vs target")δ11 estimate | δ11 truth |
|---|---|
0.6103 | 0.5952 |
Step 3 · Sensitivity analysis for the missing cell → \(\delta_{CDx}\). Because CTA−/CDx+ patients were not enrolled, \(\delta_{01}\) cannot be estimated. Define \(c=\delta_{01}/\delta_{11}\), so that \(\delta_{CDx}=(p+(1-p)c)\,\delta_{11}\). We insert the estimated \(p\) and \(\delta_{11}\) from Steps 1 and 2, vary \(c\), and then use the DGP’s true \(c\) to check recovery of the true \(\delta_{CDx}\).
true_d01 <- delta_hat(subset(big2, CTA==0 & CDx==1)) # God's-eye view: true efficacy in missing cell
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("Sensitivity c = ", delta["01"], " / ", delta["11"])),
ylab = expression(paste("Bridged ", delta[CDx])),
main = expression(paste("Step 3: Use the DGP's c to recover target ", 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("True ", delta[CDx]), paste("True c")),
col = c("#2E86AB","#C73E1D","gray40"), lty = c(1,2,3), lwd = 2, bty = "n", cex = 0.8)ft_show(data.frame(`δ_{01} truth` = round(true_d01,4), `c truth` = round(c_true,4),
`δ_{CDx} (plug in true c)` = round((p_hat+(1-p_hat)*c_true)*d11_hat,4), `δ_{CDx} truth` = round(true_dCDx,4),
check.names = FALSE), title = "Step 3: using the DGP's c recovers the target δ_{CDx}")δ01 truth | c truth | δCDx (plug in true c) | δCDx truth |
|---|---|---|---|
0.3935 | 0.6611 | 0.566 | 0.5508 |
✓ All three checks agree. Step 1 recovers PPA, NPA, and \(p\); Step 2 recovers \(\delta_{11}\); and in Step 3 the estimate evaluated at the true \(c\) falls on the true \(\delta_{CDx}\). The data do not identify \(\delta_{01}\), and therefore do not identify \(c\). This is the single assumption-driven quantity in the standard bridging pipeline. When smaller values of \(c\) are used as conservative scenarios, the bridged effect falls below the effect observed in the enriched trial; the amount of attenuation depends on both \(p\) and \(c\).
The first three steps address a structurally missing population cell. Step 4 concerns a different problem: the patient is enrolled, but the CDx result is missing because a specimen was not collected or sent, was insufficient, or produced an invalid result after degradation. This optional, trigger-based analysis is separate from Step 3. Reviews of clinical bridging likewise identify specimen retention and missing results as major practical challenges (Song et al. 2024).
This section uses a single-arm response rate: \(R_1\) is the response rate in the observed CTA+/CDx+ cell, such as a CR/CRh rate. Because a response rate is a mean of a binary indicator, the same transport identity applies:
\[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"
## Left: Structural missing—entire cell empty
plot(NA, xlim = c(0, 10), ylim = c(0, 8), axes = FALSE, xlab = "", ylab = "",
main = "Structural missing (Step 3): entire cell empty", cex.main = 1.15)
text(3.9, 7.0, "CTA+", col = "gray25", font = 2, cex = 0.95) # Column header
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) # Row header
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) # Draw X first, text on top
segments(5.8, 6.4, 8.8, 4.6, col = adjustcolor("#C73E1D", 0.5), lwd = 3)
text(3.9, 5.5, "a Observed", col = "white", font = 2, cex = 0.95)
rect(6.25, 5.22, 8.35, 5.78, col = adjustcolor("white", 0.88), border = NA) # Underlay white for X
text(7.3, 5.5, "b Not Enrolled", 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 are CDx− row, not transport targets", col = "gray45", cex = 0.78, font = 3)
text(5.0, 1.4, "Cell b: no data at all — no covariates, no outcomes.", col = "gray20", cex = 0.8, font = 2)
text(5.0, 0.6, "→ Treatment effect not identified; represent it with sensitivity parameter c", col = "#C73E1D", cex = 0.82, font = 2)
## Right: Measurement missing—only one cell missing
plot(NA, xlim = c(0, 10), ylim = c(0, 8), axes = FALSE, xlab = "", ylab = "",
main = "Measurement missing (Step 4): only one cell missing", cex.main = 1.15)
hdr <- c("Covariate X", "CTA", "CDx", "Outcome")
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, "Age / Stratum / Disease Status", 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) "Response" else "No Response", col = "gray30", cex = 0.72)
}
text(5.0, 1.4, "Other columns in the row are present → learn the relationship, impute", col = "gray20", cex = 0.78, font = 2)
text(5.0, 0.6, "→ Impute conditional distribution in this cell (multiple imputation)", col = org, cex = 0.9, font = 2)The distinction is whether any patient-level information is available. Under structural missingness, the cell contains no enrolled patients and therefore no rows of covariates or outcomes; \(\delta_{01}\) (or \(R_2\) on the response-rate scale) is not identifiable and must be represented through \(c\). Under measurement missingness, the patient row remains: demographics, baseline variables, CTA result, and outcome are observed, while only the CDx result is absent. Relationships learned from complete cases can therefore inform an imputation model.
Imputation represents a distribution, not an unknowable truth. For a patient with a missing CDx result, we draw plausible values from a conditional distribution, repeat that process across multiple imputed datasets, and propagate the resulting between-imputation variation into the analysis. That variation is part of the inferential uncertainty, which is the key advantage of multiple imputation over a single deterministic fill-in (Rubin 1987; Little and Rubin 2019).
The workflow is to quantify the missing-or-invalid proportion by cohort and trigger MI only if a prespecified threshold is exceeded (10% is used here as an example). Above the threshold, agreement and treatment effect are re-estimated from the completed datasets; below it, the observed results are analyzed directly. The threshold is not a theorem. It is an operational risk-based rule that belongs in the statistical analysis plan so that the decision is made before results are seen.
Let \(R\) be the missingness indicator (\(R=1\) means the CDx result is missing), \(Z\) the potentially missing CDx result, and \(X\) the observed covariates:
Mechanism | Definition | Interpretation | Complete-case analysis | MAR-based MI |
|---|---|---|---|---|
MCAR | Pr(R=1 | Z, X) = Pr(R=1) | Missingness is unrelated to observed or unobserved data | Unbiased | Usually unnecessary |
MAR | Pr(R=1 | Z, X) = Pr(R=1 | X) | Missingness may depend on X but, conditional on X, no longer depends on Z | Generally biased if selection changes the distribution of X | Can address the bias if the relevant X variables are included |
MNAR | Pr(R=1 | Z, X) still depends on Z | Missingness still depends on the unobserved result Z (e.g., CDx− samples are more likely to be invalid) | Biased | Cannot identify the bias without additional assumptions |
MAR is an assumption, not an empirically testable fact. Because the missing values do not appear in the observed-data likelihood, an MAR model and some MNAR models can fit the observed data equally well. The data alone cannot distinguish them. In this sense, Steps 3 and 4 place assumptions at different points in the pipeline: statistics can state those assumptions and quantify their consequences, but cannot eliminate them.
Operational information can make MNAR more plausible. For example, insufficient tissue or low tumor content may cause invalid results and may also be related to marker status. The final part of this section quantifies the consequences of such a mechanism.
Practical approach: demographics + clinical baseline (age, sex, race, risk stratum, disease status). Two deliberate choices:
If baseline covariates have only sparse missingness, a pragmatic implementation can use a “not reported” category for categorical variables or median imputation for continuous variables rather than adding another full imputation layer. That simplification should be documented because it treats covariate missingness differently from the CDx missingness of primary interest.
The missingness considered here occurs among enrolled CTA+ patients. The CTA− column usually comes from external negative specimens collected to estimate NPA and is assumed complete. Retain those specimens in the analysis because NPA depends on them, but do not impute values in a complete column. If a CTA− specimen truly lacks a CDx result, exclude it from that calculation and report the number excluded.
mice’s logreg (logistic regression
imputation) core has just three steps:
\[\text{① Fit (only people with readings):}\quad \operatorname{logit}\Pr(\text{CDx}{=}1\mid X)=X^{\top}\hat\beta\]
\[\text{② Draw coefficients:}\quad \beta^{*}=\hat\beta+L^{\top}z,\qquad L^{\top}L=\widehat{\operatorname{Cov}}(\hat\beta),\quad z\sim N(0,I)\]
\[\text{③ Draw readings:}\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)\]
The two draws represent different sources of uncertainty. Step ② represents parameter uncertainty in the fitted regression coefficients. Step ③ represents individual residual uncertainty: two patients with the same predicted probability, say \(\pi=0.7\), can receive different binary imputations. Omitting the coefficient draw and treating \(\hat\beta\) as known produces improper imputation and understates between-imputation variance. Omitting both draws and assigning \(1\) whenever \(\pi>0.5\) makes every completed dataset identical, reducing MI to deterministic single imputation with zero between-imputation variance. The example below compares all three schemes.
Each completed dataset must repeat Steps 1–3 in full. An imputed CDx result affects three parts of the calculation:
The three contributions can move in different directions, so the net effect must be calculated rather than inferred from PPA alone.
\[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\) cancels completely. The \(c=1\) row is therefore exactly the response rate in the CTA+/CDx+ cell and should match the main analysis. If a sensitivity table includes only \(c=1\), PPA, NPA, \(p\), and the variance of \(p\) all disappear algebraically; that is a property of the estimand, not a computational error.
Under Rubin’s rules, \(m=20\)–\(50\) is often adequate, although the required number depends on the fraction of missing information and the desired precision; the relative efficiency is \((1+\gamma/m)^{-1}\) (Rubin 1987). Descriptive tables sometimes report Min/Q1/Median/Q3/Max, with or without the mean, across the \(m\) point estimates. Quartiles stabilize quickly, but Min and Max are extreme-order statistics and continue to move as \(m\) grows. If those extremes are reported, hundreds or thousands of imputations may be needed merely to reduce their Monte Carlo jitter. Even then, they mean only “the smallest and largest of these \(m\) analyses,” not inferential limits. The figure below traces this behavior.
Because the DGP retains every simulated CDx result, it provides a complete-data benchmark that is unavailable in practice. We compare that benchmark with complete-case analysis and two imputation specifications to determine which methods recover it.
For this section, the DGP adds baseline covariates associated with
true marker status and a binary response for the single-arm analysis.
Assay performance, prevalence, and treatment effects remain those
specified in 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 # Single-arm: treatment group only
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), # Covariate①: true marker+ older age
RISK = rbinom(n, 1, 0.25 + 0.35 * G), # Covariate②: true marker+ more high-risk stratum
RESP = as.integer(Y > 1.0)) # Binary response (CR/CRh style)
}
# One trial = CTA+ n1 people enrichment-enrolled (readings + outcomes complete) + external negative n0 people (only for NPA)
make_trial <- function(n1 = 150, n0 = 100) {
pool <- gen_mi(4e4)
neg <- pool[pool$CTA == 0, ][seq_len(n0), ]; neg$RESP <- NA_integer_ # Negative material no outcomes
rbind(pool[pool$CTA == 1, ][seq_len(n1), ], neg)
}
PREV_CTA <- unname(assay_prev(TRUTH$assays$CTA)) # Pr(CTA+) in intended-use population, for Bayes weightThe following function combines Steps 1–3 and is rerun for every completed dataset:
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 four cells
C <- sum(cdx == 0 & cta == 1); D <- sum(cdx == 0 & cta == 0)
ppa <- A/(A + C); npa <- D/(B + D) # Step 1: Concordance
R1 <- if (A > 0) mean(resp[cdx == 1 & cta == 1]) else NA_real_ # Step 2: Observed cell response rate
p <- (ppa*prev)/(ppa*prev + (1 - npa)*(1 - prev)) # Bayes weight
c(PPA = ppa, NPA = npa, p = p, R1 = R1, A = A, # Step 3: Sweep c
setNames((p + (1 - p)*c_values)*R1, paste0("c", c_values)))
}Two missingness mechanisms are simulated. Under MAR,
missingness depends only on observed covariates (older and higher-risk
patients are more likely to have missing results). Under
MNAR, it also depends on the unobserved CDx result
(CDx− results are more likely to be invalid). The imputation function
follows the two-draw scheme above; mode determines which
draws are retained.
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) # ① Only people with readings
b <- coef(fit)
bst <- if (mode == "proper") # ② Draw β*
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) # Deterministic fill: don't draw
else as.integer(runif(length(pr)) <= pr) # ③ Draw reading
}
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) # Each complete dataset re-run entire chain
}, 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) Drawing coefficients: β* curve family
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) # Slice RISK=0, CTA=1
V <- t(chol(summary(fit1)$cov.unscaled))
plot(NA, xlim = range(ag), ylim = c(0, 1), main = "① Fit + ② Draw coefficient β*",
xlab = "Age (RISK=0, CTA+ slice)", 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("Complete case fit", "40 β* draws", "Missing age"),
col = c("#2E86AB", "gray55", "#E8871E"), lwd = c(3, 1, 2), cex = 0.85,
bg = adjustcolor("white", 0.88), box.col = NA)
## (b) Three fill methods' cross-imputation spread
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 = "Bridged-rate distributions under three imputation schemes",
xlab = expression(paste("Bridged response rate ", R[CDx], " (c = 0.5)")),
ylab = "Density", 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 (draw β*+reading) SD %.4f", sd(mi_pro[, "c0.5"])),
sprintf("Improper (only draw reading) SD %.4f", sd(mi_imp[, "c0.5"])),
"Deterministic fill 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) Five-number summary drift with 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), # Top space for legend
main = "Five-number summary drift with m", xlab = "Imputation count m (log axis)",
ylab = expression(paste("Bridged response rate ", R[CDx], " (c = 0.5)")),
cex.main = 1.1, cex.lab = 1.05)
legend("topleft", c("Min / Max (keep climbing)", "Q1 / Q3", "Median"),
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)The left panel visualizes the coefficient draw. Complete cases produce the fitted curve in blue; each draw of \(\beta^{*}\) produces a gray alternative. The width of the gray band represents parameter uncertainty, so the predicted probability \(\pi_i\) for a patient with a missing result changes across imputations.
The middle panel shows the consequence of the three schemes. Proper MI has the widest distribution; improper MI is narrower (its variance is 12% smaller in this dataset, with larger differences expected as missingness and model complexity increase). Deterministic imputation collapses to a vertical line: between-imputation uncertainty has been forced to zero.
The right panel shows the role of \(m\). The median and quartiles stabilize by roughly \(m=50\), while the dashed Min and Max curves continue to move. Thousands of imputations reduce their Monte Carlo jitter, but do not turn them into confidence limits.
In a single trial, sampling noise obscures the bias: with \(n=150\), the sampling SD of PPA is about 0.035. We therefore simulate 150 trials. Within each trial, every incomplete-data analysis is paired with that trial’s own complete-data estimate, and the mean paired difference estimates bias.
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] # Benchmark: no missing
cc <- bridged_rate(d$CDx[!mi], d$CTA[!mi], d$RESP[!mi], cf)[keys] # Discard missing
mx <- colMeans(draw_imp(d, mi, FALSE, m, cf))[keys] # Impute: covariates only
mc <- colMeans(draw_imp(d, mi, TRUE, m, cf))[keys] # Impute: + CTA reading
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(
`Method` = c("Discard missing (complete case)", "Impute: covariates only", "Impute: covariates + CTA reading"),
`Δ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])),
`Δ Bridged Rate (c=0.5)` = sprintf("%+.5f", c(g("cc")[4], g("mx")[4], g("mc")[4])),
`MC Std Error` = sprintf("%.5f", c(se("cc"), se("mx"), se("mc"))),
check.names = FALSE)
}
ft_show(bias_tbl(rep_mar), title = "MAR mechanism paired bias (150 trial replications, each m = 20)",
footers = sprintf(paste("Benchmark = full-data estimate from same trial: PPA %.4f, bridged rate %.4f; ",
"mean missing rate %.1f%% (among enrolled CTA+). Δ = that method − full-data."),
mean(rep_mar[, "full.PPA"]), mean(rep_mar[, "full.c0.5"]),
100*mean(rep_mar[, "rate"])))Method | ΔPPA | Δp | ΔR1 | Δ Bridged Rate (c=0.5) | MC Std Error |
|---|---|---|---|---|---|
Discard missing (complete case) | -0.0204 | -0.0047 | -0.0032 | -0.00364 | 0.00149 |
Impute: covariates only | -0.0208 | -0.0047 | -0.0020 | -0.00261 | 0.00056 |
Impute: covariates + CTA reading | +0.0009 | +0.0000 | -0.0024 | -0.00211 | 0.00048 |
Benchmark = full-data estimate from same trial: PPA 0.7523, bridged rate 0.3030; mean missing rate 16.6% (among enrolled CTA+). Δ = that method − full-data. | |||||
Reading down the three rows:
The most informative feature is how bias propagates across the columns. A PPA bias of 0.020 becomes a bias of only 0.005 in the Bayes-derived weight \(p\), and is then multiplied by \((1-c)R_1\). The contribution of PPA bias to the final bridged rate is therefore small. The chain derivative makes the attenuation explicit:
\[\frac{\partial R_{CDx}}{\partial\,\text{PPA}}=\underbrace{(1-c)\,R_1}_{\text{outcome path}}\times\underbrace{\frac{\partial p}{\partial\,\text{PPA}}}_{\text{Bayes path}},\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) # This data's quantities
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 ; combined dR_CDx/dPPA = %.4f\n",
dp_dppa, (1 - 0.5)*b0[["R1"]], slope))## dp/dPPA = 0.203 ; (1-c)*R1 = 0.140 ; combined dR_CDx/dPPA = 0.0284
w_R1 <- b0[["p"]] + (1 - b0[["p"]])*0.5 # R1 path coefficient
cat(sprintf("→ PPA biases %.3f carries %.4f; R1 biases %.4f carries %.4f; total %.4f, measured %.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 biases 0.020 carries 0.0006; R1 biases 0.0032 carries 0.0028; total 0.0034, measured 0.0036
The equation contains two attenuation steps. First, \(p\) changes moderately with PPA (slope 0.20). Second, the effect on the bridged rate is multiplied by $(1-c)R_1=0.14. Together they reduce the PPA perturbation by roughly a factor of 35. Apparent robustness to agreement bias here is therefore a property of the estimand, not a special corrective power of MI.
The \(R_1\) pathway is much less attenuated. Its coefficient is \(p+(1-p)c\approx0.89\), so bias in \(R_1\) is transmitted almost directly to the bridged rate. Of the observed bridged-rate bias 0.0036, only 0.0006 comes through PPA; the remainder comes through \(R_1\). An agreement table alone can therefore understate the impact of missingness. The more important question is whether missing CDx results change the response composition of the CTA+/CDx+ group.
There are two ways to summarize the \(m\) estimates, and they answer different questions:
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-th estimate point
U[i] <- var(vapply(seq_len(B_r), function(b) # i-th within variance
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 rule
ft_show(data.frame(
`Quantity` = c("θ̄ (mean of m estimates)", "W̄ (within variance: sampling uncertainty)",
"B (between variance: imputation uncertainty)", "T = W̄ + (1+1/m)·B (total variance)",
"Missing info ratio FMI = (1+1/m)B / T", "Rubin 95% interval", "Five-number Min–Max range"),
`Value` = 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] (width %.4f)", mean(th) - 1.96*sqrt(Tot),
mean(th) + 1.96*sqrt(Tot), 2*1.96*sqrt(Tot)),
sprintf("[%.4f, %.4f] (width %.4f)", min(th), max(th), max(th) - min(th))),
check.names = FALSE),
title = "Two summaries of the same analyses: Rubin's rules vs the across-imputation range",
footers = sprintf("m = %d imputations; within-imputation variance was estimated with %d bootstrap resamples per completed dataset.", m_r, B_r))Quantity | Value |
|---|---|
θ̄ (mean of m estimates) | 0.2450 |
W̄ (within variance: sampling uncertainty) | 1.336e-03 |
B (between variance: imputation uncertainty) | 3.462e-05 |
T = W̄ + (1+1/m)·B (total variance) | 1.371e-03 |
Missing info ratio FMI = (1+1/m)B / T | 2.5% |
Rubin 95% interval | [0.1724, 0.3176] (width 0.1451) |
Five-number Min–Max range | [0.2301, 0.2573] (width 0.0272) |
m = 200 imputations; within-imputation variance was estimated with 200 bootstrap resamples per completed dataset. | |
The two summaries are not interchangeable. \(\bar W\) represents sampling uncertainty in this cohort of 150 patients, while \(B\) represents additional uncertainty from the missing values. Here, the between-imputation component contributes only 2.5% of the total variance.
Now let missingness also depend on the CDx result itself, with CDx− results more likely to be invalid, and rerun the same analyses:
Method | ΔPPA | Δp | ΔR1 | Δ Bridged Rate (c=0.5) | MC Std Error |
|---|---|---|---|---|---|
Discard missing (complete case) | +0.0542 | +0.0113 | +0.0014 | +0.00319 | 0.00089 |
Impute: covariates only | +0.0343 | +0.0074 | -0.0056 | -0.00380 | 0.00061 |
Impute: covariates + CTA reading | +0.0593 | +0.0123 | -0.0088 | -0.00585 | 0.00070 |
Missing probability additionally depends on CDx reading itself. Benchmark = full-data estimate: PPA 0.7537, bridged rate 0.3065; mean missing rate 13.4%. | |||||
PPA is now biased upward because CDx− results are preferentially missing, leaving an observed sample enriched for CDx+. MAR-based imputation does not remove the bias: the covariate-plus-CTA analysis has bias +0.0593, similar in order to the complete-case bias +0.0542. The model learns from an observed subset whose relationships have already been distorted by outcome-dependent selection and carries that distortion into the imputations.
The bridged-rate bias has the opposite sign: PPA is biased upward, but the final rate is biased downward. Imputed CDx results affect both the numerator and denominator of \(R_1\); in this scenario they add response-poor patients to the CTA+/CDx+ group and lower \(R_1\), opposing the pathway through \(p\). This is why the net effect must be calculated rather than inferred from one intermediate statistic.
Addressing MNAR requires another untestable assumption—for example, adding a \(\Delta\) offset to the imputation logit and varying it in a pattern-mixture sensitivity analysis. Like \(c\) in Step 3, this is a sensitivity parameter that the observed data do not identify.
# c=1 identity self-check: p must vanish, else table inconsistent with main analysis double-positive rate.
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("✓ At c=1 bridged rate %.6f = double-positive rate R1 %.6f (p vanishes); at c=0 drops to %.6f\n",
chk[["c1"]], chk[["R1"]], chk[["c0"]]))## ✓ At c=1 bridged rate 0.280000 = double-positive rate R1 0.280000 (p vanishes); at c=0 drops to 0.219754
What Step 4 can provide: under MAR, an estimate of what the analysis would have shown if all enrolled patients had CDx results, together with a measure of sensitivity to the missing results and the \(c=1\) identity check.
What it cannot provide: a test of MAR against all MNAR alternatives, an unbiased estimate under an unspecified MNAR mechanism, or a substitute for preventing missingness through better specimen collection and retesting procedures. Step 3’s \(c\) and Step 4’s MAR assumption play analogous roles at different points: the analysis makes assumptions explicit and quantifies their consequences; it does not make them disappear. # Follow-on: Another Type of Missing Structure (Agreement-based Transport)
We now turn from bridging to follow-on CDx development, which shares the same statistical skeleton but has a different observed-data structure. Bridging begins with outcome data from the drug’s trial. A follow-on program conducts no new efficacy trial: the drug and comparator CDx (CCD) are already approved, and the new follow-on CDx (FCD) must demonstrate sufficient agreement with CCD to support an indirect transport of the CCD-based efficacy evidence. The main task therefore shifts from filling an efficacy-data gap to quantifying assay agreement. The outcome argument relies on the PPV-based dilution relationship established in the foundation. Li (2016) describes statistical frameworks for follow-on CDx validation using external agreement studies.
Question: How closely do FCD and the comparator CDx (CCD) agree when classifying the same specimens? We first define the agreement measures and their directional notation.
The follow-on program has two objectives: Objective 1, show that FCD agrees sufficiently with the approved CCD; and Objective 2, use the CCD-based efficacy evidence to support an acceptable treatment effect in the FCD+ population. A direct strategy reruns or retrospectively retests pivotal-trial specimens with FCD. An indirect strategy combines established CCD efficacy with an external FCD-versus-CCD agreement study. This chapter proceeds from notation to agreement estimation and NI testing, \(p_c\) reweighting in the absence of a reference standard, sample-size simulation, and a three-way-table supplement.
Objective 2 borrows the quantity \(\theta\) defined in the foundation:
\[\theta=PPV_c\,\delta_{\text{pos}}+(1{-}PPV_c)\,\delta_{\text{neg}},\qquad PPV_c=\Pr(G{=}1\mid CCD{=}1).\]
It uses the same two true-marker-specific treatment effects, now weighted by the CCD’s PPV. If FCD and CCD classify specimens sufficiently similarly under Objective 1, the argument is that \(\eta\approx\theta\), allowing the FCD+ population to inherit the efficacy conclusion established for CCD+. The notation remains consistent: \(\mu\) is indexed by true \(G\), \(\eta\) by FCD, and \(\theta\) by CCD.
The comparator is tested twice (CCD1 and CCD2), along with one FCD measurement. This lets us benchmark FCD-versus-CCD agreement against the repeatability of CCD itself.
Coding convention: Each assay result is coded 0 or 1, where 1 denotes a positive call and 0 a negative call. Thus \(CCD1{=}1\) means that the first CCD replicate is positive, while \(FCD{=}0\) means that the new assay is negative.
PPA and NPA quantify directional agreement on the same specimens. With assay A as the reference, positive percent agreement (PPA) is \(\Pr(B{=}1\mid A{=}1)\) and negative percent agreement (NPA) is \(\Pr(B{=}0\mid A{=}0)\). Larger values indicate greater agreement, but the measures are directional: reversing the reference and comparison assays generally changes them (U.S. Food and Drug Administration 2007).
Subscript convention: in a two-part subscript, the first assay is the reference (the conditioning event) and the second is the comparison assay. Let \(C1\) = CCD1, \(C2\) = CCD2, and \(F\) = FCD. Then:
\[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)\]
The NI logic is that FCD-versus-CCD agreement (\(C1F\), \(C2F\)) should not be worse than CCD’s own repeatability (\(C1C2\), \(C2C1\)) by more than a prespecified margin.
This yields four one-sided non-inferiority hypotheses: two for PPA and two for NPA, using CCD1 and CCD2 in turn as the reference. Each \(\xi\) is “CCD repeat agreement minus FCD-versus-CCD agreement”; \(\delta_1\) and \(\delta_0\) are the acceptable PPA and NPA margins.
\[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\]
Rejecting \(H_0\) when the upper confidence bound for the gap is below the margin supports the conclusion that FCD is non-inferior to CCD’s own repeatability.
Question: True \(G\) is unavailable, and the agreement study may be enriched on CCD1. How can agreement be estimated without carrying that enrichment bias into quantities conditioned on CCD2? Naive calculations from the enriched sample are systematically distorted.
We first define the cell counts, then give estimators conditional on CCD1, and finally derive the \(p_c\) reweighting needed for CCD2-conditioned quantities.
Count table (symbolic notation):
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₀ |
Within the CCD1+ stratum, let \(a_1,b_1,c_1,d_1\) denote the (CCD2, FCD) cell counts, with FCD in rows and CCD2 in columns. The marginal counts are:
\[m_1 = a_1 + b_1 \quad(\text{number of FCD+})\] \[n_{11} = a_1 + c_1 \quad(\text{number of CCD2+})\] \[n_1 = n_{11} + n_{10} \quad(\text{total CCD1+})\]
Within the CCD1− stratum, define \(a_0,b_0,c_0,d_0\) analogously. Then:
\[r_0 = c_0 + d_0 \quad(\text{number of FCD−})\] \[n_{00} = b_0 + d_0 \quad(\text{number of CCD2−}),\qquad n_0 = n_{01} + n_{00} \quad(\text{total CCD1−})\]
Quantities conditional on CCD1 can be estimated directly from the within-stratum counts; enrichment changes the stratum sizes but not these conditional proportions:
\[\hat{PPA}_{C1F}=m_1/n_1\quad(\text{proportion among CCD1+ where FCD also positive})\] \[\hat{PPA}_{C1C2}=n_{11}/n_1\quad(\text{proportion among CCD1+ where CCD2 also positive})\] \[\hat{NPA}_{C1F}=r_0/n_0\quad(\text{proportion among CCD1− where FCD also negative})\] \[\hat{NPA}_{C1C2}=n_{00}/n_0\quad(\text{proportion among CCD1− where CCD2 also negative})\]
Sampling by CCD1 distorts the proportion of CCD1+ and CCD1− specimens. Let \(p_c=\Pr(CCD1{=}1)\) be the CCD1 positivity rate in the target population, not the enriched sample proportion. Reweighting the two CCD1 strata by \(p_c\) recovers the four CCD2-conditioned quantities:
\[\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}\]
After dividing numerator and denominator by \(n_1n_0\), each expression can be read as a weighted combination of within-CCD1-stratum sample proportions. The target-population positivity rate \(p_c\), obtained from external data or a justified assumption, replaces the stratum weights distorted by enrichment. The worked example validates all four expressions against known DGP targets.
pc <- assay_prev(TRUTH$assays$CCD) # p_c = Pr(CCD1=1)
tc_fcd <- true_concordance(TRUTH$assays$CCD, TRUTH$assays$CDx) # FCD vs CCD truth
tc_c2c1 <- true_concordance(TRUTH$assays$CCD, TRUTH$assays$CCD) # CCD self-repeat truth
pool <- generate_population(3e5)
enr <- rbind(pool[pool$CCD1==1,][1:2000,], pool[pool$CCD1==0,][1:2000,]) # Enrichment 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(
`Quantity` = c("PPA_{C2F}", "PPA_{C2C1}", "NPA_{C2F}", "NPA_{C2C1}"),
`Truth` = round(unname(truth),4),
`Naive (enrichment bias)` = round(unname(naive),4),
`p_{c} reweighted` = round(unname(rw),4),
check.names = FALSE),
title = "p_{c} reweighting (4 CCD2-conditioned quantities): naive deviates, reweighting recovers truth")Quantity | Truth | Naive (enrichment bias) | pc reweighted |
|---|---|---|---|
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 |
✓ All four reweighted estimators recover their targets within simulation error. Naive estimates from the enriched sample are biased, whereas \(p_c\) reweighting restores the target-population values. Confidence intervals can be constructed with suitable binomial or bootstrap methods; the inferential procedure must match the prespecified one- or two-sided acceptance criterion.
Question: Does FCD agree closely enough with CCD to support replacement? Using a representative sample, we estimate the four agreement quantities, compare them with their DGP targets, and then test whether FCD-versus-CCD agreement is non-inferior to CCD repeatability.
conc <- generate_population(1200) # Representative concordance study
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(
`Agreement Quantity` = c("PPA_{C1F}", "PPA_{C1C2}", "NPA_{C1F}", "NPA_{C1C2}"),
`Estimate` = round(est, 4), `Truth` = round(truth_conc, 4),
check.names = FALSE),
title = "Agreement estimation vs truth (CCD1-conditioned)")Agreement Quantity | Estimate | Truth |
|---|---|---|
PPAC1F | 0.8197 | 0.8309 |
PPAC1C2 | 0.8388 | 0.8162 |
NPAC1F | 0.9329 | 0.9319 |
NPAC1C2 | 0.9209 | 0.9131 |
All four estimates are close to their targets. The code then illustrates the NI procedure with \(\xi_{ppa1}\); the other three hypotheses are handled analogously. Reject \(H_0\) if the one-sided bootstrap upper confidence bound is below \(\delta_1\).
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 = "One-sided NI test (PPA)")Qty | Value |
|---|---|
xi_point | 0.0191 |
ci_upper_95 | 0.0492 |
margin | 0.1000 |
reject_H0 | 1.0000 |
✓ The estimates recover their targets, and the test correctly rejects \(H_0\) for the higher-performing FCD in this DGP.
Question: How many specimens are needed to achieve adequate power for the FCD agreement criterion? Closed-form calculations may be available under simple random sampling from the target population, but practical enrichment and dependence structures often make simulation more appropriate.
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 = "Sample Size Simulation Workflow", cex.main = 1.5)
steps <- c("Specify stratum sample sizes", "Assume PPA/NPA and phi", "Simulate CCD2, FCD results",
"Estimate PPA/NPA + bootstrap CI", "One-sided NI test", "Repeat N times → estimate 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 = "Target n per stratum", ylab = "power", main = "PPA NI Test Power vs Sample Size")
abline(h = 0.8, lty = 3, col = "gray50")Power increases with \(n\); the curve identifies the first simulated sample size at which estimated power reaches 0.80.
A final technical point concerns residual dependence. FCD and CCD2 are conditionally independent given \(G\), but they remain positively associated within the CCD1+ stratum because conditioning on CCD1 does not fully reveal \(G\). The phi coefficient quantifies this dependence and yields the anchor
\[P_{a1}=\varphi_1\sqrt{PPA_{C1C2}PPA_{C1F}(1{-}PPA_{C1C2})(1{-}PPA_{C1F})}+PPA_{C1C2}PPA_{C1F}.\]
The three-way table—one FCD result and two CCD replicates—is parameterized by the agreement quantities and anchors \(P_{a1}\) and \(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 |
Total | 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} empirical` = round(a1p, 4),
`P_{a1} formula` = round(phi1*sqrt(pC2*pF*(1-pC2)*(1-pF)) + pC2*pF, 4)),
title = "φ and Anchor P_{a1}: empirical = formula")Qty | Value |
|---|---|
φ1 | 0.5868 |
Pa1 empirical | 0.7705 |
Pa1 formula | 0.7705 |
✓ The joint-table parameterization is self-consistent. A positive phi emphasizes that conditional independence given \(G\) does not imply independence after conditioning only on CCD1. Follow-on sample-size simulations must preserve this residual dependence.
Unified conclusion: When the required parameters and weights are correct, the bridging and follow-on procedures recover the DGP targets despite selective sampling. Their vulnerability lies in the quantities not identified by the observed data—such as the missing-cell effect \(\delta_{01}\), its sensitivity ratio \(c\), and target-population positivity rates obtained externally or assumed.
Are bridging and follow-on data or methods identical? No. They share a generative logic, but the observed data, missingness, and estimands differ. The table compares them directly.
Dimension | Bridging | Follow-on |
|---|---|---|
Data source | The drug's pivotal trial | Separate agreement study plus established evidence for the approved CCD |
Outcome data available | Yes (T and Y in the trial's CTA+ population) | No new T or Y data for FCD |
Observed variables | Assay results, treatment arm T, and outcome Y | Assay results only (CCD1/CCD2/FCD) |
Missing-data structure | CTA−/CDx+ stratum not enrolled → δ01 missing | Enrichment distorts CCD1 stratum proportions; no reference standard |
Source of clinical evidence | Transport from the pivotal trial (CTA+ → CDx+) | Borrow the CCD-based conclusion for the FCD+ population |
Core method | δCDx = p·δ11 + (1−p)·δ01 | NI tests for PPA/NPA plus pc reweighting |
Quantity requiring assumption or external input | δ01 (assumed via sensitivity parameter c) | Target-population positivity rate pc; within-stratum residual correlation φ |
Shared skeleton: both settings combine inter-assay agreement, PPV-based population mixing, and at least one quantity not identified from the study data. The decisive difference is whether randomized outcome data are present. Bridging has such data and transports them to a new assay-defined population; follow-on development has no new efficacy data and instead borrows the CCD-based conclusion after testing agreement. Every additional assay-selection layer—such as prescreening or multiple CTAs—adds another dependence on assumptions, which is why multilayer prescreening is statistically costly.
In the worked example, bridging and follow-on analyses share the same latent population, assay parameters, and
TRUTHobject. This is a pedagogical simplification that makes their common structure and common targets visible. In practice, the two programs use different datasets, as the table shows.
Location | Conclusion |
|---|---|
§ Treatment-effect mixture η | ✓ Mixing identity = direct estimate = closed-form target |
§ PPA/NPA | ✓ NPA = d/(b+d) read directly from table |
§ Bayes-derived p | ✓ Bayes-derived p = direct estimate |
§ Joint-table parameterization | ✓ Joint margins are self-consistent (φ>0) |
§ pc reweighting | ✓ pc reweighting recovers the target (pc = target-population CCD1 positivity rate) |
§ Standard bridging | ✓ At the true c, the calculation recovers the target δCDx |
§ Step 4 MAR imputation | ✓ Correct MAR imputation (including CTA in the model) recovers the complete-data estimate; covariates-only imputation is conservative; MNAR remains unidentified |
§ c=1 identity | ✓ At c=1, p cancels algebraically, bridging rate = double-positive response rate (stopifnot self-check) |
§ Double bridging | ✓ Restricting every stage to CDx+ recovers the target |
§ Multi-CTA agreement weights | ✓ Weighting by positive/negative denominators recovers the pooled agreement target |
The last two rows of the verification table refer to the double-bridging and multi-CTA examples in the later Advanced chapter.
Throughout the worked example, treatment effect is expressed as a mean difference \(\delta\) in a continuous endpoint \(Y\). This is deliberate. The two transport identities—\(\eta=PPV\,\delta_{\text{pos}}+(1{-}PPV)\,\delta_{\text{neg}}\) and \(\delta_{CDx}=p\,\delta_{11}+(1{-}p)\,\delta_{01}\)—are applications of the law of total expectation. They are exact for collapsible expectation-based measures such as a mean difference, a fixed-time survival-probability difference, or a restricted mean survival time (RMST) difference.
Oncology trials often report a hazard ratio (HR) from a Cox model. The HR is non-collapsible: a prevalence-weighted average of subgroup HRs does not generally equal the marginal HR in the pooled population. An HR is not an expectation, so the linear mixture cannot be applied to it directly. A real bridging analysis must either work on a collapsible scale—such as a mean difference, fixed-time survival-probability difference, or RMST difference—or use a model-based numerical procedure whose assumptions are stated explicitly.
In short, the continuous endpoint keeps the transport identity exact and transparent. With an HR, choosing an appropriate effect scale becomes part of the bridging problem itself. The internal partial-likelihood estimation of the Cox model is outside the scope of these notes.
To explore the DGP, modify TRUTH—prevalence, assay
sensitivity/specificity, or the true-marker-specific treatment effects.
All downstream targets and estimates will update together.
Standard bridging has one missing subgroup, CTA−/CDx+. Two scenarios create additional selection layers: Case I, prescreening and double bridging; and Case II, multiple CTAs. Each additional layer moves the CDx treatment-effect estimand farther from direct observation and increases dependence on assumptions.
A coarse biomarker prescreen may be added before CTA to accelerate enrollment and reduce cost. This creates an upstream gap: patients who would have been CTA+ but are excluded by the prescreen.
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: Double 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(Not enrolled)", "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+ (Enrolled)", "#2E86AB", w = 2.5, h = 0.75, cex = 0.92)
draw_box(7.0, 5.4, "CTA-\n(Not enrolled)", "#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+ Observed (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+ Missing (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+ Missing (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+ → Complete CTA+ stage-2: CTA+ → CDx+",
cex = 0.95, col = "gray30", font = 3)Adjustment now has two stages. Stage 1 recovers the full CTA+ cohort from the observed Pre+/CTA+ group. Stage 2 transports the effect from CTA+ to the CDx+ population.
\[\delta_{CTA+}=(p_1+(1{-}p_1)c_1)\,\delta'_{11},\qquad \delta_{CDx+}=(p+(1{-}p)c_2)\,\delta_{CTA+}\]
Key: every subgroup in both stages must first be restricted to CDx+ and only then stratified by Pre or CTA. The estimand \(\delta_{CDx+}\) is defined within the CDx+ population, so \(\delta'_{11}\) is the effect among Pre+/CTA+/CDx+ patients, not among all Pre+/CTA+ patients. Omitting the CDx+ restriction substitutes the broader CTA+ effect for the CTA+/CDx+ effect and produces systematic bias. The code below applies the restriction explicitly.
big3 <- generate_population(4e6, with_endpoint = TRUE); cdxpos <- subset(big3, CDx == 1)
# stage 1: recover by Pre within CTA+&CDx+
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: recover by CTA within CDx+
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),
`Stage 1 Recovery` = round(delta_CTApos_s1,4), `Stage 1 True Value` = round(true_d11c,4),
`Stage 2 Recovery` = round(delta_CDx_s2,4), `δ_{CDx} True Value` = round(true_delta_in_assay_pos(TRUTH$assays$CDx),4),
check.names = FALSE),
title = "Double Bridging: Two-Stage Sequential Recovery of True Value")p1 | c1 | Stage 1 Recovery | Stage 1 True Value | Stage 2 Recovery | δCDx True Value |
|---|---|---|---|---|---|
0.7942 | 0.9592 | 0.5928 | 0.5928 | 0.5494 | 0.5508 |
✓ The two stages recover the target sequentially. Each missing branch requires its own sensitivity parameter, so assumptions compound across stages. With partial prescreening, only the affected subsets require double bridging. Retaining patient-level prescreen results makes the Pre+ and Pre− branches explicit and improves the transparency of Stage 1. The practical implication is to avoid unnecessary prescreening layers, which make the final CDx effect less direct and less precise.
Multiple CTAs may be used because of payer requirements, site preferences, or country-specific regulation. Each can have different agreement with the CDx and therefore induce a different transport bias. The example assumes that each patient is enrolled through exactly one CTA and that selection behavior is otherwise comparable across CTA cohorts.
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 = "Two CTAs: Parallel Bridging", cex.main = 1.5)
draw_box(1.2, 4, "Sample", "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+ Observed", "#2E86AB", w = 2.8, h = 0.66, cex = 0.9)
draw_box(10.6, 5, "CDx+ Missing → 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+ Observed", "#3AA0C0", w = 2.8, h = 0.66, cex = 0.9)
draw_box(10.6, 1, "CDx+ Missing → 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)truth2 <- TRUTH
truth2$assays$CTA1 <- c(sens = 0.85, spec = 0.90) # Positivity rate ~0.33
truth2$assays$CTA2 <- c(sens = 0.90, spec = 0.50) # Poor specificity, positivity rate ~0.62 (strong contrast)
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(`Method B (Weighted Efficacy)` = round(dB1*n1c/(n1c+n2c) + dB2*n2c/(n1c+n2c), 4),
`δ_{CDx} True Value` = round(true_delta_in_assay_pos(truth2$assays$CDx, truth2), 4),
check.names = FALSE),
title = "Multiple CTAs Method B (Weighted Efficacy) Recovers True Value")Method B (Weighted Efficacy) | δCDx True Value |
|---|---|
0.5557 | 0.5508 |
A common error when pooling agreement: PPA must be weighted by the number of CTA-positive patients in each cohort, and NPA by the number of CTA-negative patients—not by each cohort’s total sample size. PPA is a conditional proportion among CTA-positive patients, so its natural denominator determines the pooling weight. The code contrasts the two schemes.
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(`Weighted by Positive Counts (Correct)` = round(unname((pos1*ppa1+pos2*ppa2)/(pos1+pos2)), 4),
`Weighted by Total Sample Size (Incorrect)` = round(unname(ppa1*n1c/(n1c+n2c) + ppa2*n2c/(n1c+n2c)), 4),
check.names = FALSE),
title = "Pooling Concordance: Weighted by Positive Counts vs Total Sample Size")Weighted by Positive Counts (Correct) | Weighted by Total Sample Size (Incorrect) |
|---|---|
0.5454 | 0.5951 |
✓ Method B recovers the target, and pooling PPA by positive counts gives the correct pooled agreement; total-sample-size weighting does not. The final combination of cohort-specific treatment effects is a separate issue. Weighting those effects by \(n_i/n\) encodes an assumption about the relative use of each CTA and requires external design or operational justification.
Reference table. \(\delta_{\text{pos}}\) and \(\delta_{\text{neg}}\) are shorthand introduced here for the two true-marker-specific treatment effects, each defined as a contrast between two \(\mu\) values.
Symbol | Definition | Notes |
|---|---|---|
T | Treatment assignment: 1 = experimental treatment, 0 = control | |
G | True marker status: 1 = marker-positive, 0 = marker-negative | Observed only with a reference standard |
μTG | Outcome mean, indexed by (treatment arm T, true marker G) in a four-cell table | Cell-level outcome mean |
δpos = μ11 − μ01 | Treatment effect in the true marker+ population (building block one) | Shorthand introduced here |
δneg = μ10 − μ00 | Treatment effect in the true marker− population (building block two) | Shorthand introduced here |
η | Population treatment effect with FCD+ = PPVf·δpos + (1−PPVf)·δneg | Indexed by FCD |
θ | Population treatment effect with CCD+ = PPVc·δpos + (1−PPVc)·δneg | Indexed by CCD; borrowed in follow-on development |
δCDx | Population treatment effect with CDx+ (bridging estimation target) | |
δ11, δ01 | (CTA+ & CDx+) / (CTA− & CDx+) population treatment effect | Observed / missing |
δ'11, δ'01, δCTA+ | Double bridging, Stage 1: effects in Pre±/CTA+/CDx+ cells and the recovered CTA+ cohort effect | Advanced example |
c, c1, c2 | Sensitivity parameter: δ01 = c·δ11 (and two-stage version), c ∈ (0,1) | The missing-cell effect is not identified |
p | Pr(CTA+ | CDx+)—transport weight for bridging | |
p1 | Pr(Pre+ | CTA+)—double bridging stage one weight | Advanced example |
pc | Pr(CCD1=1)—CCD1 positivity rate in the target population (for reweighting) | External target-population quantity |
prev, π | Pr(CTA+)—CTA positivity rate in the intended-use population | Used in the Bayes calculation of p |
PPVf, NPVf | Positive/negative predictive values for FCD: Pr(G=1|FCD=1), Pr(G=0|FCD=0) | |
PPVc, NPVc | Positive/negative predictive values for CCD (same definition with CCD as the assay) | |
PPA, NPA, OPA | Positive/negative/overall percent agreement (OPA=(a+d)/n, the overall proportion of equal assay calls; listed but not used in the transport formula); two-letter subscript = reference assay + comparison assay (C1=CCD1, C2=CCD2, F=FCD) | |
ξppa1, … | Difference in NI testing, e.g., ξppa1 = PPAC1C2 − PPAC1F | |
δ1, δ0 | Acceptable NI margin for PPA / NPA | NI margins are unrelated to the treatment-effect symbol δ |
φ1, φ2 | φ coefficient: correlation between FCD and CCD2 within CCD1+ / CCD1− strata | |
Pa1, Pd0 | Two probability anchors in 3-way contingency table | |
R1, R2, RCDx | Response-rate version: observed CTA+/CDx+ rate, missing-cell rate (=c·R1), and bridged CDx+ rate | Same transport identity as the δ version |
R (missing indicator) | Missingness indicator: R=1 means the patient's CDx result is missing (Step 4) | Measurement-level missingness |
X, Z | X = observed covariates in the imputation model; Z = the potentially missing CDx result | Variables in the MAR definition |
m, β* | m = number of imputations; β* = coefficient vector drawn from the estimated sampling distribution of β̂ | Imputation-level quantities |
W̄, B, T, FMI | Rubin's rules: mean within-imputation variance, between-imputation variance, total variance T = W̄+(1+1/m)B, and fraction of missing information | Across-imputation summaries reflect B, not the within-imputation component |
(A fuller framework could define FCD-indexed components \(\eta_{11},\eta_{01},\ldots\) and CCD-indexed components \(\theta_{11},\theta_{01},\ldots\). This document uses only their contrasts, \(\eta=\eta_{11}-\eta_{01}\) and \(\theta=\theta_{11}-\theta_{01}\), so the component means are not notated separately.)
Abbrev | Full term | Plain-language meaning | Where used here |
|---|---|---|---|
PPA / NPA / OPA | positive / negative / overall percent agreement | directional positive/negative agreement and overall agreement between assays | inter-assay agreement (OPA is listed for completeness but not used in transport) |
PPV / NPV | positive / negative predictive value | probability that an assay call reflects the true marker status | treatment-effect mixing in assay-positive populations |
NI | non-inferiority | not worse than a reference by more than a prespecified margin | FCD-versus-CCD agreement testing |
CI | confidence interval | range quantifying uncertainty in an estimate | uncertainty in agreement estimates |
RCT | randomized controlled trial | trial with random assignment to treatment and control | trial-design context |
MI | multiple imputation | analysis across multiple draws of the missing values | Step 4: imputation of missing CDx results |
MCAR / MAR / MNAR | missing completely at random / at random / not at random | three assumptions about how missingness relates to observed and unobserved data | Step 4: three missing-data mechanisms |
FMI | fraction of missing information | share of total uncertainty attributable to missing information | proportion of total variance attributable to missing information under Rubin's rules |
Author: Dr. Yang Hu | First published: 2026-08-25 | Last updated: 2026-08-28 13:29 EDT