This vignette fits a time-stratified case-crossover model to daily mortality and PM10 in London (2002–2006), using the example dataset from Tobias et al. (2024).
Strata are defined by day-of-week within month and year via
dirichlet_multinom(..., by = "year_month_dow"). PM10 enters
through an integrated Wiener process smooth
(iwp(pm10, ...)), with relative humidity as a linear fixed
effect. Fitting is done with adlaplace().
## city date year month day dow all tmean rh pm10
## 1 London 2002-01-01 2002 Jan 1 Tue 199 -0.22500001 75.6783 71.70000
## 2 London 2002-01-02 2002 Jan 2 Wed 231 0.08749995 77.5250 40.20000
## 3 London 2002-01-03 2002 Jan 3 Thu 210 0.85000002 81.3250 41.80000
## 4 London 2002-01-04 2002 Jan 4 Fri 203 0.53750002 85.4500 50.40000
## 5 London 2002-01-05 2002 Jan 5 Sat 224 4.25000000 93.5250 49.36429
## 6 London 2002-01-06 2002 Jan 6 Sun 198 7.06874990 96.4000 31.10000
## 0% 25% 50% 75% 100%
## 8.10000 30.70000 39.72857 50.50000 99.60000
## 0% 25% 50% 75% 100%
## -1.400507 7.506250 11.472748 16.198438 28.174999
Case-crossover strata absorb the intercept, so the formula uses
0 + on the right-hand side.
london$temp_div10 <- london$tmean / 10
cc_formula <- dirichlet_multinom(
all,
by = c("year", "month", "dow"),
init = 0.2,
parscale = 5
) ~ 0 + rh + iwp(
pm10,
p = 2,
knots = seq(0, 100, by = 10),
ref_value = 40,
init = 0.001, parscale = 100
) + iwp(
tmean,
p = 3, knots = seq(-2, 30, by = 2),
init = 0.01, parscale = 100,
ref_value = 20
)fit <- adlaplace(
formula = cc_formula,
data = london,
num_threads = 2L,
num_groups = 50L,
config = list(transform_theta = TRUE),
control = list(
maxit = 200, trace = 0, REPORT = 1,
factr = 1e3,
pgtol = 1e-8
),
verbose = FALSE
)## Laplace-approximate maximum likelihood fit (adlaplace)
##
## Call:
## adlaplace(formula = cc_formula, data = london, config = list(transform_theta = TRUE),
## num_threads = 2L, num_groups = 50L, control = list(maxit = 200,
## trace = 0, REPORT = 1, factr = 1000, pgtol = 1e-08),
## verbose = FALSE)
##
## Coefficients (natural scale):
## rh_linear all_dirichlet_multinom_sd pm10_iwp
## 0.0006 0.0169 0.0001
## tmean_iwp
## 0.0018
##
## Log-likelihood: -5334 df: 4 n: 1825 random effects: 29
## Laplace-approximate maximum likelihood fit (adlaplace)
##
## Call:
## adlaplace(formula = cc_formula, data = london, config = list(transform_theta = TRUE),
## num_threads = 2L, num_groups = 50L, control = list(maxit = 200,
## trace = 0, REPORT = 1, factr = 1000, pgtol = 1e-08),
## verbose = FALSE)
##
## Log-likelihood: -5334 df: 4 n: 1825
## Convergence code: 0 max|gradient|: 0.0026
##
## Coefficients (natural scale, 95% CI):
## Estimate Std. Error lower upper
## rh_linear 0.0006 2e-04 0.0002 0.0011
## all_dirichlet_multinom_sd 0.0169 NA 0.0135 0.0213
## pm10_iwp 0.0001 NA 0.0000 0.0059
## tmean_iwp 0.0018 NA 0.0008 0.0041
##
## Random effects: 29 coefficients
num_sim <- 200L
num_grid <- 101L
gamma_sims <- rmvnldl(n = num_sim, fit = fit)
smooth_vars <- unique(fit$model_data$term_data$info$gamma$term)
par(mfrow = c(1, 2), mar = c(2.25, 2.25, 0, 0), bty = "l", mgp = c(1, 0.5, 0))
for (D in smooth_vars) {
sim_here <- sim_random(
D,
fit = fit, gamma_sims = gamma_sims, num_grid = num_grid
)
rr <- exp(sim_here$sim)
if (requireNamespace("GET", quietly = TRUE)) {
q <- GET::central_region(
GET::create_curve_set(list(obs = rr)),
coverage = 0.8
)[, c("lo", "central", "hi")]
} else {
q <- t(apply(rr, 1, quantile, probs = c(0.1, 0.5, 0.9)))
}
matplot(
sim_here$x, rr,
type = "l", lty = 1, col = "#00000010",
xlab = D, ylab = "RR", xaxs = "i", yaxs = "i",
ylim = quantile(rr, prob = c(0.005, 0.95))
)
matlines(sim_here$x, q, lty = c(2, 1, 2), col = "red")
}Build model_data, shard map, and ad_pack
without running outer optimization, then maximize the Laplace profile
likelihood with optim directly.
md <- model_data(cc_formula, data = london, verbose = FALSE)
config_dev <- list(
transform_theta = TRUE,
num_threads = 2L,
verbose = FALSE
)
config_dev$obs_groups <- obs_groups(
A = md$term_data$A,
elgm_matrix = md$term_data$elgm_matrix,
num_groups = 50L,
min_groups = min(50L, 2L * 4L)
)
par_info <- md$term_data$info$parameters
cache <- new.env(parent = emptyenv())
cache$gamma <- rep(0, nrow(md$term_data$info$gamma))
control_dev <- list(
maxit = 200, trace = 0, REPORT = 1,
factr = 1e3,
pgtol = 1e-8,
parscale = par_info$parscale
)
control_inner <- list(report.level = 0)
af <- ad_pack(md, config_dev, num_threads = 2L)
slotNames(af)## [1] "ptr" "group_sparsity" "outer" "inner"
## [5] "map_outer" "map_inner" "parallel_map" "chol_inner"
## [9] "chol_inner_list" "sizes" "info"
## [1] 420 50
## term model label init lower upper
## 1 rh linear rh_linear 0.000000 -Inf Inf
## 2 all dirichlet_multinom all_dirichlet_multinom_sd -1.609438 -Inf Inf
## 3 pm10 iwp pm10_iwp -6.907755 -20.72327 Inf
## 4 tmean iwp tmean_iwp -4.605170 -20.72327 Inf
## parscale log
## 1 1 FALSE
## 2 5 TRUE
## 3 100 TRUE
## 4 100 TRUE
At starting values, evaluate the full log-density and its gradient
over all parameters (beta, inner gamma, and
theta).
sz <- af@sizes
gamma0 <- rep(0, nrow(md$term_data$info$gamma))
x_full <- c(
par_info$init[seq_len(sz["beta"])],
gamma0,
par_info$init[seq(sz["beta"] + 1L, length.out = sz["theta"])]
)
# Serial dens/grad/hess on a dens-safe clone (multi-thread affinity cleared).
dens_ptr <- clone_ad_pack_ptr(af@ptr)
shards <- seq.int(from = 0L, length.out = adlaplace:::n_groups(dens_ptr))
by_shard <- vapply(
shards,
function(s) joint_log_dens(dens_ptr, x_full, ad_shards = s),
numeric(1)
)
by_shard[1:5]## [1] -2785.855 -1890.515 -2484.733 -4613.652 -2210.951
## [1] -6907.58198 -5915.20166 -71.40109 -64.52488 266320.89838
## [1] 6733.595
## [1] 6733.595
## [1] 6.016247e+01 3.017066e+03 7.925580e+02 4.325034e+01 1.816206e-01
## [6] -9.138977e+03 -5.996974e+03 -3.562910e+03 -1.699437e+03 -5.317709e+02
## [11] -2.426920e+01 -1.589603e+03 -3.087635e+02 -6.337118e+02 -7.072894e+02
## [16] -6.355527e+02 -5.199163e+02 -3.996765e+02 -2.562626e+02 -1.222508e+02
## [21] -4.154798e+01 -5.795445e+00 -8.536926e-02 -5.010426e+02 -1.784458e+02
## [26] -4.580742e+01 -5.072920e+00 -2.398693e-03 -3.239870e+02 -7.960846e+02
## [31] 1.334465e+03 1.000000e+01 1.600000e+01
With outer parameters fixed at their starting values,
log_lik_laplace() optimizes random effects and returns the
log-likelihood.
x_outer <- par_info$init
lik <- log_lik_laplace(
x = x_outer,
ad_pack = af,
config = modifyList(config_dev, list(verbose = TRUE)),
gamma = gamma0,
control = list(report.level = 2, report.freq = 1),
deriv = TRUE
)## inner_opt: threads = 2, shards = 53, params = 33 (beta = 1, gamma = 29, theta = 3)
## Beginning optimization
##
## iter f nrm_gr status
## 1 6729.011705 1298.802480 Continuing
## 2 6728.943320 18.431171 Continuing
## 3 6728.943305 0.004503 Continuing
## 4 6728.943305 0.000000 Continuing
##
## Iteration has terminated
## 4 6728.943305 0.000000 Success
##
## trace_hinv_t: threads = 2, shards = 53, params = 33
## trace_hinv_t: finished
## inner_opt: finished
## [1] 6888.477
## [1] -480.018802 1337.537165 1.464126 3.223225
## [1] -1.697941e-06 -6.855554e-06 6.603516e-07 -1.536926e-08 -1.994934e-05
## [6] 1.777748e-05
neg_log_lik is the negative profile log-likelihood after
inner optimization over random effects.