---
title: "Bayesian Meta-Analysis"
author: "František Bartoš"
date: "28th of April 2026"
output:
  rmarkdown::html_vignette:
    self_contained: yes
bibliography: ../inst/REFERENCES.bib
csl: ../inst/apa.csl
link-citations: true
vignette: >
  %\VignetteIndexEntry{Bayesian Meta-Analysis}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
  %\VignetteEngine{knitr::rmarkdown_notangle}
---

```{r child = "_vignette-nowrap.md", echo = FALSE, eval = TRUE}
```

```{r setup, include = FALSE}
source("vignette-cache.R", local = knitr::knit_global())

cached_fits <- vignette_cache(
  name     = "v02-bayesian-meta-analysis",
  objects  = c("fit1_brma", "fit2_brma", "fit3_brma"),
  packages = c("metafor", "metadat")
)

knitr::opts_chunk$set(
  collapse    = TRUE,
  comment     = "#>",
  eval        = vignette_cache_eval(cached_fits),
  message     = FALSE,
  warning     = FALSE,
  fig.width   = 7,
  fig.height  = 3.5,
  dev         = "png",
  fig.retina  = 3
)
if (.Platform$OS.type == "windows") {
  knitr::opts_chunk$set(dev.args = list(type = "cairo"))
}
```

```{r load-models, include = FALSE}
library("metafor")
library("RoBMA")

data("dat.bcg", package = "metadat")

dat <- metafor::escalc(
  ai = tpos, bi = tneg, ci = cpos, di = cneg,
  measure = "RR", data = dat.bcg
)

vignette_cache_load(cached_fits)
```

```{r precompute-models, include = FALSE, eval = FALSE}
library("metafor")
library("RoBMA")

data("dat.bcg", package = "metadat")

dat <- metafor::escalc(
  ai = tpos, bi = tneg, ci = cpos, di = cneg,
  measure = "RR", data = dat.bcg
)

fit1_brma <- brma(
  yi = yi, vi = vi, measure = "RR",
  data = dat, seed = 1
)
fit1_brma <- add_loo(fit1_brma)
fit1_brma <- add_marglik(fit1_brma)

fit2_brma <- brma(
  yi = yi, vi = vi, measure = "RR",
  mods = ~ ablat,
  data = dat, seed = 1
)
fit2_brma <- add_loo(fit2_brma)
fit2_brma <- add_marglik(fit2_brma)

fit3_brma <- brma(
  yi = yi, vi = vi, measure = "RR",
  mods = ~ ablat + alloc,
  data = dat, seed = 1
)
fit3_brma <- add_loo(fit3_brma)
fit3_brma <- add_marglik(fit3_brma)

vignette_cache_save(cached_fits)
```

This vignette is a starting point for using the `RoBMA` R package [@RoBMA].
We walk through the BCG vaccine dataset from the `metafor` package [@metafor], showing each `brma()` call alongside its `metafor::rma()` counterpart and building from a simple random-effects model up to meta-regression with a continuous and a categorical moderator.
The interface is intentionally close to the `metafor` package: the same effect-size columns, moderator formulas, and standard plotting and diagnostic functions apply.

All `brma()` calls keep the default prior distributions; see the [*Prior Distributions*](v01-prior-distributions.html) vignette for the prior definitions and customization options.

## Random-Effects Model

The BCG vaccine dataset from the `metadat` package contains 13 randomized trials on tuberculosis prevention.
`escalc()` computes log risk ratios `yi` and their sampling variances `vi`.

```{r escalc}
data("dat.bcg", package = "metadat")

dat <- metafor::escalc(
  ai = tpos, bi = tneg, ci = cpos, di = cneg,
  measure = "RR", data = dat.bcg
)
```

`metafor::rma()` fits the random-effects model.

```{r fit1-metafor}
fit1_metafor <- metafor::rma(yi, vi, data = dat, method = "REML")
fit1_metafor
```

The matching Bayesian fit reuses the same effect-size columns.
`measure = "RR"` tells `brma()` to use the default prior distributions for log risk ratios.
The default prior distributions are weakly informative and provide slight regularization toward zero.

```{r fit1-brma, eval = FALSE}
fit1_brma <- brma(
  yi = yi, vi = vi, measure = "RR",
  data = dat, seed = 1
)
```

`summary()` reports posterior means, credible intervals, and (suppressed here) MCMC convergence diagnostics.
Convergence diagnostics are included by default; chain-level visual diagnostics are available via `plot_diagnostic_trace()`, `plot_diagnostic_density()`, and `plot_diagnostic_autocorrelation()`.

```{r fit1-brma-summary}
summary(fit1_brma, include_mcmc_diagnostics = FALSE)
```

The posterior mean for the log risk ratio is close to the REML estimate from the `metafor` package; the slight pull toward zero reflects the default `Normal(0, 1)` prior distribution.
`pooled_effect()` returns the same summary, backtransformed to the risk-ratio scale via `transform = "EXP"`:

```{r fit1-brma-pooled}
pooled_effect(fit1_brma, transform = "EXP")
```

`plot()` visualizes the posterior distribution, and `prior = TRUE` overlays the prior distribution so the shift from prior to posterior is visible.

```{r fit1-brma-plot, fig.width = 5, fig.height = 3.5}
par(mar = c(4, 4, 1, 1))
plot(fit1_brma, parameter = "mu", prior = TRUE, xlim = c(-3, 3))
```

## Heterogeneity

`metafor::confint()` reports profile-likelihood confidence intervals for `tau^2`, `tau`, `I^2`, and `H^2`.
`summary_heterogeneity()` is the Bayesian counterpart, returning posterior summaries of the same quantities.

```{r heterogeneity-confint}
confint(fit1_metafor)
```

```{r heterogeneity-bayes}
summary_heterogeneity(fit1_brma)
```

## Funnel Plot

`funnel()` plots observed effect sizes against the fitted sampling distribution.
We turn off the `sampling_heterogeneity` argument, since the `RoBMA` R package shows the full sampling distribution under the model by default.

```{r funnel-plot, fig.width = 7, fig.height = 3.2}
par(mfrow = c(1, 2), mar = c(4, 4, 2, 1), cex.axis = 0.8, cex.lab = 0.8, cex.main = 0.75)
metafor::funnel(fit1_metafor, main = "metafor", xlim = c(-2, 1), ylim = c(0, 0.8))
funnel(fit1_brma, main = "RoBMA", xlim = c(-2, 1), ylim = c(0, 0.8), sampling_heterogeneity = FALSE)
```

## Meta-Regression with a Continuous Moderator

A continuous moderator can be added with the standard formula syntax via the `mods` argument.
`ablat` is the absolute latitude of the trial, frequently used to explain BCG effect-size variation.

```{r fit2-metafor}
fit2_metafor <- metafor::rma(yi, vi, mods = ~ ablat, data = dat)
fit2_metafor
```

```{r fit2-brma, eval = FALSE}
fit2_brma <- brma(
  yi = yi, vi = vi, measure = "RR",
  mods = ~ ablat,
  data = dat, seed = 1
)
```
```{r fit2-brma-summary}
summary(fit2_brma, include_mcmc_diagnostics = FALSE)
```

`regplot()` produces the familiar bubble plot with the fitted regression line.

```{r regplot-continuous, fig.width = 7, fig.height = 3.2}
par(mfrow = c(1, 2), mar = c(4, 4, 2, 1), cex.axis = 0.8, cex.lab = 0.8, cex.main = 0.75)
metafor::regplot(fit2_metafor, main = "metafor", xlim = c(10, 60), ylim = c(-2, 0.5))
regplot(fit2_brma, main = "RoBMA", xlim = c(10, 60), ylim = c(-2, 0.5))
```

`predict()` returns posterior summaries for user-specified moderator values.

```{r fit2-predict}
new_data <- data.frame(ablat = c(15, 35, 55))
predict(fit2_brma, newdata = new_data, type = "terms")
```

## Comparing Models with Bayes Factors

Marginal likelihoods enable model comparison via Bayes factors [@kass1995bayes].
After attaching marginal likelihoods to both fits with `add_marglik()`, `bf()` returns the Bayes factor for one model against another.

```{r add-marglik, eval = FALSE}
fit1_brma <- add_marglik(fit1_brma)
fit2_brma <- add_marglik(fit2_brma)
```
```{r bayes-factor}
bf(fit2_brma, fit1_brma)
```

The Bayes factor quantifies how much the data shift the relative predictive support between the two models.
Here we obtain strong evidence in favor of the model including the `ablat` predictor.

## Adding a Categorical Moderator

`alloc` is a three-level factor describing the trial allocation method (random, alternate, systematic).
The same formula syntax extends the meta-regression.

```{r fit3-metafor}
fit3_metafor <- metafor::rma(yi, vi, mods = ~ ablat + alloc, data = dat)
```

```{r fit3-brma, eval = FALSE}
fit3_brma <- brma(
  yi = yi, vi = vi, measure = "RR",
  mods = ~ ablat + alloc,
  data = dat, seed = 1
)
```
```{r fit3-brma-summary}
summary(fit3_brma, include_mcmc_diagnostics = FALSE)
```

The `RoBMA` R package also allows direct visualization of categorical moderators in the bubble plot
(the `metafor` package only supports a single dummy coefficient at the moment).

```{r regplot-categorical, fig.width = 7, fig.height = 3.2}
par(mfrow = c(1, 2), mar = c(4, 4, 2, 1), cex.axis = 0.8, cex.lab = 0.8, cex.main = 0.75)
metafor::regplot(fit3_metafor, mod = "allocrandom", main = "metafor: random dummy", ylim = c(-2, 0.5))
regplot(fit3_brma, mod = "alloc", main = "RoBMA: alloc factor", ylim = c(-2, 0.5))
```

`marginal_means()` computes estimated marginal means for each moderator term, averaging over the other moderators in the fit.
Continuous moderators (here `ablat`) are evaluated at $-1$, $0$, and $+1$ standard deviations of the predictor; categorical moderators (here `alloc`) at each level.

```{r marginal-means}
emm <- marginal_means(fit3_brma)
summary(emm)
```

The `plot()` function visualizes the posterior distribution of the estimated marginal means.

```{r marginal-means-plot, fig.width = 4.5, fig.height = 3.25}
par(mar = c(4, 4, 1, 1), cex.axis = 0.8, cex.lab = 0.8, cex.main = 0.75)
plot(emm, parameter = "alloc", xlim = c(-2, 0.5), lwd = 2)
```

## Model Diagnostics

The `RoBMA` R package implements a wide set of model fit diagnostics. Here, we highlight only a few.

`metafor::rstudent()` returns frequentist studentized residuals computed by deletion;
`RoBMA::rstudent()` returns LOO-PIT residuals, which propagate posterior uncertainty and leverage through leave-one-out prediction.
The two definitions are comparable but not identical; the `RoBMA` R package's LOO-PIT residuals first require LOO computation via `add_loo()`.

```{r add-loo, eval = FALSE}
fit3_brma <- add_loo(fit3_brma)
```
```{r residuals}
head(rstudent(fit3_metafor)$z)
head(rstudent(fit3_brma)$z)
```

`qqnorm()` checks the residuals against a standard normal reference.

```{r qqplot, fig.width = 7, fig.height = 3.2}
par(mfrow = c(1, 2), mar = c(4, 4, 2, 1), cex.axis = 0.8, cex.lab = 0.8, cex.main = 0.75)
qqnorm(fit3_metafor, main = "metafor: studentized", xlim = c(-1.5, 1.5), ylim = c(-3, 3))
qqnorm(fit3_brma, main = "RoBMA: LOO-PIT", xlim = c(-1.5, 1.5), ylim = c(-3, 3))
```

`influence()` collects DFFITS, Cook's distance, COVRATIO, leverage, and DFBETAS where the diagnostic is available.

```{r influence}
inf_brma <- influence(fit3_brma)
head(inf_brma$inf)
```

## References
