Speed-ups (shared PVI fit, clones once, case-control sampled deferred model); final scale 200 reps extending the draft (D51)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-27 09:31:03 -04:00
co-authored by Claude Opus 5.5
parent e1457ccc1e
commit cfc9da2c87
6 changed files with 288 additions and 65 deletions
+81 -44
View File
@@ -51,31 +51,45 @@ fit_pvi <- function(data, newdata, variant, extra_covs = NULL) {
d <- data[win == w]
nd <- newdata[i_new]
tt_levels <- if (w == 1L) "factor(tt)" else "splines::ns(t, df = 4)"
# deferred period (about 2 million person-months, few PVIs): fit on every PVI and a
# fixed 10% sample of the other months, with offset log(1/0.1) so the coefficients
# are those of the full model (case-control sampling of person-months)
if (w == 0L) d <- sample_controls(d, frac = 0.1)
# no exposure at all in a phase (the NCE after the window): probability 0
out[i_new] <- if (sum(d$pvi) == 0) 0 else fit_pvi_phase(d, nd, variant, tt_levels, extra_covs)
}
out
}
#' Keep all events and a deterministic fraction of non-events; offset column `off`.
sample_controls <- function(d, frac) {
keep <- d$pvi == 1L | (((d$id * 7919L + d$t * 104729L) %% 1000L) < frac * 1000)
d <- d[keep]
d[, off := log(1 / frac)] # sampling raises every row's odds by 1/frac
d
}
fit_pvi_phase <- function(d, nd, variant, time_terms, extra_covs = NULL) {
if (!"off" %in% names(d)) d[, off := 0]
nd <- copy(nd)[, off := 0] # predictions are for the full population
covs <- c(BASE_COVS, "sev_obs", "sev0", extra_covs)
if (variant == "V4_within") covs <- setdiff(covs, "obl")
rhs <- paste(c(covs, time_terms), collapse = " + ")
if (variant %in% c("V1_ignore", "V2_cluster")) {
f <- as.formula(paste("~", rhs))
X <- model.matrix(f, d)
fit <- fastglm::fastglm(X, d$pvi, family = binomial(), method = 2)
fit <- fastglm::fastglm(X, d$pvi, family = binomial(), method = 2, offset = d$off)
b <- coef(fit); b[is.na(b)] <- 0
Xn <- model.matrix(f, nd)
return(as.vector(plogis(Xn %*% b)))
}
if (variant == "V3_random") {
fit <- lme4::glmer(as.formula(paste("pvi ~", rhs, "+ (1 | phys_id)")), data = d,
fit <- lme4::glmer(as.formula(paste("pvi ~", rhs, "+ offset(off) + (1 | phys_id)")), data = d,
family = binomial(), nAGQ = 0, control = lme4::glmerControl(calc.derivs = FALSE))
return(as.vector(predict(fit, newdata = nd, type = "response", allow.new.levels = TRUE)))
}
if (variant == "V4_within") {
fit <- fixest::feglm(as.formula(paste("pvi ~", rhs, "| phys_id")), data = d,
fit <- fixest::feglm(as.formula(paste("pvi ~", rhs, "| phys_id")), data = d, offset = ~off,
family = binomial(), notes = FALSE, warn = FALSE)
pr <- as.vector(predict(fit, newdata = nd, type = "response"))
pr[is.na(pr)] <- 0
@@ -162,22 +176,33 @@ restrict_variant <- function(rec, variant) {
#' patient-level for V1, physician-cluster-robust for V2-V4.
run_design <- function(rec, design, variant, outcomes = c(male = "t_male", disease = "t_disease",
nco1 = "t_nco1", nco2 = "t_nco2")) {
run_designs(rec, design, variant, outcomes)
}
#' Several designs under one variant, sharing the PVI fit between CCW and sequential
#' trials (the sequential propensity score is the window part of the same model).
run_designs <- function(rec, designs, variant, outcomes = c(male = "t_male", disease = "t_disease",
nco1 = "t_nco1", nco2 = "t_nco2")) {
rec <- restrict_variant(rec, variant)
rs <- if (design %in% c("ccw", "seq")) pvi_risk_set(rec) else NULL
phat <- if (design == "ccw") fit_pvi(rs, rs, variant, rec$extra_covs) else NULL
need <- intersect(designs, c("ccw", "seq"))
rs <- if (length(need)) pvi_risk_set(rec) else NULL
phat <- if ("ccw" %in% need) fit_pvi(rs, rs, variant, rec$extra_covs)
else if ("seq" %in% need) { x <- rep(NA_real_, nrow(rs)); w <- rs$win == 1L
x[w] <- fit_pvi(rs[w], rs[w], variant, rec$extra_covs); x }
cmp <- if (is.null(rec$comparators)) c("B1", "B2") else rec$comparators
out <- lapply(names(outcomes), function(o) {
use_patient <- variant == "V1_ignore"
rbindlist(lapply(designs, function(design) {
r <- switch(design,
naive = cbind(contrast = "early_none", est_naive(rec, outcomes[[o]], variant)),
landmark = cbind(contrast = "early_none", est_landmark(rec, outcomes[[o]], variant, extra_covs = rec$extra_covs)),
ccw = est_ccw(rec, outcomes[[o]], variant, rs = copy(rs), phat = phat, comparators = cmp),
seq = cbind(contrast = "now_not", est_seq(rec, outcomes[[o]], variant, rs = copy(rs))))
r[, `:=`(outcome = o, design = design, variant = variant, n = nrow(rec$patients))]
use_patient <- variant == "V1_ignore"
naive = rbindlist(lapply(names(outcomes), function(o)
cbind(contrast = "early_none", est_naive(rec, outcomes[[o]], variant), outcome = o))),
landmark = rbindlist(lapply(names(outcomes), function(o)
cbind(contrast = "early_none", est_landmark(rec, outcomes[[o]], variant, extra_covs = rec$extra_covs), outcome = o))),
ccw = est_ccw(rec, outcomes, variant, rs = copy(rs), phat = phat, comparators = cmp),
seq = cbind(contrast = "now_not", est_seq(rec, outcomes, variant, rs = copy(rs), phat = phat)))
r[, `:=`(design = design, variant = variant, n = nrow(rec$patients))]
r[, se := if (use_patient) se_patient else se_cluster]
r
})
rbindlist(out, fill = TRUE)
}), fill = TRUE)
}
#' Naive "ever early PVI" design: exposure = PVI in months 0..W-1, follow-up from month 0,
@@ -237,13 +262,14 @@ est_ccw <- function(rec, outcome = "t_male", variant = "V1_ignore", horizon = 60
comparators = c("B1", "B2"), rs = NULL, phat = NULL) {
W <- rec$par$size$window
if (is.null(rs)) rs <- pvi_risk_set(rec)
if (is.null(phat)) phat <- fit_pvi(rs, rs, variant)
if (is.null(phat)) phat <- fit_pvi(rs, rs, variant, rec$extra_covs)
rs[, ph := phat]
rs <- rs[!is.na(ph)]
# everyone eligible at diagnosis is cloned, including patients with a MALE event or
# death in month 0 (events in the grace period count in both arms)
# death in month 0 (events in the grace period count in both arms). Clones and weights
# are built once over follow-up to death or the horizon; each outcome then truncates.
p <- copy(rec$patients)
p[, t_out := get(outcome)]
p[, t_out := NA_integer_]
base <- expand_followup(p, 0L, horizon)
# P(PVI) for each eligible month; months where a patient is not at risk of PVI have
# probability 0 of deviating
@@ -251,7 +277,7 @@ est_ccw <- function(rec, outcome = "t_male", variant = "V1_ignore", horizon = 60
base[is.na(ph), ph := 0]
base[, `:=`(tp = fifelse(is.na(t_pvi), Inf, as.numeric(t_pvi)), el = pvi_elective %in% TRUE)]
out <- list()
arms <- list()
# arm A: censored after month W-1 if no PVI by then (and still at risk of PVI then)
a <- copy(base)[, arm := "A"]
cens_A <- a[t == W - 1L & ph > 0 & tp > W - 1L, id] # deviated at month W-1
@@ -259,50 +285,61 @@ est_ccw <- function(rec, outcome = "t_male", variant = "V1_ignore", horizon = 60
pA <- base[t == W - 1L & ph > 0, .(id, pA = ph)]
a <- pA[a, on = "id"]
a[, w := ifelse(t > W - 1L & !is.na(pA) & tp == W - 1L, 1 / pA, 1)]
out$A <- a[, .(id, phys_id, arm, t, w, ev, dth)]
arms$A <- a[, .(id, phys_id, arm, t, w)]
for (cmp in comparators) {
b <- copy(base)[, arm := cmp]
last <- if (cmp == "B1") W - 1L else horizon
b <- b[!(el & tp <= last & t > tp)] # censored after month tp
b[, q := ifelse(t <= last, 1 - ph, 1)]
b[, w := 1 / shift(cumprod(q), fill = 1), by = id]
out[[cmp]] <- b[, .(id, phys_id, arm, t, w, ev, dth)]
arms[[cmp]] <- b[, .(id, phys_id, arm, t, w)]
}
aj <- aj_cif(rbindlist(out), horizon)
rbindlist(lapply(comparators, function(cmp) cbind(contrast = paste0("A_", cmp), rd_from_aj(aj, "A", cmp))))
clones <- rbindlist(arms)
ev_times <- rec$patients[, c("id", "t_death", unname(outcome)), with = FALSE]
rbindlist(lapply(seq_along(outcome), function(k) {
d <- ev_times[, .(id, t_death, t_out = get(outcome[k]))][clones, on = "id"]
d <- d[is.na(t_out) | t <= t_out]
aj <- aj_cif(mark_events(d), horizon)
rbindlist(lapply(comparators, function(cmp)
cbind(contrast = paste0("A_", cmp), rd_from_aj(aj, "A", cmp))))[
, outcome := if (is.null(names(outcome))) outcome[k] else names(outcome)[k]]
}))
}
#' Sequential trials, ITT (D14): a trial at each month m = 0..W-1 among patients eligible
#' for PVI at m; treatment = PVI at m; propensity score from covariates at m; outcome
#' counted in months m+1..horizon-1; trial RDs pooled with weights = number eligible.
est_seq <- function(rec, outcome = "t_male", variant = "V1_ignore", horizon = 60L, rs = NULL) {
est_seq <- function(rec, outcome = "t_male", variant = "V1_ignore", horizon = 60L, rs = NULL,
phat = NULL) {
W <- rec$par$size$window
if (is.null(rs)) rs <- pvi_risk_set(rec)
if (!is.null(phat)) rs[, ps := phat]
trials <- rs[t < W]
trials[, tt := t] # trial-month indicators in the PS model
ps <- fit_pvi(trials, trials, variant)
trials[, ps := ps]
# the trial-baseline propensity score is the window model of the PVI fit
if (is.null(phat)) trials[, ps := fit_pvi(trials, trials, variant, rec$extra_covs)]
trials <- trials[!is.na(ps)]
trials[, `:=`(m = t, arm = ifelse(pvi == 1L, "pvi", "no"),
w = ifelse(pvi == 1L, 1 / ps, 1 / (1 - ps)))]
p <- rec$patients[, .(id, t_out = get(outcome))]
trials <- p[trials, on = "id"]
trials <- trials[is.na(t_out) | t_out > m]
res <- list(); infs <- list()
for (mm in 0:(W - 1)) {
tr <- trials[m == mm]
d <- expand_followup(tr[, .(id, phys_id, arm, w, t_out, t_death)], mm + 1L, horizon)
aj <- aj_cif(d, horizon)
x <- dcast(aj$inf, id + phys_id ~ arm, value.var = "inf", fill = 0)
res[[mm + 1L]] <- data.table(m = mm, n = nrow(tr), est = aj$F[["pvi"]] - aj$F[["no"]])
infs[[mm + 1L]] <- data.table(id = x$id, phys_id = x$phys_id, m = mm, inf = x$pvi - x$no)
}
res <- rbindlist(res); infs <- rbindlist(infs)
res[, wt := n / sum(n)]
infs <- res[, .(m, wt)][infs, on = "m"]
pi <- infs[, .(inf = sum(wt * inf)), by = .(id, phys_id)]
data.table(est = sum(res$wt * res$est), se_patient = sqrt(sum(pi$inf^2)),
se_cluster = sqrt(sum(tapply(pi$inf, pi$phys_id, sum)^2)), risk1 = NA_real_, risk0 = NA_real_)
trials <- trials[, .(id, phys_id, m, arm, w, t_death)]
rbindlist(lapply(seq_along(outcome), function(k) {
tr_k <- rec$patients[, .(id, t_out = get(outcome[k]))][trials, on = "id"][is.na(t_out) | t_out > m]
res <- list(); infs <- list()
for (mm in 0:(W - 1)) {
tr <- tr_k[m == mm]
d <- expand_followup(tr[, .(id, phys_id, arm, w, t_out, t_death)], mm + 1L, horizon)
aj <- aj_cif(d, horizon)
x <- dcast(aj$inf, id + phys_id ~ arm, value.var = "inf", fill = 0)
res[[mm + 1L]] <- data.table(m = mm, n = nrow(tr), est = aj$F[["pvi"]] - aj$F[["no"]])
infs[[mm + 1L]] <- data.table(id = x$id, phys_id = x$phys_id, m = mm, inf = x$pvi - x$no)
}
res <- rbindlist(res); infs <- rbindlist(infs)
res[, wt := n / sum(n)]
infs <- res[, .(m, wt)][infs, on = "m"]
pi <- infs[, .(inf = sum(wt * inf)), by = .(id, phys_id)]
data.table(est = sum(res$wt * res$est), se_patient = sqrt(sum(pi$inf^2)),
se_cluster = sqrt(sum(tapply(pi$inf, pi$phys_id, sum)^2)), risk1 = NA_real_, risk0 = NA_real_,
outcome = if (is.null(names(outcome))) outcome[k] else names(outcome)[k])
}))
}
#' Negative-control exposure (D32, D36): the early low-value procedure (NCE, months