--- title: "Case-crossover models with adlaplace" author: "Patrick Brown" date: "`r Sys.Date()`" output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{Case-crossover models with adlaplace} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ## Introduction 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)](https://github.com/aureliotobias/casecrossover). 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()`. ```{r setup, include=FALSE} library(adlaplace) ``` ## Data ```{r data} data("london", package = "adlaplace") head(london) quantile(london$pm10, na.rm = TRUE) quantile(london$tmean, na.rm = TRUE) ``` ## Model specification Case-crossover strata absorb the intercept, so the formula uses `0 +` on the right-hand side. ```{r model-spec} 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 ```{r fit} 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 ) ``` ```{r summary} fit summary(fit) ``` ```{r plot_results} 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") } ``` ## AD development check Build `model_data`, shard map, and `ad_pack` without running outer optimization, then maximize the Laplace profile likelihood with `optim` directly. ```{r for_dev} 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) dim(config_dev$obs_groups) par_info bob <- stats::optim( par = par_info$init, fn = outer_fn, gr = outer_gr, method = "L-BFGS-B", control = control_dev, lower = par_info$lower, upper = par_info$upper, config = config_dev, ad_pack = af, cache = cache, control_inner = control_inner, hessian = TRUE ) ``` ## Joint log density At starting values, evaluate the full log-density and its gradient over all parameters (`beta`, inner `gamma`, and `theta`). ```{r joint-log-dens} 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] by_shard[seq(to = length(by_shard), length.out = 5)] sum(by_shard) joint_log_dens(dens_ptr, x_full) grad(dens_ptr, x_full) bob <- hessian(dens_ptr, x_full) ``` ## Likelihood With outer parameters fixed at their starting values, `log_lik_laplace()` optimizes random effects and returns the log-likelihood. ```{r 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 ) lik$neg_log_lik lik$deriv$d_neg_log_lik head(lik$inner_opt$solution) ``` `neg_log_lik` is the negative profile log-likelihood after inner optimization over random effects.