The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
This vignette fits a Bayesian Bernoulli GLMM to the
bacteria data from MASS, then integrates
the Laplace-approximate marginal posterior for the random-effect
standard deviation with aghq. The formula interface
hides the AD / Laplace details: known-SD Gaussian priors on the former
fixed effects, an exponential prior on the child-level SD, and one call
to adlaplace() followed by aghq::aghq().
There are 220 swabs on 50 children (ID), a three-level
treatment, and follow-up week at 0, 2, 4, 6, and 11.
rate <- aggregate(present ~ week + trt, data = bacteria, FUN = mean)
cols <- c(placebo = "#4C78A8", drug = "#F58518", "drug+" = "#54A24B")
plot(
present ~ week, data = rate, type = "n",
ylim = c(0, 1), ylab = "proportion present"
)
for (g in levels(bacteria$trt)) {
d <- rate[rate$trt == g, ]
lines(d$week, d$present, type = "b", pch = 19, col = cols[[g]])
}
legend("bottomleft", legend = names(cols), col = cols, pch = 19, lty = 1, bty = "n")The hierarchical model is
\[ \begin{aligned} y_{ij} &\sim \mathrm{Bernoulli}(p_{ij}), \\ \mathrm{logit}(p_{ij}) &= \alpha + \mathbf{x}_{ij}^\top\boldsymbol{\beta} + u_i, \\ \alpha, \beta_k &\sim N(0, 10^2), \\ u_i &\sim N(0, \sigma^2), \\ \sigma &\sim \mathrm{Exp}(\text{median} = 1). \end{aligned} \]
Moving the intercept, treatment, and week coefficients into the random-effect block with known SDs leaves a one-dimensional outer parameter (\(\log\sigma\)). Nested Laplace then integrates the latent field; AGHQ integrates \(\log\sigma\).
fit <- adlaplace(
binom(present) ~
intercept(sd = 10) +
linear(trt, sd = 10) +
linear(week, sd = 10) +
iid(ID, init = 1) +
prior(theta = 0, dist = "exp", median = 1),
data = bacteria
)
summary(fit)
#> Laplace-approximate maximum likelihood fit (adlaplace)
#>
#> Call:
#> adlaplace(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), data = bacteria)
#>
#> Log-likelihood: -115.1 df: 1 n: 220
#> Convergence code: 0 max|gradient|: 2.67e-05
#>
#> Coefficients (natural scale, 95% CI):
#> Estimate Std. Error lower upper
#> ID_iid 1.293 NA 0.7098 2.3554
#>
#> Random effects: 54 coefficientsintercept(sd = 10), linear(trt, sd = 10),
and linear(week, sd = 10) are known-SD Gaussian priors (no
estimated \(\theta\)).
iid(ID) supplies the child intercepts and their SD.
prior(theta = 0, ...) puts an exponential prior on the
first (0-based) theta — here the only one, ID_iid.
adlaplace() returns the MAP of \(\log\sigma\) under that prior. The outer
vector has length one:
aghq_ff(fit) wraps the same Laplace objective used by
adlaplace() for aghq. Use
negate = TRUE because the wrappers return the negated log
posterior.
ff <- aghq_ff(fit)
quad <- aghq::aghq(
ff = ff,
k = aghq_k,
startingvalue = fit$par_info$mle_internal,
control = aghq::default_control(negate = TRUE)
)
summary(quad)
#> AGHQ on a 1 dimensional posterior with 5 quadrature points
#>
#> The posterior mode is: 0.2569348
#>
#> The log of the normalizing constant/marginal likelihood is: -115.2683
#>
#> The covariance matrix used for the quadrature is...
#> [,1]
#> [1,] 0.09360647
#>
#> Here are some moments and quantiles for the transformed parameter:
#>
#> mean sd 2.5% median 97.5%
#> theta1 0.1709174 0.3463276 -0.707282 0.192187 0.717169
mode_log_sigma <- as.numeric(quad$optresults$mode)
c(log_sigma = mode_log_sigma, sigma = exp(mode_log_sigma))
#> log_sigma sigma
#> 0.2569348 1.2929608naw <- quad$normalized_posterior$nodesandweights
nodes <- as.numeric(naw[[1L]])
w <- exp(as.numeric(naw$logpost_normalized))
w <- w / sum(w)
sigma <- exp(nodes)
ord <- order(sigma)
plot(
sigma[ord], w[ord],
type = "h", lwd = 3, col = "#4C78A8",
xlab = expression(sigma), ylab = "normalized weight"
)
abline(v = exp(mode_log_sigma), lty = 2)The nested approximation for a coefficient \(\gamma_j\) (intercept, treatment, week) is a mixture of normals: at AGHQ node \(\theta_k = \log\sigma_k\) with weight \(w_k\),
\[ \gamma_j \mid \theta_k, y \;\approx\; N\bigl(\hat\gamma_j(\theta_k),\; [H(\theta_k)^{-1}]_{jj}\bigr), \qquad p(\gamma_j \mid y) \;\approx\; \sum_k w_k\, \varphi\bigl(\gamma_j;\, \hat\gamma_j(\theta_k),\, [H(\theta_k)^{-1}]_{jj}\bigr). \]
gamma_info <- fit$model_data$term_data$info$gamma
coef_idx <- which(gamma_info$model != "iid")
coef_lab <- gamma_info$gamma_label[coef_idx]
ctrl_mix <- list(
maxit = if (vignette_full) 100L else 20L,
report.level = 0,
report.freq = 0
)
mix_mean <- matrix(NA_real_, length(nodes), length(coef_idx))
mix_sd <- mix_mean
colnames(mix_mean) <- colnames(mix_sd) <- coef_lab
gamma_start <- fit$gamma
for (k in seq_along(nodes)) {
ll <- log_lik_laplace(
x = nodes[[k]],
ad_pack = fit$ad_pack,
config = fit$config,
gamma = gamma_start,
deriv = FALSE,
control = ctrl_mix
)
gamma_start <- ll$inner_opt$solution
names(gamma_start) <- names(fit$gamma)
half <- laplace_half_H_inv(ll)
mix_mean[k, ] <- gamma_start[coef_lab]
mix_sd[k, ] <- sqrt(Matrix::rowSums(half[coef_idx, , drop = FALSE]^2))
}
mix_dnorm <- function(x, mean, sd) {
dens <- vapply(
seq_along(w),
function(k) stats::dnorm(x, mean[[k]], sd[[k]]),
numeric(length(x))
)
as.numeric(dens %*% w)
}
mix_qnorm <- function(p, mean, sd) {
lo <- min(mean - 8 * sd)
hi <- max(mean + 8 * sd)
stats::uniroot(
function(q) sum(w * stats::pnorm(q, mean, sd)) - p,
lower = lo,
upper = hi
)$root
}
pretty_coef <- function(lab) {
if (identical(lab, "intercept")) {
return("intercept")
}
if (startsWith(lab, "trt_linear_")) {
lev <- sub("^trt_linear_trt", "", lab)
return(paste0("trt: ", lev, " vs placebo"))
}
if (startsWith(lab, "week_linear_")) {
return("week")
}
lab
}int_lab <- coef_lab[gamma_info$model[coef_idx] == "intercept"]
trt_lab <- coef_lab[gamma_info$term[coef_idx] == "trt"]
oldpar <- par(no.readonly = TRUE)
par(mfrow = c(1, 2), mar = c(4, 4, 1.5, 0.5))
int_m <- mix_mean[, int_lab]
int_s <- mix_sd[, int_lab]
xr <- seq(min(int_m - 4 * int_s), max(int_m + 4 * int_s), length.out = 200)
plot(
xr, mix_dnorm(xr, int_m, int_s),
type = "l", lwd = 2, col = "#4C78A8",
xlab = "intercept", ylab = "density"
)
trt_cols <- unname(cols[c("drug", "drug+")])
lo_trt <- mix_mean[, trt_lab, drop = FALSE] - 4 * mix_sd[, trt_lab, drop = FALSE]
hi_trt <- mix_mean[, trt_lab, drop = FALSE] + 4 * mix_sd[, trt_lab, drop = FALSE]
xg <- seq(min(lo_trt), max(hi_trt), length.out = 200)
dens_trt <- lapply(trt_lab, function(lab) {
mix_dnorm(xg, mix_mean[, lab], mix_sd[, lab])
})
ymax <- max(unlist(dens_trt))
plot(
xg, dens_trt[[1]],
type = "l", lwd = 2, col = trt_cols[[1]],
xlab = "treatment effect (logit)", ylab = "density",
ylim = c(0, ymax)
)
if (length(trt_lab) > 1L) {
lines(xg, dens_trt[[2]], lwd = 2, col = trt_cols[[2]])
}
legend(
"topright",
legend = vapply(trt_lab, pretty_coef, character(1)),
col = trt_cols[seq_along(trt_lab)],
lty = 1, lwd = 2, bty = "n"
)Posterior quantiles invert the mixture CDF. The child-level SD uses the AGHQ nodes (a discrete mixture on \(\log\sigma\)). Child-specific IID intercepts are omitted.
probs <- c(0.025, 0.5, 0.975)
q_coef <- t(vapply(
coef_lab,
function(lab) {
vapply(probs, mix_qnorm, numeric(1), mean = mix_mean[, lab], sd = mix_sd[, lab])
},
numeric(length(probs))
))
sigma_q <- vapply(probs, function(p) {
o <- order(sigma)
cw <- cumsum(w[o])
sigma[o][which(cw >= p)[1L]]
}, numeric(1))
post_q <- data.frame(
parameter = c(vapply(coef_lab, pretty_coef, character(1)), "sigma (ID)"),
`2.5%` = c(q_coef[, 1L], sigma_q[[1L]]),
`50%` = c(q_coef[, 2L], sigma_q[[2L]]),
`97.5%` = c(q_coef[, 3L], sigma_q[[3L]]),
check.names = FALSE
)
knitr::kable(post_q, digits = 3)| parameter | 2.5% | 50% | 97.5% | |
|---|---|---|---|---|
| intercept | intercept | 1.791 | 2.807 | 3.998 |
| trt_linear_trtdrug | trt: drug vs placebo | -2.485 | -1.148 | 0.133 |
| trt_linear_trtdrug+ | trt: drug+ vs placebo | -2.066 | -0.705 | 0.587 |
| week_linear_week | week | -0.230 | -0.133 | -0.040 |
| sigma (ID) | 0.539 | 1.293 | 1.958 |
Subject-specific fitted probabilities at the MAP use
fitted(fit) (modes of all random effects, including the
former fixed effects).
bacteria$p_hat <- plogis(fitted(fit))
fit_rate <- aggregate(p_hat ~ week + trt, data = bacteria, FUN = mean)
plot(
p_hat ~ week, data = fit_rate, type = "n",
ylim = c(0, 1), ylab = "probability present"
)
for (g in levels(bacteria$trt)) {
d <- fit_rate[fit_rate$trt == g, ]
o <- rate[rate$trt == g, ]
lines(d$week, d$p_hat, col = cols[[g]], lwd = 2)
points(o$week, o$present, col = cols[[g]], pch = 19)
}
legend("bottomleft", legend = names(cols), col = cols, lty = 1, lwd = 2, pch = 19, bty = "n")set.seed(1)
re <- sim_iid(fit, n = if (vignette_full) 400L else 80L)
pw <- re$ID$pointwise
ord <- order(pw[, "0.5"])
pw <- pw[ord, ]
idx <- seq_along(ord)
plot(
idx, pw[, "0.5"],
ylim = range(pw),
xlab = "child (ordered by median)",
ylab = "random intercept",
pch = 19, cex = 0.6
)
arrows(idx, pw[, "0.1"], idx, pw[, "0.9"], length = 0, col = "grey50")
abline(h = 0, lty = 2)These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.