--- title: "Random-effects meta-analysis with known sampling variances" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Random-effects meta-analysis with known sampling variances} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") if (!"package:drmTMB" %in% search()) { library(drmTMB) } ``` A random-effects meta-analysis pools effect sizes from several studies while acknowledging that the studies differ. Each study reports an effect size together with its sampling variance, which is treated as **known**. The model estimates two things on top of that known uncertainty: a pooled mean effect and the between-study heterogeneity. This is a specialist route for data that already are effect sizes with known sampling variances; for raw observations, start with [Distributional regression with drmTMB](drmTMB.html) instead. In `drmTMB` this is ordinary Gaussian distributional regression with a known sampling covariance. There is no separate meta-analysis family. You fit `family = gaussian()`, you supply the known per-study variances with `meta_V()` inside the location formula, and the between-study heterogeneity is the residual scale `sigma`. Throughout, `Normal(a, b)` uses the variance (not the standard deviation) as its second argument. ## The model Write \(y_i\) for the observed effect size from study \(i\) and \(v_i\) for its **known** sampling variance. The random-effects model is \[ y_i \mid \mu, \tau, v_i \;\sim\; \operatorname{Normal}\!\left(\mu,\; v_i + \tau^2\right), \qquad i = 1, \ldots, K. \] Each study sees the same pooled mean \(\mu\) but its own total variance \(v_i + \tau^2\): the known sampling variance \(v_i\) that the primary study already quantified, plus a shared between-study variance \(\tau^2\) that the meta-analysis estimates. When \(\tau = 0\) every study is just a noisy measurement of one common effect (a fixed-effect, or common-effect, meta-analysis); when \(\tau > 0\) the true study effects themselves scatter around \(\mu\). The two unknowns map onto the two `drmTMB` distributional parameters: | Meta-analysis quantity | Symbol | `drmTMB` parameter | How to read it | |---|---|---|---| | pooled effect | \(\mu\) | `mu` intercept | the average effect across studies | | known sampling variance | \(v_i\) | `meta_V(V = vi)` | supplied, not estimated | | between-study SD | \(\tau\) | `sigma` | how much true effects differ across studies | | between-study variance | \(\tau^2\) | `sigma^2` | the heterogeneity variance \(\tau^2\) | The matching R syntax is ```r drmTMB( bf(yi ~ 1 + meta_V(V = vi), sigma ~ 1), family = gaussian(), data = dat ) ``` Two points are worth stating plainly. First, `vi` must be a **variance**. If your dataset stores standard errors, square them (`vi <- se^2`) before fitting. Second, the residual scale `sigma` *is* the between-study heterogeneity \(\tau\). Meta-analysts usually call this quantity `tau` and report `tau^2`. `drmTMB` keeps the public parameter name `sigma` so that meta-analysis uses exactly the same distributional grammar as every other Gaussian model in the package; you recover the familiar `tau^2` simply by squaring `sigma`. The marker `meta_V()` puts the *known* variance into the likelihood; `sigma` estimates the *unknown* variance that is left over. This is an implemented, source-tested interface and the example below checks its ML fit against `metafor`. It does **not** currently have a registered `meta_V()` capability-ledger cell, so this tutorial does not assign it an evidence tier or make an interval-coverage claim. Treat fitted-model intervals as the methods returned by the current fit, and state their source when reporting them. ## A simulated dataset We simulate `K = 30` studies. Each study has a true effect drawn around a common mean, and we observe that true effect with known sampling error. ```{r} set.seed(101) K <- 30 mu_true <- 0.40 # pooled effect tau_true <- 0.30 # between-study SD # Known sampling variances: larger studies (smaller vi) and smaller studies. vi <- runif(K, 0.02, 0.10) # True study effects scatter around mu_true with SD tau_true. theta_i <- rnorm(K, mean = mu_true, sd = tau_true) # Observed effect sizes: each true effect seen with its known sampling error. yi <- rnorm(K, mean = theta_i, sd = sqrt(vi)) dat <- data.frame(study = factor(seq_len(K)), yi = yi, vi = vi) head(dat) ``` The data frame has one row per study: the effect size `yi` and its known sampling variance `vi`. ## Fitting the model ```{r} fit <- drmTMB( bf(yi ~ 1 + meta_V(V = vi), sigma ~ 1), family = gaussian(), data = dat ) summary(fit) ``` Before reading the coefficients, confirm the fit is sound. A clean optimisation and a positive-definite Hessian are necessary diagnostics for routine Wald output, but they do not by themselves establish that a `sigma` interval is finite, usable, or coverage-valid. ```{r} is_converged(fit) # optimizer convergence is_converged(fit, include_hessian = TRUE) # also requires a positive-definite Hessian ``` ```{r} diagnostics <- check_drm(fit) diagnostics[, c("check", "status", "value", "message")] ``` If `is_converged(fit, include_hessian = TRUE)` is `FALSE`, use the status and message in `diagnostics` to identify the failed check before interpreting Wald intervals. A Hessian warning is an inference warning, not by itself proof that the fitted pooled effect is unusable. ## The pooled effect The pooled effect is the `mu` intercept. Its Wald confidence interval comes from `confint()`. ```{r} mu_hat <- coef(fit, "mu")[["(Intercept)"]] mu_hat confint(fit, parm = "mu:(Intercept)")[, c("parm", "lower", "upper")] ``` In this run the pooled estimate is about `r round(mu_hat, 3)`, which sits near the simulated `mu_true = 0.40`. The interval reflects uncertainty in the mean *after* both the known sampling variances and the estimated between-study heterogeneity have been accounted for. ## Between-study heterogeneity The between-study SD \(\tau\) is the residual scale `sigma`. Because the `sigma` formula here is intercept-only, every study shares the same value, so we take the first element. Squaring it gives the heterogeneity variance \(\tau^2\) that meta-analysis reports. ```{r} tau_hat <- sigma(fit)[1] c(tau = unname(tau_hat), tau_squared = unname(tau_hat^2)) ``` Heterogeneity is easier to communicate as a proportion. \(I^2\) is the share of the total variation that is between-study rather than sampling noise. With the usual "typical" within-study variance \(\tilde v\) of Higgins and Thompson (2002), \[ \tilde v = \frac{(K-1)\sum_i w_i}{\left(\sum_i w_i\right)^2 - \sum_i w_i^2}, \qquad w_i = 1 / v_i, \qquad I^2 = \frac{\tau^2}{\tau^2 + \tilde v}. \] ```{r} w <- 1 / dat$vi v_typical <- ((K - 1) * sum(w)) / (sum(w)^2 - sum(w^2)) I2 <- tau_hat^2 / (tau_hat^2 + v_typical) c( tau_squared = unname(tau_hat^2), typical_v = v_typical, I2_percent = unname(100 * I2) ) ``` An \(I^2\) of this size means a substantial fraction of the variation among the observed effect sizes reflects genuine differences between studies, not just within-study sampling error. The pooled mean is still meaningful, but it is a mean of effects that really do differ. ## Cross-check against metafor The same model can be fitted with `metafor::rma()` using maximum likelihood. It should agree with `drmTMB`, because both fit the identical random-effects likelihood \(y_i \sim \operatorname{Normal}(\mu, v_i + \tau^2)\). This is a useful sanity check when you first adopt the `drmTMB` spelling. ```{r} if (requireNamespace("metafor", quietly = TRUE)) { rma_fit <- metafor::rma(yi = yi, vi = vi, method = "ML", data = dat) comparison <- data.frame( quantity = c("pooled mu", "tau^2", "I^2 (%)"), drmTMB = c(mu_hat, tau_hat^2, 100 * I2), metafor = c(as.numeric(rma_fit$beta), rma_fit$tau2, rma_fit$I2) ) print(comparison, row.names = FALSE, digits = 4) } ``` The two engines return the same pooled effect and the same heterogeneity variance. `drmTMB` is doing ML random-effects meta-analysis; it simply spells the known sampling variance as `meta_V()` and the heterogeneity as `sigma`. ## REML for the heterogeneity The ML estimate of \(\tau^2\) is known to be biased downward, because it does not account for the degrees of freedom spent estimating \(\mu\). When the mean model is fixed and you only want a better heterogeneity estimate, restricted maximum likelihood (`REML = TRUE`) is the standard remedy. Keep ML (`REML = FALSE`, the default) whenever you intend to compare different fixed-effect mean models with AIC or BIC, since restricted likelihoods are not comparable across different mean structures. ```{r} fit_reml <- drmTMB( bf(yi ~ 1 + meta_V(V = vi), sigma ~ 1), family = gaussian(), data = dat, REML = TRUE ) data.frame( estimator = c("ML", "REML"), pooled_mu = c(coef(fit, "mu")[[1]], coef(fit_reml, "mu")[[1]]), tau = c(sigma(fit)[1], sigma(fit_reml)[1]), tau_squared = c(sigma(fit)[1]^2, sigma(fit_reml)[1]^2) ) ``` The REML between-study variance is slightly larger than the ML one, as expected. ## Meta-regression: moderators on the mean If a study-level covariate might explain part of the variation in effect sizes, add it to the `mu` formula. This is a random-effects meta-regression: the known sampling variances stay in `meta_V()`, `sigma` becomes the *residual* (after moderators) between-study SD, and the new coefficient measures how the effect size changes with the moderator. ```{r} set.seed(202) dat$dose <- scale(runif(K, 1, 10))[, 1] # a study-level moderator # Give the effect size a genuine dependence on the moderator. dat$yi <- dat$yi + 0.25 * dat$dose fit_mr <- drmTMB( bf(yi ~ 1 + dose + meta_V(V = vi), sigma ~ 1), family = gaussian(), data = dat ) coef(fit_mr, "mu") ``` The `dose` coefficient is the change in the pooled effect per one-SD change in the moderator. After fitting a moderator, the residual `sigma` is the between-study heterogeneity that the moderator did *not* explain; comparing it with the no-moderator `sigma` shows how much heterogeneity the moderator absorbed. ```{r} c( residual_tau_no_moderator = unname(sigma(fit)[1]), residual_tau_with_moderator = unname(sigma(fit_mr)[1]) ) ``` ## Multiple effect sizes per study The worked example above has one effect size per study, so `sigma` carries the whole between-study story. When a study contributes several effect sizes, two levels of variation appear: a study-level random effect for the studies, and a residual for the effect sizes within a study. Those are different questions, and `drmTMB` keeps them in different places: ```r # Schematic: several effect sizes per study (not evaluated here). drmTMB( bf(yi ~ 1 + moderator + (1 | study) + meta_V(V = vi), sigma ~ 1), family = gaussian(), data = dat_repeated ) ``` Here `(1 | study)` is the between-study random effect and `sigma` is the within-study residual heterogeneity, while `meta_V(V = vi)` still supplies the known sampling variances. A grouping factor used in `(1 | study)` must have at least one study with repeated rows; a data set with exactly one row per study is the single-level model shown above, where `sigma` alone represents between-study heterogeneity. ## Layered heterogeneity: an experimental contract When repeated effects are nested in studies, three SD layers answer different questions: known sampling uncertainty in `meta_V(V = V)`, residual heterogeneity in `sigma`, and the SD of a study- or effect-level location random effect in `sd(group)`. The following formulas are accepted by the current Gaussian implementation: ```{r layered-meta-syntax, eval=FALSE} # Study-level location-SD regression (LSS): z_study is constant within study. drmTMB( bf( yi ~ x + (1 | study) + meta_V(V = V), sigma ~ z, sd(study) ~ z_study ), family = gaussian(), data = dat ) # Nested effect-level location-SD regression (LSSS): effect is nested in study # and has repeated rows. drmTMB( bf( yi ~ x + (1 | study) + (1 | effect) + meta_V(V = V), sigma ~ z, sd(study) ~ z_study, sd(effect) ~ z_effect ), family = gaussian(), data = dat ) ``` These formulas are an **experimental local-contract surface**, not a claim of calibrated inference. In the Arc 7B local sentinel, the dense-`V` LSS study-SD profiles had non-finite endpoints despite a positive-definite Hessian. Before using these layered fits for interval-based conclusions, inspect every profile target and consult the current development evidence. A random term in `sigma`, such as `sigma ~ z + (1 | study)`, is a different double-hierarchical model: it models variation in residual SD, not variation in the SD of the location random effect. ## Known sampling variance is not a weight Inverse-variance weights and known sampling variances answer different questions, and `meta_V()` is not the same as the top-level `weights` argument. A likelihood weight multiplies a study's contribution to the log-likelihood: \[ \ell(\theta) = \sum_i w_i \, \ell_i(\theta). \] A known sampling variance enters the *covariance* of the response: \[ y_i \sim \operatorname{Normal}(\mu,\; v_i + \tau^2). \] So `weights = 1 / vi` is **not** the random-effects meta-analysis model. It rescales how much each row counts toward the likelihood; it does not put `vi` into the modelled sampling variance, and it does not let `tau^2` be estimated on top of the known variances. For meta-analysis with known sampling variances, use `meta_V(V = vi)`. Reserve `weights =` for genuine likelihood weights such as externally defined case weights. ## Correlated effect sizes: a small dense covariance example Use a dense `V` when the *sampling errors* of different effect sizes are known to be correlated, for example because they share participants or a control group. This remains an observation-level measurement-error input. It is not a weight, and it does not create a latent study, phylogenetic, spatial, or relatedness effect. For \(n\) effect sizes, the dense route fits \[ \mathbf y \sim \operatorname{Normal}\!\left(X\boldsymbol\beta, V + \sigma^2 I_n\right), \] where the supplied \(V\) is the known sampling covariance and `sigma` is the estimated residual heterogeneity. The following small example is executable; the off-diagonal entries say that nearby effect sizes have correlated sampling errors. ```{r} set.seed(303) n_dense <- 8 dat_dense <- data.frame( yi = 0.25 + 0.10 * seq_len(n_dense) + stats::rnorm(n_dense, sd = 0.04), x = seq_len(n_dense) ) V_dense <- 0.012 * outer( seq_len(n_dense), seq_len(n_dense), function(i, j) 0.55^abs(i - j) ) # A useful preflight: V is numeric, n by n, symmetric, and PSD. stopifnot( is.numeric(V_dense), identical(dim(V_dense), c(nrow(dat_dense), nrow(dat_dense))), isTRUE(all.equal(V_dense, t(V_dense))), min(eigen(V_dense, symmetric = TRUE, only.values = TRUE)$values) >= 0 ) fit_dense <- drmTMB( bf(yi ~ x + meta_V(V = V_dense), sigma ~ 1), family = gaussian(), data = dat_dense ) check_drm(fit_dense) ``` The order is part of the data contract: row and column `i` of `V_dense` must describe the sampling covariance for row `i` of `dat_dense`. Supply one row and one column per original data row; do not reorder the data after building `V`. For a dense matrix, `drmTMB` first applies the ordinary model-frame exclusions (for example a missing response or predictor), then removes the corresponding rows *and columns* of `V` together. Let the model frame make that exclusion and keep the original ordering rather than filtering the data and covariance matrix by separate rules. The retained matrix must have finite entries, be symmetric, have a non-negative diagonal, and be positive semidefinite. ## Notes on the function names * `meta_V(V = V)` is the current marker for known sampling variance or covariance. The argument may be a column of **variances** (not standard errors), a vector, a diagonal matrix, or a dense covariance matrix when the effect sizes are correlated. A dense matrix must follow the row order and validity contract in the preceding example. * `meta_known_V(V = V)` is a deprecated alias kept only for backward compatibility. It routes to the same additive known-variance likelihood but emits a deprecation warning; prefer `meta_V()` in new code. * There is intentionally no `meta_gaussian()` family and no `tau ~` syntax. Meta-analysis reuses `family = gaussian()` and `sigma ~ ...` so that it shares the distributional-regression grammar with the rest of `drmTMB`. ## Reference Higgins, J. P. T., and Thompson, S. G. (2002). Quantifying heterogeneity in a meta-analysis. *Statistics in Medicine*, 21(11), 1539-1558.