This vignette applies the approximate maximum influence perturbation
(AMIP) of Broderick, Giordano, and Meager in two settings. After
fitting, outer parameters (beta, theta) are
held at the MLE and only the inner random effects gamma are
allowed to move. Package helpers phi_gamma(),
influence_scores(), amip_set(), and
ad_pack_drop() cover the shared pipeline: the fit-time
observation tapes supply the inner Hessian \(H\); per-unit scores use streaming one-unit
observation gradients (grad_obs_units) so scores do not
require holding an identity shard pack.
The first example is the London case-crossover model: the most
influential strata for flipping the PM10 effect at
exposure 80. The second is the MASS bacteria Bernoulli
GLMM: the most influential swabs for flipping the sign
of the drug-versus-placebo treatment effect. In both cases an inner
optimization with those units removed confirms the linear approximation,
and a full refit on the reduced data is compared to the original
fit.
Same London daily mortality / PM10 specification as the case-crossover vignette. Abbreviated knits use 2003 only.
data("london", package = "adlaplace")
if (!vignette_full) {
london <- london[london$year == 2003, ]
}
cc_formula <- dirichlet_multinom(
all,
by = c("year", "month", "dow"),
init = 0.02
) ~ 0 + rh + iwp(
pm10,
p = 2,
knots = seq(0, 100, by = 10),
ref_value = 40,
init = 0.001
) + iwp(
tmean,
p = 3,
knots = seq(-2, 30, by = 2),
init = 0.001,
ref_value = 20
)Outer Hessian is not needed for this inner-only AMIP calculation.
fit <- adlaplace(
formula = cc_formula,
data = london,
config = list(
num_threads = vignette_num_threads,
num_shards = fit_num_shards
),
control = list(
maxit = fit_maxit,
trace = 3,
REPORT = 5
),
verbose = FALSE,
hessian = FALSE
)
#> N = 4, M = 5 machine precision = 2.22045e-16
#> At X0, 0 variables are exactly at the bounds
#> At iterate 0 f= 5352.3 |proj g|= 10604
#> At iterate 5 f = 5349.2 |proj g|= 121.32
#> At iterate 10 f = 5349.1 |proj g|= 1659
#> At iterate 15 f = 5346.5 |proj g|= 438.17
#> At iterate 20 f = 5341.5 |proj g|= 8.1316
#>
#> iterations 21
#> function evaluations 30
#> segments explored during Cauchy searches 21
#> BFGS updates skipped 0
#> active bounds at final generalized Cauchy point 0
#> norm of the final projected gradient 1.00993
#> final function value 5341.48
#>
#> F = 5341.48
#> final value 5341.481082
#> converged
md <- fit$model_data
n_strata <- n_obs_units(fit)
fit$par_info[, c("label", "mle", "mle_internal")]
#> label mle mle_internal
#> 1 rh_linear 0.0006178345 0.0006178345
#> 2 all_dirichlet_multinom_sd 0.0023596821 -6.0492283834
#> 3 pm10_iwp 0.0010411985 -6.8673827805
#> 4 tmean_iwp 0.0090517336 -4.7047989832
n_strata
#> [1] 420The PM10 smooth is centered at ref_value = 40, so the
effect at 80 is a nontrivial linear functional \(\phi(\gamma) = a^\top\gamma\) of the IWP
and random-polynomial coefficients. influence_scores()
freezes the outer MLE, re-optimizes \(\gamma\) on the fit pack (for \(H\)), and streams one-unit observation
gradients to form \(\psi_s = -v^\top
g_s\).
a <- phi_gamma(fit, term = "pm10", new_x = data.frame(pm10 = 80))
inf <- influence_scores(fit, a, batch_size = 32L)
gamma_hat <- inf$gamma_hat
phi_hat <- inf$phi
psi <- inf$psi
gamma_lab <- names(fit$gamma)
pm_terms <- Filter(
function(tt) {
identical(tt@name, "pm10") &&
identical(as.character(tt@model_role), "random")
},
md$terms
)
c(phi_hat = phi_hat, n_strata = n_strata)
#> phi_hat n_strata
#> 0.0111662 420.0000000
summary(psi)
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> -1.458e-03 -1.748e-04 1.129e-05 1.149e-06 1.546e-04 1.444e-03Leaving out stratum \(s\) changes
\(\phi\) by approximately \(-\psi_s\). amip_set() drops
the largest same-sign scores until the linear approximation flips \(\hat\phi\). If that is impossible, it
reports an empty removal set.
S <- amip_set(psi, phi_hat)
drop_idx <- S$idx
amip_alpha <- S$alpha
amip_change <- S$approx_change
c(
phi_hat = phi_hat,
n_drop = length(drop_idx),
alpha = amip_alpha,
approx_change = amip_change
)
#> phi_hat n_drop alpha approx_change
#> 0.01116620 11.00000000 0.02619048 -0.01150765
dropped <- data.frame(
idx = drop_idx,
strata = colnames(md$term_data$elgm_matrix)[drop_idx],
psi = psi[drop_idx]
)
dropped
#> idx strata psi
#> 1 97 2003.Feb.Sat 0.0014440752
#> 2 320 2005.Oct.Fri 0.0013216182
#> 3 185 2004.Mar.Wed 0.0012736235
#> 4 134 2003.Aug.Mon 0.0012212622
#> 5 352 2006.Mar.Tue 0.0010324870
#> 6 64 2002.Oct.Mon 0.0009507317
#> 7 60 2002.Sep.Thu 0.0008982068
#> 8 187 2004.Mar.Fri 0.0008964684
#> 9 100 2003.Mar.Tue 0.0008780937
#> 10 88 2003.Jan.Thu 0.0008163989
#> 11 413 2006.Nov.Sun 0.0007746860oldpar <- par(no.readonly = TRUE)
par(mar = c(2.5, 2.5, 1, 0.5), bty = "l", mgp = c(1.5, 0.5, 0))
parts <- strsplit(colnames(md$term_data$elgm_matrix), ".", fixed = TRUE)
stratum_info <- data.frame(
year = as.integer(vapply(parts, `[[`, character(1L), 1L)),
month = vapply(parts, `[[`, character(1L), 2L),
dow = vapply(parts, `[[`, character(1L), 3L),
stringsAsFactors = FALSE
)
stratum_info$month <- factor(stratum_info$month, levels = levels(london$month))
stratum_info$dow <- factor(stratum_info$dow, levels = levels(london$dow))
stratum_info$name <- paste(stratum_info$month, stratum_info$dow, sep = "\n")
stratum_info$num <- stratum_info$year +
(as.integer(stratum_info$month) - 1) / 12 +
0.1 * (as.integer(stratum_info$dow) - 1) / 7
Scol <- grey(seq(0.2, 0.8, length.out = 12))
stratum_info$pch <- as.character(as.integer(stratum_info$dow))
stratum_info$col <- Scol[as.integer(stratum_info$month)]
stratum_info$pch[drop_idx] <- "o"
stratum_info$col[drop_idx] <- "firebrick"
plot(
stratum_info$num, psi,
pch = stratum_info$pch,
cex = 0.55, col = stratum_info$col,
xlab = "stratum", ylab = expression(psi[s])
)
text(
stratum_info$num[drop_idx], psi[drop_idx],
stratum_info$name[drop_idx],
cex = 0.5
)
legend("bottomright", legend = levels(stratum_info$month), fill = Scol, bty = "n", inset = 0)
abline(h = 0, lty = 2, col = "grey50")ad_pack_drop() retapes the likelihood without the
Approximate Most Influential Set (filtering the fit’s coarse observation
shards). Keep the outer MLE fixed and re-optimize gamma.
The resulting change in \(\phi(80)\) is
an exact finite-sample lower bound on the worst-case strata-removal
effect (Broderick et al.).
phi_from_gamma <- function(gamma) {
sum(a * as.numeric(gamma))
}
pm_curve <- function(gamma, grid = seq(0, 100, by = 2)) {
new_grid <- data.frame(pm10 = grid)
design_list <- lapply(pm_terms, design, data = new_grid)
design_list <- Filter(Negate(is.null), design_list)
D <- as.matrix(do.call(cbind, design_list))
g <- as.numeric(gamma)
names(g) <- gamma_lab
list(x = grid, f = as.numeric(D %*% g[colnames(D)]))
}
curve0 <- pm_curve(gamma_hat)
if (!length(drop_idx)) {
phi_drop <- NA_real_
curve1 <- NULL
message("No strata dropped; skipping confirmation retape.")
} else {
af_drop <- ad_pack_drop(fit, drop_idx, num_threads = vignette_num_threads)
io_drop <- inner_opt(
parameters = as.numeric(fit$par_info$mle_internal),
gamma = gamma_hat,
ad_pack = af_drop,
control = list(report.level = 0, report.freq = 0),
deriv = FALSE,
return_hessians = FALSE,
verbose = FALSE
)
gamma_drop <- as.numeric(io_drop$inner_opt$solution)
phi_drop <- phi_from_gamma(gamma_drop)
curve1 <- pm_curve(gamma_drop)
}
c(
phi_full = phi_hat,
phi_dropped = phi_drop,
actual_change = if (is.finite(phi_drop)) phi_drop - phi_hat else NA_real_,
approx_change = if (length(drop_idx)) amip_change else NA_real_
)
#> phi_full phi_dropped actual_change approx_change
#> 0.011166203 -0.001367806 -0.012534009 -0.011507652Drop the nominated strata from the data and re-estimate the full outer MLE (smoothness and overdispersion included).
elgm <- md$term_data$elgm_matrix
if (!length(drop_idx)) {
fit_drop <- NULL
phi_refit <- NA_real_
message("No strata dropped; skipping refit.")
} else {
in_drop <- Matrix::rowSums(elgm[, drop_idx, drop = FALSE]) > 0
london_drop <- md$term_data$data[!in_drop, , drop = FALSE]
c(
n_full = nrow(md$term_data$data),
n_drop = nrow(london_drop),
n_strata_drop = length(drop_idx)
)
fit_drop <- adlaplace(
formula = cc_formula,
data = london_drop,
config = list(
num_threads = vignette_num_threads,
num_shards = fit_num_shards
),
control = list(
maxit = fit_maxit,
trace = 3,
REPORT = 5
),
verbose = FALSE,
hessian = FALSE
)
phi_refit <- sum(a * fit_drop$gamma[names(a)])
fit_drop$par_info[, c("label", "mle", "mle_internal")]
}
#> N = 4, M = 5 machine precision = 2.22045e-16
#> At X0, 0 variables are exactly at the bounds
#> At iterate 0 f= 5195.4 |proj g|= 10009
#> At iterate 5 f = 5192.6 |proj g|= 238.16
#> Warning: inner_opt status is Radius of trust region is less than
#> stop.trust.radius; the Laplace approximation used this inner solution anyway
#> At iterate 10 f = 5192.3 |proj g|= 2919.5
#> At iterate 15 f = 5190.4 |proj g|= 9.1964
#> At iterate 20 f = 5176.9 |proj g|= 410.27
#>
#> iterations 24
#> function evaluations 33
#> segments explored during Cauchy searches 24
#> BFGS updates skipped 0
#> active bounds at final generalized Cauchy point 0
#> norm of the final projected gradient 0.693709
#> final function value 5176.77
#>
#> F = 5176.77
#> final value 5176.765891
#> converged
#> label mle mle_internal
#> 1 rh_linear 0.0005938911 0.0005938911
#> 2 all_dirichlet_multinom_sd 0.0154897141 -4.1675790832
#> 3 pm10_iwp 0.0009891239 -6.9186909740
#> 4 tmean_iwp 0.0093463415 -4.6727702970
c(
phi_full = phi_hat,
phi_inner_drop = phi_drop,
phi_refit = phi_refit
)
#> phi_full phi_inner_drop phi_refit
#> 0.0111662032 -0.0013678056 -0.0009615618num_sim <- 200L
num_grid <- 101L
sim_full <- sim_random(
"pm10",
fit = fit,
num_grid = num_grid,
num_sim = num_sim,
transform = exp
)
q_full <- if (!is.null(sim_full$global)) sim_full$global else sim_full$pointwise
sim_drop <- NULL
q_drop <- NULL
if (!is.null(fit_drop)) {
sim_drop <- sim_random(
"pm10",
fit = fit_drop,
new_x = data.frame(pm10 = sim_full$x),
num_sim = num_sim,
transform = exp
)
q_drop <- if (!is.null(sim_drop$global)) sim_drop$global else sim_drop$pointwise
}
oldpar <- par(no.readonly = TRUE)
par(mar = c(3, 3, 0, 0), mgp = c(1.5, 0.5, 0), bty = "l")
matplot(
sim_drop$x, q_drop,
type = "l",
lty = c(3, 1, 3), col = "darkblue", lwd = 2,
xlab = "pm10", ylab = "RR", xaxs = "i",
ylim = c(0.975, 1.065), yaxs = "i", bty = "l"
)
matlines(sim_full$x, q_full, lty = c(3, 1, 3), col = "red", lwd = 2)
abline(h = 1, lty = 3, col = "grey50")
legend(
"topleft",
legend = c("full data", "AMIP strata removed"),
col = c("red", "darkblue"),
lwd = 2,
bty = "n"
)The MASS bacteria data are 220 swabs on 50 children,
with a three-level treatment (placebo, drug,
drug+) and follow-up week. Treatment coefficients are
placed in the inner field with known-SD Gaussian priors, so the
drug-versus-placebo contrast is a coordinate of \(\gamma\) and the same inner AMIP applies.
There is no ELGM map, so grad_obs_units scores individual
swabs. (Summing scores to children is not enough to flip this contrast
under the linear approximation.)
data(bacteria, package = "MASS")
bacteria$present <- as.integer(bacteria$y == "y")
bac_formula <- binom(present) ~
intercept(sd = 10) +
linear(trt, sd = 10) +
linear(week, sd = 10) +
iid(ID, init = 1) +
prior(theta = 0, dist = "exp", median = 1)
bac_maxit <- if (vignette_full) 200L else 80L
bac_shards <- if (vignette_full) 20L else 8Lfit_bac <- adlaplace(
formula = bac_formula,
data = bacteria,
config = list(
num_threads = vignette_num_threads,
num_shards = bac_shards
),
control = list(
maxit = bac_maxit,
trace = 3,
REPORT = 5
),
verbose = FALSE,
hessian = FALSE
)
#> N = 1, M = 5 machine precision = 2.22045e-16
#> At X0, 0 variables are exactly at the bounds
#> At iterate 0 f= 115.38 |proj g|= 2.1965
#> At iterate 5 f = 115.07 |proj g|= 2.4538e-05
#>
#> iterations 5
#> function evaluations 7
#> segments explored during Cauchy searches 5
#> BFGS updates skipped 0
#> active bounds at final generalized Cauchy point 0
#> norm of the final projected gradient 2.45385e-05
#> final function value 115.069
#>
#> F = 115.069
#> final value 115.069342
#> converged
md_bac <- fit_bac$model_data
n_obs_bac <- n_obs_units(fit_bac)
gamma_lab_bac <- md_bac$term_data$info$gamma$gamma_label
trt_labs <- grep("^trt_linear_", gamma_lab_bac, value = TRUE)
drug_lab <- trt_labs[endsWith(trt_labs, "drug")]
if (!length(drug_lab)) {
drug_lab <- trt_labs[[1L]]
}
fit_bac$par_info[, c("label", "mle", "mle_internal")]
#> label mle mle_internal
#> 1 ID_iid 1.292995 0.2569615Swab-level scores via the same helpers as the case-crossover example.
a_bac <- phi_gamma(fit_bac, coef = drug_lab)
inf_bac <- influence_scores(fit_bac, a_bac, batch_size = 32L)
gamma_hat_bac <- inf_bac$gamma_hat
phi_hat_bac <- inf_bac$phi
psi_bac <- inf_bac$psi
c(target = drug_lab, phi_hat = phi_hat_bac, n_swabs = n_obs_bac)
#> target phi_hat n_swabs
#> "trt_linear_trtdrug" "-1.15406057362567" "220"
summary(psi_bac)
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> -0.0714118 -0.0093329 -0.0014039 -0.0003902 0.0068631 0.0994313S_bac <- amip_set(psi_bac, phi_hat_bac)
drop_idx_bac <- S_bac$idx
amip_alpha_bac <- S_bac$alpha
amip_change_bac <- S_bac$approx_change
c(
phi_hat = phi_hat_bac,
n_drop = length(drop_idx_bac),
alpha = amip_alpha_bac,
approx_change = amip_change_bac
)
#> phi_hat n_drop alpha approx_change
#> -1.1540606 37.0000000 0.1681818 1.1564267
dropped_bac <- md_bac$term_data$data[drop_idx_bac, c("ID", "week", "trt"), drop = FALSE]
dropped_bac$psi <- psi_bac[drop_idx_bac]
table(dropped_bac$trt)
#>
#> placebo drug drug+
#> 19 18 0
xtabs(~ week + trt, dropped_bac)
#> trt
#> week placebo drug drug+
#> 0 1 2 0
#> 2 4 2 0
#> 4 0 5 0
#> 6 4 5 0
#> 11 10 4 0oldpar <- par(no.readonly = TRUE)
par(mar = c(2.5, 2.5, 1, 0.5), bty = "l", mgp = c(1.5, 0.5, 0))
dat_bac <- md_bac$term_data$data
trt_cols <- c(placebo = "#4C78A8", drug = "#F58518", "drug+" = "#54A24B")
pch_bac <- rep(19, n_obs_bac)
col_bac <- unname(trt_cols[as.character(dat_bac$trt)])
pch_bac[drop_idx_bac] <- 1
col_bac[drop_idx_bac] <- "firebrick"
plot(
seq_len(n_obs_bac), psi_bac,
pch = pch_bac, cex = 0.55, col = col_bac,
xlab = "swab", ylab = expression(psi[i])
)
legend(
"bottomright",
legend = names(trt_cols),
col = trt_cols, pch = 19, bty = "n", inset = 0
)
abline(h = 0, lty = 2, col = "grey50")Inner confirmation drops the nominated swabs with
ad_pack_drop(), keeps \(\theta\) at the original MLE, and
re-optimizes \(\gamma\).
if (!length(drop_idx_bac)) {
phi_drop_bac <- NA_real_
gamma_drop_bac <- NULL
message("No swabs dropped; skipping confirmation retape.")
} else {
af_drop_bac <- ad_pack_drop(
fit_bac,
drop_idx_bac,
num_threads = vignette_num_threads
)
io_drop_bac <- inner_opt(
parameters = as.numeric(fit_bac$par_info$mle_internal),
gamma = gamma_hat_bac,
ad_pack = af_drop_bac,
control = list(report.level = 0, report.freq = 0),
deriv = FALSE,
return_hessians = FALSE,
verbose = FALSE
)
gamma_drop_bac <- as.numeric(io_drop_bac$inner_opt$solution)
names(gamma_drop_bac) <- names(gamma_hat_bac)
phi_drop_bac <- sum(a_bac * gamma_drop_bac)
}
c(
phi_full = phi_hat_bac,
phi_dropped = phi_drop_bac,
actual_change = if (is.finite(phi_drop_bac)) phi_drop_bac - phi_hat_bac else NA_real_,
approx_change = if (length(drop_idx_bac)) amip_change_bac else NA_real_
)
#> phi_full phi_dropped actual_change approx_change
#> -1.154061 5.162091 6.316152 1.156427if (!length(drop_idx_bac)) {
fit_bac_drop <- NULL
phi_refit_bac <- NA_real_
message("No swabs dropped; skipping refit.")
} else {
bacteria_drop <- md_bac$term_data$data[-drop_idx_bac, , drop = FALSE]
c(
n_full = nrow(md_bac$term_data$data),
n_keep = nrow(bacteria_drop),
n_swab_drop = length(drop_idx_bac)
)
fit_bac_drop <- adlaplace(
formula = bac_formula,
data = bacteria_drop,
config = list(
num_threads = vignette_num_threads,
num_shards = bac_shards
),
control = list(
maxit = bac_maxit,
trace = 3,
REPORT = 5
),
verbose = FALSE,
hessian = FALSE
)
phi_refit_bac <- unname(fit_bac_drop$gamma[[drug_lab]])
fit_bac_drop$par_info[, c("label", "mle", "mle_internal")]
}
#> N = 1, M = 5 machine precision = 2.22045e-16
#> At X0, 0 variables are exactly at the bounds
#> At iterate 0 f= 72.158 |proj g|= 3.0927
#> At iterate 5 f = 71.093 |proj g|= 0.00020307
#>
#> iterations 7
#> function evaluations 9
#> segments explored during Cauchy searches 7
#> BFGS updates skipped 0
#> active bounds at final generalized Cauchy point 0
#> norm of the final projected gradient 3.61823e-08
#> final function value 71.0926
#>
#> F = 71.0926
#> final value 71.092560
#> converged
#> label mle mle_internal
#> 1 ID_iid 1.768907 0.5703617
c(
phi_full = phi_hat_bac,
phi_inner_drop = phi_drop_bac,
phi_refit = phi_refit_bac
)
#> phi_full phi_inner_drop phi_refit
#> -1.154061 5.162091 5.099038Treatment logit effects with 80% pointwise intervals from the inner Laplace approximation. The targeted drug-versus-placebo contrast is the first pair.
num_sim_bac <- if (vignette_full) 200L else 80L
sims_bac <- rmvnldl(n = num_sim_bac, fit = fit_bac)
q_trt <- function(sims, labs, probs = c(0.1, 0.5, 0.9)) {
apply(sims[, labs, drop = FALSE], 2L, stats::quantile, probs = probs)
}
q_full_bac <- q_trt(sims_bac, trt_labs)
q_drop_bac <- NULL
if (!is.null(fit_bac_drop)) {
sims_bac_drop <- rmvnldl(n = num_sim_bac, fit = fit_bac_drop)
q_drop_bac <- q_trt(sims_bac_drop, trt_labs)
}
pretty_trt <- sub("^trt_linear_trt", "", trt_labs)
ylim_bac <- range(q_full_bac, 0, finite = TRUE)
if (!is.null(q_drop_bac)) {
ylim_bac <- range(ylim_bac, q_drop_bac, finite = TRUE)
}
oldpar <- par(no.readonly = TRUE)
par(mar = c(3, 3, 0.5, 0.5), mgp = c(1.5, 0.5, 0), bty = "l")
plot(
NA, NA,
xlim = c(0.5, length(trt_labs) + 0.5),
ylim = ylim_bac,
xaxt = "n", xlab = "", ylab = "treatment effect (logit)"
)
axis(1, at = seq_along(trt_labs), labels = pretty_trt)
abline(h = 0, lty = 3, col = "grey50")
offset <- if (is.null(q_drop_bac)) 0 else 0.12
for (j in seq_along(trt_labs)) {
x0 <- j - offset
arrows(x0, q_full_bac[1L, j], x0, q_full_bac[3L, j], length = 0, col = "red", lwd = 2)
points(x0, q_full_bac[2L, j], pch = 19, col = "red")
if (!is.null(q_drop_bac)) {
x1 <- j + offset
arrows(x1, q_drop_bac[1L, j], x1, q_drop_bac[3L, j], length = 0, col = "darkblue", lwd = 2)
points(x1, q_drop_bac[2L, j], pch = 19, col = "darkblue")
}
}
legend(
"topright",
legend = c("full data", "AMIP swabs removed"),
col = c("red", "darkblue"),
pch = 19, lwd = 2, bty = "n"
)n_obs_units() for the domain
size.grad_obs_units inside influence_scores);
\(H\) comes from the fit pack via
inner_opt. Confirmation retapes a filtered likelihood with
ad_pack_drop().