Applied Bayesian Inference · Chapter 5

Chapter 5
Markov chain Monte Carlo

Lectures 9–10 · how the posterior is computed: Metropolis, Hamiltonian Monte Carlo and how to tell when a sampler can be trusted

Cartoon: explorers on a hillFour small explorers walk over a hilly landscape, leaving trails of footprints. One shouts: Is this the top? Another, holding a clipboard, replies: All four of us agree. R-hat is 1.00. Is this the top? All four of us agree.R-hat is 1.00.
a sampler is an explorer; diagnostics tell you whether it has seen the whole island

Every model so far has ended with the same line: brm(...), then a summary. Behind that line, software drew thousands of samples from a posterior distribution that no one could write down in closed form. For the first four chapters that was fine: the models were small, the samplers behaved, and the diagnostics were all 1.00 and thousands. But samplers do not always behave, and a researcher who cannot tell when they have failed will eventually publish a result the computer never actually computed.

This chapter opens the black box. You will write two samplers yourself, in about twenty lines of R each, and run them on a real posterior: the probability of re-arrest by age in the COMPAS data of Chapter 4. The first, the Metropolis algorithm, is the original Markov chain Monte Carlo method, simple enough to explain to a child and slow enough to fail in instructive ways. The second, Hamiltonian Monte Carlo, is the engine inside Stan and brms; it borrows the physics of a skateboard rolling in a bowl to move through high-dimensional posteriors efficiently. Then you will learn to read the diagnostics that tell you whether a sampler has done its job, and see a real dataset break a sampler in a way that is entirely the model's fault.

Think About It: Before You Start

A posterior distribution for 50 parameters is, in effect, a landscape in 50 dimensions. Suppose you could only take a few thousand steps through it, and at each step you could feel the height of the ground (the posterior density) and, if you wished, its slope. How would you decide where to step next, so that the places you visit end up in proportion to the posterior? Sketch a rule before reading Section 5.2.

Lecture 9 — How sampling works

5.1 Why not a grid?

In Section 3.2 we computed a posterior for two parameters by brute force: a grid of 150×150=22,500 points, the posterior evaluated at each. For ten parameters, the same resolution would need 15010≈6×1021 points, more than the number of grains of sand on Earth. Grids are hopeless beyond three or four parameters, and real models have dozens or thousands.

There is a deeper problem, and it explains why every successful algorithm works the way it does. In high dimensions, almost none of the posterior's probability is near its peak. Consider a posterior that is a standard normal distribution in d dimensions. Its density is highest at the centre, but there is very little volume near the centre, and a great deal of volume a little further out. Probability is density times volume, so the probability mass ends up concentrated in a thin shell at distance about d from the peak: the typical set (Betancourt, 2017). A sample from the posterior will almost never be near the mode.

Where the probability is: the typical set

3,000 draws from a standard normal distribution in d dimensions. The histogram shows each draw's distance from the peak (the mode: the red dot at distance 0). The dashed line marks d.

In one dimension, the draws cluster near the peak. In ten, not one draw in a thousand is within distance 1 of it, and in a hundred the middle 98% of draws lie between distances of about 8.4 and 11.7: a thin shell around 100=10. This has two consequences. First, finding the mode (as optimisation does) tells you little about where the probability is; that is one reason point estimates can mislead in complex models. Second, an algorithm that explores the posterior must spend its time in the typical set, a thin, curved region that is hard to stay in by taking random steps. The rest of this lecture is about two ways of doing it.

5.2 The Metropolis algorithm

Here is the idea, as a story. A politician must visit a chain of islands, spending time on each in proportion to its population, but she does not know the populations and has no map. She can only ask the population of the island she is on and of one neighbour. Her rule: each day, toss a coin to pick a neighbouring island. If it has more people than the current one, go there. If it has fewer, go there with probability equal to the ratio of the populations (say, a neighbour with 60% of the current population is visited 60% of the time); otherwise stay. Remarkably, in the long run the share of days she spends on each island is exactly proportional to its population. Replace islands with parameter values and population with posterior density, and you have the Metropolis algorithm.

Definition: The Metropolis Algorithm

To draw samples from a posterior p(θ∣y)∝p(y∣θ)p(θ):

  1. Start at some value θ(0).
  2. At step t, propose a new value θ*=θ(t−1)+ε, with ε drawn from a symmetric distribution such as Normal(0,s); s is the step size.
  3. Accept the proposal with probability min(1,p(y∣θ*)p(θ*)p(y∣θ(t−1))p(θ(t−1))): set θ(t)=θ*. Otherwise set θ(t)=θ(t−1).

The sequence θ(1),θ(2),… is a Markov chain (each value depends only on the previous one) whose long-run distribution is the posterior. Only ratios of posterior densities are needed, so the unknown normalising constant of Bayes' theorem cancels.

That last point is why Metropolis was revolutionary. Bayes' theorem divides by p(y), an integral over all parameter values that is usually impossible to compute. Metropolis never needs it: it only compares two points at a time.

History: Los Alamos, 1953

The algorithm was published in 1953 in a six-page paper in the Journal of Chemical Physics by Nicholas Metropolis, Arianna Rosenbluth, Marshall Rosenbluth, Augusta Teller and Edward Teller, who were simulating the behaviour of molecules in a liquid on MANIAC, one of the first electronic computers, at the Los Alamos laboratory. Historians now credit most of the work to the Rosenbluths: Marshall developed the method and Arianna wrote the code. The idea of using random numbers to solve hard problems, "Monte Carlo", had been named a few years earlier by Stanisław Ulam and Metropolis, after the casino where Ulam's uncle liked to gamble. Statisticians largely ignored the algorithm for nearly forty years, until Alan Gelfand and Adrian Smith showed in 1990 how such methods could compute Bayesian posteriors for realistic models. Within a decade, Bayesian statistics went from a philosophical position to a practical toolkit.

Now write it. Our target is the posterior for a two-parameter logistic regression from Chapter 4: the probability of re-arrest within two years as a function of age alone, for the 6,172 COMPAS defendants. Age is centred at 35 and measured in decades, so a is the log-odds of re-arrest at age 35 and b the change per decade.

R
library(tidyverse)
library(brms)
library(posterior)   # summaries and diagnostics of posterior draws
options(mc.cores = parallel::detectCores())

harbour <- c(ink = "#10191d", muted = "#4a5f68", grid = "#d5e3e6",
             sky = "#1d5187", amber = "#805907", rose = "#ad3b28",
             moss = "#1d6b54", lilac = "#5b3a94")
theme_harbour <- function(base_size = 11) {
  theme_minimal(base_size = base_size) +
    theme(text = element_text(colour = harbour[["ink"]]),
          axis.text = element_text(colour = harbour[["muted"]]),
          panel.grid.major = element_line(colour = harbour[["grid"]]),
          panel.grid.minor = element_blank(),
          strip.text = element_text(colour = harbour[["ink"]], hjust = 0),
          legend.position = "top")
}
theme_set(theme_harbour())
R
url <- "https://raw.githubusercontent.com/propublica/compas-analysis/master/compas-scores-two-years.csv"
compas <- read_csv(url, show_col_types = FALSE, name_repair = make.unique) |>
  filter(between(days_b_screening_arrest, -30, 30), is_recid != -1,
         c_charge_degree != "O", score_text != "N/A")
y   <- compas$two_year_recid             # 1 = re-arrested within two years
age <- compas$age
x_c <- (age - 35) / 10                   # age centred at 35, in decades

log_post <- function(theta, x, y) {      # log posterior, up to a constant
  eta <- theta[1] + theta[2] * x
  sum(y * eta - log1p(exp(eta))) +       # Bernoulli log-likelihood on the logit scale
    dnorm(theta[1], 0, 1.5, log = TRUE) + dnorm(theta[2], 0, 1, log = TRUE)   # priors
}
log_post(c(0, 0), x_c, y)
output
[1] -4280
R
metropolis <- function(n_iter, start, step, x, y) {
  draws <- matrix(NA, n_iter, 2, dimnames = list(NULL, c("a", "b")))
  current <- start; lp_current <- log_post(current, x, y); accepted <- 0
  for (t in 1:n_iter) {
    proposal <- current + rnorm(2, 0, step)                  # 1. propose a nearby point
    lp_proposal <- log_post(proposal, x, y)
    if (log(runif(1)) < lp_proposal - lp_current) {          # 2. accept with prob min(1, ratio)
      current <- proposal; lp_current <- lp_proposal; accepted <- accepted + 1
    }
    draws[t, ] <- current                                    # 3. record where we are
  }
  attr(draws, "accept") <- accepted / n_iter
  draws
}
set.seed(1)
chain_c <- metropolis(5000, start = c(1, 1), step = 0.03, x = x_c, y = y)
attr(chain_c, "accept")                                      # acceptance rate
kept <- chain_c[-(1:500), ]                                  # discard the first 500 (warmup)
rbind(mean = colMeans(kept), sd = apply(kept, 2, sd)) |> round(3)
output
[1] 0.475
          a      b
mean -0.207 -0.342
sd    0.027  0.023

The chain started at (1,1), far from the posterior, and walked downhill into it within a few hundred steps; we discard those first 500 draws as warmup. The remaining 4,500 draws give a posterior mean of about −0.21 for a (a 35-year-old's probability of re-arrest is logit−1(−0.21)≈45%) and about −0.34 for b: each decade of age lowers the log-odds by about a third. These agree with what brms gives for the same model (Section 5.5).

But 4,500 Metropolis draws are not worth 4,500 independent draws. Each draw is either a copy of the previous one (a rejection) or a small step away from it (an acceptance), so successive draws are strongly correlated. The effective sample size (ESS) measures how many independent draws the chain is worth for estimating a posterior mean. Play with the step size below and watch what it does to the chain.

Metropolis on the COMPAS posterior

The heat map is the exact posterior of (a,b) for re-arrest by age, computed from the 6,172 defendants. The chain starts in the upper corner. Press run to take 300 more steps; the right panel traces b. Choose the parameterisation: centred uses age in decades from 35; raw uses age in years, as in an uncentred model.

Three regimes appear. With a tiny step size, nearly every proposal is accepted, but the chain crawls: it takes hundreds of steps to cross the posterior, and the trace looks like a slow random walk. With a huge step size, nearly every proposal lands far out in the tails and is rejected, so the chain sits still for long stretches and the trace looks like a staircase. In between (for this posterior, around s=0.03, with about half of proposals accepted) the chain moves well. Theory for well-behaved posteriors says the most efficient random-walk Metropolis accepts about 23% of proposals in many dimensions (Roberts, Gelman and Gilks, 1997) and about 44% in one (Gelman, Roberts and Gilks, 1996).

5.3 How you write the model changes how hard it is to sample

Now switch the widget to the raw parameterisation, with age in years and not centred. The model is mathematically the same (the predictions for every defendant are identical), but the posterior looks completely different: a long, thin, tilted ridge. The intercept is now the log-odds of re-arrest at age zero, a meaningless extrapolation that depends sharply on the slope: make the slope a little steeper and the intercept must rise a lot to keep the line through the data. In this parameterisation the posterior correlation between a and b is −0.95, and the slope's posterior standard deviation (0.0023 per year) is 35 times smaller than the intercept's.

R
set.seed(2)
chain_r <- metropolis(5000, start = c(0.99, -0.034), step = 0.03, x = age, y = y)
c(accept_centred = attr(chain_c, "accept"), accept_raw = attr(chain_r, "accept"))
c(ess_centred = ess_bulk(chain_c[-(1:500), "b"]), ess_raw = ess_bulk(chain_r[-(1:500), "b"]))

tibble(iteration = rep(1:5000, 2),
       slope_per_decade = c(chain_c[, "b"], 10 * chain_r[, "b"]),
       model = rep(c("centred: age in decades from 35", "raw: age in years"), each = 5000)) |>
  filter(iteration > 500) |>
  ggplot(aes(iteration, slope_per_decade)) +
  geom_line(colour = harbour[["sky"]], linewidth = 0.3) +
  facet_wrap(~ model, ncol = 1) +
  labs(x = "iteration", y = "slope per decade")
output
accept_centred     accept_raw 
        0.4750         0.0328 
ess_centred     ess_raw 
    522.649       1.457 
Two trace plots of the slope against iterationTwo trace plots of the slope against iteration. The centred chain is a dense band of rapid fluctuation around minus 0.34. The raw chain moves in long slow excursions with flat stretches. raw: age in years centred: age in decades from 35 1000 2000 3000 4000 5000 -0.40 -0.35 -0.30 -0.40 -0.35 -0.30 iteration slope per decade
Trace plots of the slope (per decade of age) from 5,000 Metropolis steps with the same step size, under the centred and raw parameterisations. The centred chain mixes well; the raw chain is stuck for long stretches.

Even started at the right answer, the raw chain is nearly useless. With a step size that suits the intercept, nearly every proposal changes the slope by far too much and is rejected; with a step size that suits the slope, the intercept barely moves. It accepts only 3% of its proposals, and its effective sample size for the slope falls from about 520 to about 1. Nothing is wrong with the data or the model; the trouble is entirely in the coordinates. Centring and scaling predictors, which Section 3.3 recommended for interpretability, is also one of the most effective things you can do for computation. (brms centres predictors internally for exactly this reason, before translating the results back.)

Check Yourself

Why does centring age make the posterior easier to sample?

5.4 Hamiltonian Monte Carlo

Metropolis explores by guessing: it proposes a random nearby point and checks it. In two dimensions that is fine. In a hundred, a random direction is almost always a bad one (it points out of the thin typical set), so proposals must be tiny and the chain diffuses at a snail's pace. A sampler that knew which way the posterior curves could do much better. Hamiltonian Monte Carlo (HMC) uses the gradient of the log posterior, which tells it the slope of the landscape at every point, to propose distant points that are still likely to be accepted.

The idea comes from physics. Turn the posterior upside down, so that high probability becomes low ground: a bowl whose shape is −log⁡p(θ∣y). Put a frictionless skateboard at the current point and give it a random push (a random momentum). It rolls downhill, speeds up, climbs the far side, slows, turns and keeps rolling. After a while, stop it and record where it is. Because the board follows the shape of the bowl, it travels along the typical set rather than leaving it, and because its energy is conserved, it ends at a point of about the same total energy as it started, which means the proposal is almost always accepted.

A computer simulates the rolling in small discrete steps with the leapfrog integrator: half a step of momentum change using the gradient, a full step of position change using the momentum, and so on. With a small enough step size the simulated energy is almost exactly conserved; with too large a step the simulation becomes unstable and the energy explodes. The widget shows both.

One Hamiltonian trajectory

The same COMPAS posterior, in the raw parameterisation, shown in standardised units (each parameter divided by its posterior standard deviation), so the ridge is the tilted ellipse. Each press of new trajectory gives the current point a random momentum and simulates L leapfrog steps of size ε. The trajectory is accepted with probability min(1,e−ΔH), where ΔH is the change in total energy.

With a moderate step size, the trajectory swings along the ridge and lands far from where it started, and the energy error is tiny, so the move is accepted: one HMC step can cross the whole posterior. Increase ε toward 0.5 and the trajectory starts to zigzag across the narrow direction of the ridge, the energy error grows, and acceptance collapses. Push it further and the trajectory flies off entirely: a divergence, which Section 5.7 explains. Notice also that the trajectory eventually turns back on itself. The No-U-Turn Sampler (NUTS) of Hoffman and Gelman (2014), used by Stan, stops the simulation automatically when it starts to turn back, so the user never has to choose L, and it tunes ε during warmup.

Here is HMC in R. It needs the gradient of the log posterior, which for logistic regression is simple: for the intercept, ∑i(yi−pi) minus the prior's pull; for the slope, ∑i(yi−pi)xi minus the prior's pull.

R
grad_log_post <- function(theta, x, y) {
  p <- plogis(theta[1] + theta[2] * x)
  c(sum(y - p) - theta[1] / 1.5^2, sum((y - p) * x) - theta[2] / 1^2)
}
hmc <- function(n_iter, start, eps, L, x, y) {
  draws <- matrix(NA, n_iter, 2, dimnames = list(NULL, c("a", "b")))
  current <- start; accepted <- 0
  for (t in 1:n_iter) {
    q <- current; p <- rnorm(2)                          # a random push (momentum)
    H0 <- -log_post(q, x, y) + sum(p^2) / 2             # total energy at the start
    p <- p + eps / 2 * grad_log_post(q, x, y)            # leapfrog: half step of momentum,
    for (l in 1:L) {
      q <- q + eps * p                                   #   full step of position,
      if (l < L) p <- p + eps * grad_log_post(q, x, y)   #   full step of momentum,
    }
    p <- p + eps / 2 * grad_log_post(q, x, y)            #   final half step of momentum
    H1 <- -log_post(q, x, y) + sum(p^2) / 2             # total energy at the end
    if (log(runif(1)) < H0 - H1) { current <- q; accepted <- accepted + 1 }
    draws[t, ] <- current
  }
  attr(draws, "accept") <- accepted / n_iter
  draws
}
set.seed(3)
t_hmc  <- system.time(chain_h <- hmc(1000, start = c(0, 0), eps = 0.01, L = 20, x = x_c, y = y))
t_metr <- system.time(chain_m <- metropolis(5000, start = c(1, 1), step = 0.03, x = x_c, y = y))
tibble(sampler = c("Metropolis, 5,000 steps", "HMC, 1,000 steps"),
       accept  = c(attr(chain_m, "accept"), attr(chain_h, "accept")),
       ess_b   = c(ess_bulk(chain_m[-(1:500), "b"]), ess_bulk(chain_h[-(1:100), "b"])),
       seconds = c(t_metr[["elapsed"]], t_hmc[["elapsed"]])) |>
  mutate(ess_per_second = ess_b / seconds)
output
# A tibble: 2 x 5
  sampler                 accept ess_b seconds ess_per_second
  <chr>                    <dbl> <dbl>   <dbl>          <dbl>
1 Metropolis, 5,000 steps  0.486  467.   0.807           579.
2 HMC, 1,000 steps         0.989 1850.   3.93            471.

One thousand HMC iterations, each with 20 leapfrog steps, produce an effective sample size about twice the number of draws kept: successive HMC draws are essentially independent (an ESS larger than the number of draws means successive draws are slightly negatively correlated, which makes averages even more precise). One warning from experience: started at (1,1) instead of (0,0), this same sampler crawls for over 800 iterations before reaching the posterior, because its fixed step size and unit "mass" suit the posterior's centre, not the steep landscape far from it. Stan avoids this by adapting the step size and a mass matrix (the scale of each parameter) during warmup, which is one reason its warmup draws must be discarded. Metropolis needed five times as many iterations to produce about a quarter of the effective draws. In two dimensions, per second of computing, the two samplers come out roughly level (the timings vary from run to run and machine to machine), because each HMC iteration costs 21 gradient evaluations. In hundreds of dimensions the gap becomes enormous, because Metropolis's efficiency falls roughly in proportion to the number of parameters while HMC's falls much more slowly (Neal, 2011). That is why every modern general-purpose Bayesian engine uses gradients.

History: From Quarks to Stan

HMC was invented in 1987 by four physicists, Simon Duane, Anthony Kennedy, Brian Pendleton and Duncan Roweth, to simulate quantum chromodynamics on a lattice; they called it "hybrid Monte Carlo". The statistician Radford Neal brought it to statistics in the 1990s, using it to fit Bayesian neural networks (Chapter 14). It remained a specialist tool because it needs gradients and careful tuning. Two developments changed that around 2011: automatic differentiation, which computes exact gradients of any model written as a program, and the No-U-Turn Sampler, which tunes itself. A team led by Andrew Gelman, Bob Carpenter and Matt Hoffman combined them in Stan, named after Stanisław Ulam, released in 2012. brms writes Stan code for you.

Check Yourself

In the trajectory widget, a very large step size produces trajectories that are rarely accepted. Why?

Key Takeaways: Lecture 9

  • Grids fail in high dimensions, and posterior mass concentrates in a thin typical set far from the mode.
  • Metropolis proposes random steps and accepts them with probability equal to the posterior ratio; it needs no normalising constant but mixes slowly, and its step size must be tuned.
  • Successive draws are correlated; the effective sample size says how many independent draws a chain is worth.
  • Parameterisation matters: centring and scaling predictors turn thin ridges into round hills.
  • Hamiltonian Monte Carlo uses gradients to simulate a physical trajectory, making distant, nearly independent proposals. Stan's NUTS tunes it automatically.

Lecture 10 — Trusting the result

5.5 Many chains: R-hat and effective sample size

A single chain can look perfectly healthy while exploring only part of the posterior. The standard defence is to run several chains (Stan's default is four) from different random starting points and check that they agree. If the chains have all converged to the same distribution, the variation between chains should be no larger than the variation within each chain. The potential scale reduction factor, R^ ("R-hat"; Gelman and Rubin, 1992), compares the two.

Definition: R-hat, ESS and MCSE

For a parameter sampled by M chains, each split in half, R^ is roughly the square root of (between-chain variance plus within-chain variance) over (within-chain variance). It equals 1 when the chains agree and exceeds 1 when they do not. Modern software uses the rank-normalised version of Vehtari, Gelman, Simpson, Carpenter and Bürkner (2021), which also detects disagreement in the tails. Use the draws only if R^≤1.01.

The bulk effective sample size (Bulk-ESS) measures how precisely the chains estimate the centre of the posterior, and the tail ESS how precisely they estimate its 5% and 95% quantiles. Both should exceed about 100 per chain (400 in total). The Monte Carlo standard error (MCSE) of a posterior mean is about sd/ESS: the error due to using a finite number of draws, which should be small compared with the precision you report.

Four chains and R-hat

Four Metropolis chains on the COMPAS posterior, started from widely spread points, with a step size of your choice. The plot traces the slope (per decade) in each chain; the readout gives the split-R^ and the effective sample size, summed over chains, of the draws after discarding the first half of each chain as warmup.

With the centred parameterisation and a step size near 0.03, the four chains find each other within a few hundred steps and R^ drops below 1.01 within a few thousand. Now switch to raw. The chains creep along the ridge from their separate starting points and, whatever step size you choose, R^ stays far above 1: in Exercise 5, four raw chains still have R^≈1.7 after 20,000 steps each. Any one of those chains, examined alone, might have looked like a slowly drifting but plausible trace. Only the comparison between chains reveals that none of them has explored the posterior.

Now fit the centred model with brms and look at its diagnostics the way you should for every model you fit.

R
d_age <- tibble(recid = y, x_c = x_c)
m_age <- brm(recid ~ 1 + x_c, data = d_age, family = bernoulli(),
             prior = c(prior(normal(0, 1.5), class = Intercept), prior(normal(0, 1), class = b)),
             seed = 5, refresh = 0, file = "fits/ch5_age")
draws_age <- as_draws_array(m_age, variable = c("b_Intercept", "b_x_c"))
summarise_draws(draws_age, "mean", "sd", "rhat", "ess_bulk", "ess_tail", "mcse_mean")
nuts_params(m_age) |> filter(Parameter == "divergent__") |> summarise(divergences = sum(Value))
output
# A tibble: 2 x 7
  variable      mean     sd  rhat ess_bulk ess_tail mcse_mean
  <chr>        <dbl>  <dbl> <dbl>    <dbl>    <dbl>     <dbl>
1 b_Intercept -0.208 0.0263  1.00    3290.    2575.  0.000458
2 b_x_c       -0.343 0.0236  1.00    2770.    2441.  0.000450
  divergences
1           0

Read the line for the slope, b_x_c. The posterior mean is −0.34, matching our hand-written samplers. R^ is 1.00, the bulk and tail ESS are in the thousands, there are no divergences, and the Monte Carlo standard error of the mean (about 0.00045) is fifty times smaller than the posterior standard deviation. We can report the slope to two decimal places with complete confidence that the computation is not the limiting factor. Pictures help too: trace plots should look like overlapping "hairy caterpillars", and rank plots (histograms of the rank of each draw among all draws, separately by chain) should be flat for every chain.

R
dd <- as_draws_df(m_age) |> select(b_x_c, .chain, .iteration) |>
  mutate(chain = factor(.chain), rank = rank(b_x_c))
chain_cols <- c(harbour[["sky"]], harbour[["amber"]], harbour[["rose"]], harbour[["moss"]])
p1 <- ggplot(dd, aes(.iteration, b_x_c, colour = chain)) + geom_line(linewidth = 0.25, alpha = 0.8) +
  labs(x = "iteration (after warmup)", y = "slope b", colour = "chain")
p2 <- ggplot(dd, aes(rank, fill = chain)) + geom_histogram(bins = 20, colour = NA) +
  facet_wrap(~ chain, nrow = 2) + labs(x = "rank of draw among all chains", y = NULL) +
  theme(legend.position = "none", axis.text = element_blank())
patchwork::wrap_plots(p1, p2, widths = c(1.4, 1)) &
  scale_colour_manual(values = chain_cols) & scale_fill_manual(values = chain_cols)
Two panelsTwo panels. Left: four overlapping trace lines in different colours forming a uniform band. Right: four small histograms of ranks, all roughly flat. -0.40 -0.35 -0.30 0 250 500 750 1000 iteration (after warmup) slope b chain 1 2 3 4 3 4 1 2 rank of draw among all chains
Diagnostic plots for the slope of the centred model in brms. Left: trace plots of the four chains, which overlap completely. Right: rank histograms for each chain, which are all close to flat; a chain exploring a different region from the others would show a sloped or lumpy histogram.

Research Practice: Report the Computation

A methods section should let a reader reproduce and trust your computation. State the software and versions (R, Stan, brms), the number of chains, warmup and post-warmup iterations, and any non-default settings (such as adapt_delta). Report that all R^≤1.01, that bulk and tail ESS exceeded a threshold (say 400), and the number of divergent transitions (ideally zero; if not, say what you did). Put full diagnostic tables in the supplement. For key quantities, check that the Monte Carlo standard error is small relative to the number of digits you report: if the MCSE of a posterior mean is 0.01, do not report it to three decimal places. The Bayesian Analysis Reporting Guidelines (Kruschke, 2021) include a checklist.

Check Yourself

A parameter's posterior standard deviation is 0.024 and its bulk ESS is 1,600. Approximately what is the Monte Carlo standard error of its posterior mean?

A model's summary shows R^=1.08 for one parameter and 1.00 for the rest. What should you do?

5.6 When the sampler struggles, look at the model

The COMPAS file that ProPublica published contains several columns twice: the decile score appears as decile_score and again, identically, as decile_score.1 (the same is true of the count of prior arrests). This is common in real data assembled by joining tables. Suppose an analyst, not noticing, includes both versions in a model. To keep things fast, we aggregate the data by score, which gives a binomial model with ten rows (Exercise 3 of Chapter 4 showed that aggregation does not change the likelihood).

R
dup <- compas |>
  group_by(d1 = decile_score, d2 = decile_score.1) |>
  summarise(k = sum(two_year_recid), n = n(), .groups = "drop")
all(dup$d1 == dup$d2)                                       # the two columns are identical
m_dup_flat <- brm(k | trials(n) ~ 1 + d1 + d2, data = dup, family = binomial(),
                  seed = 5, refresh = 0, file = "fits/ch5_dup_flat")   # brms default: flat priors on b
summarise_draws(as_draws_array(m_dup_flat, variable = c("b_d1", "b_d2")),
                "mean", "sd", "rhat", "ess_bulk")
nuts_params(m_dup_flat) |> filter(Parameter == "treedepth__") |>
  summarise(iterations = n(), hit_max_treedepth = sum(Value >= 10))
output
[1] TRUE
# A tibble: 2 x 5
  variable  mean    sd  rhat ess_bulk
  <chr>    <dbl> <dbl> <dbl>    <dbl>
1 b_d1     -48.2  104.  2.37     4.96
2 b_d2      48.5  104.  2.37     4.96
  iterations hit_max_treedepth
1       4000              3233

The sampler complains loudly. R^ is far above 1, the effective sample sizes are tiny, and most iterations hit the maximum tree depth (Stan prints a warning saying so): its trajectories kept running without ever turning back. The coefficients' posterior standard deviations are absurd. Nothing is wrong with Stan. The model has a problem: with two identical columns, only the sum b1+b2 affects the predictions, so any pair with the same sum fits the data equally well. With brms's default flat priors on the coefficients, nothing pins down the individual values, and the posterior is an infinitely long, flat ridge. The chains wander along it without end.

R
m_dup_prior <- update(m_dup_flat, prior = prior(normal(0, 1), class = b),
                      seed = 5, refresh = 0, file = "fits/ch5_dup_prior")
summarise_draws(as_draws_array(m_dup_prior, variable = c("b_d1", "b_d2")), "mean", "sd", "rhat", "ess_bulk")
sums <- as_draws_df(m_dup_prior) |> mutate(total = b_d1 + b_d2)
c(mean_sum = mean(sums$total), sd_sum = sd(sums$total))

bind_rows(`flat priors` = as_draws_df(m_dup_flat), `Normal(0, 1) priors` = as_draws_df(m_dup_prior),
          .id = "model") |>
  slice_head(n = 250, by = c(model, .chain)) |>                   # plot 250 draws per chain
  mutate(model = factor(model, levels = c("flat priors", "Normal(0, 1) priors"))) |>
  ggplot(aes(b_d1, b_d2, colour = factor(.chain))) +
  geom_point(alpha = 0.3, size = 0.6) +
  facet_wrap(~ model, scales = "free") +
  scale_colour_manual(values = c(harbour[["sky"]], harbour[["amber"]], harbour[["rose"]], harbour[["moss"]])) +
  labs(x = expression(b[1]), y = expression(b[2]), colour = "chain") + guides(colour = guide_legend(override.aes = list(size = 2, alpha = 1)))
output
# A tibble: 2 x 5
  variable  mean    sd  rhat ess_bulk
  <chr>    <dbl> <dbl> <dbl>    <dbl>
1 b_d1     0.159 0.727  1.00     874.
2 b_d2     0.122 0.727  1.00     874.
mean_sum   sd_sum 
 0.28034  0.01027 
Two scatter plots of b1 against b2Two scatter plots of b1 against b2. Left: points from four chains spread in long diagonal streaks over a wide range. Right: points form a bounded diagonal segment from about (−2, 2) to (2, −2), centred near (0.15, 0.15). flat priors Normal(0, 1) priors -200 -100 0 100 -2 -1 0 1 2 -2 -1 0 1 2 -100 0 100 200 b 1 b 2 chain 1 2 3 4
Posterior draws of the two coefficients on the duplicated column. Left: with flat priors, the chains wander along an unbounded ridge (b₁ + b₂ is fixed; the difference is not). Right: with Normal(0, 1) priors the ridge is bounded and the sampler converges, but the individual coefficients remain meaningless; only their sum is identified.

With weakly informative Normal(0, 1) priors, the ridge becomes bounded: the prior gently prefers coefficients near zero, so the posterior is a bounded diagonal segment and the sampler converges. But look at what we learn: b1 and b2 each have huge uncertainty and are almost perfectly negatively correlated, while their sum is estimated precisely (it is the same as the coefficient on a single copy of the column). The right fix is not a prior; it is to remove the duplicate.

This example is extreme, but the pattern is general, and it has a name. Andrew Gelman's folk theorem of statistical computing says: when you have computational problems, often there's a problem with your model. Slow mixing, high R^, maximum tree depth and divergences are frequently symptoms of predictors that are nearly collinear, parameters that the data cannot separate, priors that are too vague for the data, or a likelihood that does not suit the outcome. Before reaching for more iterations, ask what the sampler is telling you about the model.

Research Practice: Treat Warnings as Findings

Never suppress or ignore sampler warnings to get a result. Each warning points somewhere:

  • High R^ or low ESS: look at trace and pairs plots for ridges (non-identified or collinear parameters) and multiple modes. Check for duplicated or derived variables, and for parameters with no data informing them.
  • Maximum tree depth: often a long ridge or very different parameter scales; rescale predictors, or rethink the parameterisation.
  • Divergences: a region of high curvature, typically in hierarchical models (Chapter 6); reparameterise rather than just raising adapt_delta.

Report what you found and changed. A sentence such as "An initial model including both versions of the decile score failed to converge; the duplicate was removed" shows a reviewer you understand your analysis.

5.7 Divergences

HMC has a built-in alarm that Metropolis lacks. As you saw in the trajectory widget, when the leapfrog step size is too large for the curvature of the posterior, the simulated energy explodes. Stan watches the energy along every trajectory (Stan Development Team, 2024), and when the error passes a threshold it stops the trajectory and records a divergent transition. Divergences matter because they reveal regions of the posterior that the sampler cannot enter: typically narrow, highly curved regions where the step size tuned for the rest of the posterior is far too large. The draws then systematically under-represent those regions, and the posterior summaries are biased, not just noisy.

A few divergences out of thousands of iterations, scattered randomly, can sometimes be removed by asking Stan for a smaller step size (control = list(adapt_delta = 0.95) or 0.99). Divergences that persist, or that cluster in one region of a pairs plot, need a better parameterisation. The classic case is the "funnel" in hierarchical models, where a group-level standard deviation near zero squeezes all the group effects into a narrow neck. Chapter 6 meets it on real data and shows the fix, the non-centred parameterisation, which is the same idea as centring a predictor here: change the coordinates so the posterior becomes a round hill.

Project Connection: Cancer Gene-Expression Subtypes

A Bayesian model of tumour subtypes from gene expression may have thousands of parameters: an effect for each gene in each subtype. That is exactly where Hamiltonian Monte Carlo earns its keep, and exactly where the diagnostics of this lecture become essential. Genes are highly correlated (co-expressed pathways), which creates ridges like the duplicated column; mixture models of subtypes can have several modes that chains settle into separately (Chapter 11); and sparsity priors (Chapter 10) produce funnel-like geometry. Plan to standardise expression measurements, start with a subset of genes, check R^, ESS and divergences at every stage, and use variational inference (Chapter 9) only when you have verified it against MCMC on a smaller problem.

5.8 Can the model recover the truth?

All the diagnostics so far check whether the sampler explored the posterior of the model you wrote. They cannot tell you whether that posterior would lead you to the right answer if the model were true. For that there is a simple, powerful habit: fake-data simulation. Choose parameter values, simulate a dataset from the model with those values (keeping the real predictors), fit the model, and check that the posterior covers the values you chose. Repeat a few times.

R
truth <- c(a = -0.2, b = -0.35)
recovery <- map_dfr(1:20, function(r) {
  set.seed(100 + r)
  fake <- tibble(x_c = x_c, recid = rbinom(length(x_c), 1, plogis(truth[["a"]] + truth[["b"]] * x_c)))
  fit <- update(m_age, newdata = fake, chains = 2, iter = 1000, seed = r, refresh = 0)
  as_tibble(fixef(fit), rownames = "parameter") |> mutate(rep = r)
}) |>
  mutate(parameter = recode(parameter, Intercept = "a (intercept)", x_c = "b (slope)"),
         true = if_else(parameter == "a (intercept)", truth[["a"]], truth[["b"]]),
         covered = Q2.5 <= true & true <= Q97.5)
recovery |> group_by(parameter) |> summarise(coverage = mean(covered), mean_error = mean(Estimate - true))
ggplot(recovery, aes(Estimate, rep)) +
  geom_vline(aes(xintercept = true), linetype = "dashed", colour = harbour[["muted"]]) +
  geom_errorbarh(aes(xmin = Q2.5, xmax = Q97.5, colour = covered), height = 0) +
  geom_point(aes(colour = covered), size = 1.2) +
  facet_wrap(~ parameter, scales = "free_x") +
  scale_x_continuous(n.breaks = 4) + theme(panel.spacing.x = unit(3, "lines")) +
  scale_colour_manual(values = c(`TRUE` = harbour[["sky"]], `FALSE` = harbour[["rose"]])) +
  labs(x = "posterior mean and 95% interval", y = "simulated dataset", colour = "covers truth")
output
# A tibble: 2 x 3
  parameter     coverage mean_error
  <chr>            <dbl>      <dbl>
1 a (intercept)     0.95    0.00321
2 b (slope)         0.9     0.00605
Two panels of 20 horizontal interval lines each, for the intercept and slope, scattered arTwo panels of 20 horizontal interval lines each, for the intercept and slope, scattered around dashed vertical lines at the true values; nearly all intervals cross the dashed line. a (intercept) b (slope) -0.3 -0.2 -0.1 -0.45 -0.40 -0.35 -0.30 -0.25 5 10 15 20 posterior mean and 95% interval simulated dataset covers truth FALSE TRUE
Parameter recovery: 20 datasets simulated from the age model with known parameters (dashed lines), each refitted with brms. Points are posterior means and lines are 95% credible intervals. If the computation and model are sound, about 95% of intervals should contain the true value.

The intervals scatter around the true values, and the share that contains them is close to 95%, as it should be. Twenty repetitions is a small check (with 20 intervals, anywhere from 17 to 20 hits is consistent with 95% coverage), but it is enough to catch the commonest errors: a coding mistake, a variable on the wrong scale, a prior that silently dominates. For more complex models, run it before touching the real data, and especially for any parameter whose estimate your argument depends on.

Research Practice: Simulate Before You Fit

Fake-data simulation is the single best habit for avoiding embarrassing errors in applied Bayesian work (Gelman et al., 2020, "Bayesian Workflow"). It checks that your code does what you think, that the data can identify the parameters you care about with the design you have, and that the priors are not doing more than you intend. Its formal version, simulation-based calibration (Talts et al., 2018; Modrák et al., 2025), repeats the exercise hundreds of times with parameters drawn from the prior and checks that the ranks of the true values among the posterior draws are uniform; packages such as SBC automate it. In a paper, a sentence like "In 20 simulated datasets, 95% intervals for the treatment effect covered the true value 19 times" is reassuring and cheap to produce. It also makes an excellent power analysis: simulate data with the effect size you expect and see how often the posterior would detect it.

Check Yourself

In fake-data simulation, the 95% intervals for a parameter cover the true value in only 9 of 20 simulated datasets, and the posterior means are all too high. What is the most likely explanation?

Key Takeaways: Lecture 10

  • Run several chains and require R^≤1.01, adequate bulk and tail ESS, small MCSE and no divergences before using the draws. Look at trace and rank plots.
  • The folk theorem: computational problems usually point to model problems, such as duplicated or collinear predictors, non-identified parameters or awkward geometry. Fix the model, not the sampler.
  • Divergences signal regions the sampler cannot explore; they bias results and call for reparameterisation.
  • Fake-data simulation checks that the model and code recover known parameters; do it before fitting real data.
  • Report computational details, diagnostics and checks in every paper.

5.9 Exercises

Each exercise has a hint and an answer button.

Exercises

  1. Tuning Metropolis. Run the metropolis() function on the centred posterior for 5,000 steps with step sizes 0.003, 0.03 and 0.3 (same start, same seed). For each, report the acceptance rate and the bulk ESS of b after discarding 500 warmup draws. Which works best?

    Loop over the step sizes with map() and use ess_bulk(chain[-(1:500), "b"]).

    With step 0.003 the chain accepts 84% of proposals but is still crawling toward the posterior after 5,000 steps: bulk ESS about 5, and its mean for b (−0.24) is not yet the posterior mean. With step 0.03, it accepts 48% and has ESS about 520. With step 0.3, it accepts only 2% and has ESS about 44. The middle value is best by an order of magnitude; both extremes waste almost all their iterations.

  2. Posterior correlation. Compute the posterior correlation between the intercept and the slope from the centred chain chain_c and from the raw chain chain_r (after warmup). Explain the difference.

    cor(chain[-(1:500), ]).

    The posterior correlation between a and b is 0.07 in the centred chain and −0.96 in the raw chain. Without centring, the intercept is the log-odds at age zero, which is determined almost entirely by the slope (a steeper decline with age implies a higher value at age zero), so the two parameters move in lockstep along a thin ridge. Centring at 35 puts the intercept in the middle of the data, where it is estimated almost independently of the slope.

  3. HMC step size. Run hmc() on the centred posterior for 500 iterations with L=20 and step sizes 0.005, 0.01, 0.03 and 0.05. Report the acceptance rate for each. What happens at the largest step size, and why?

    The posterior standard deviation of each parameter is about 0.025. How far does one leapfrog step move relative to that?

    Started at the posterior mean, with L=20, the acceptance rates are 1.00 (ε=0.005), 0.99 (0.01), 0.84 (0.03) and 0 (0.05). At 0.05 the leapfrog integrator is unstable: the posterior standard deviation of each parameter is about 0.025, and leapfrog steps larger than about twice the narrowest standard deviation (here about 0.046) make the simulated energy grow without bound. Every trajectory diverges and none is accepted. Smaller steps are more accurate but cover less distance per gradient evaluation, so the best step size is the largest one that is still comfortably stable; this is what Stan's warmup tunes.

  4. Thinning. Keep every tenth draw of the post-warmup centred Metropolis chain. Compare the bulk ESS of b and the ESS per kept draw with the unthinned chain. Does thinning increase the information in the chain?

    thinned <- kept[seq(1, nrow(kept), by = 10), ].

    The full post-warmup chain (4,500 draws) has bulk ESS about 520, or 0.12 per draw. Keeping every tenth draw leaves 450 draws with ESS about 340, or 0.76 per draw. Thinning makes the kept draws less correlated, but it throws information away: the total ESS falls from 520 to 340. Thin only to save memory or storage, never to improve estimates.

  5. R-hat by hand. Run four metropolis() chains on the raw posterior for 2,000 steps with step size 0.003, from starting points (0,0), (2,−0.06), (0.5,−0.01) and (1.5,−0.05). Compute R^ for b using rhat() on a matrix of the post-warmup draws (iterations × chains). Then repeat with 20,000 steps.

    rhat(sapply(chains, function(ch) ch[-(1:n_warm), "b"])).

    After 2,000 steps per chain, R^ for b is about 3.0 and the total bulk ESS about 5; after 20,000 steps, R^ is still about 1.7 and ESS about 6. On the raw ridge, isotropic Metropolis proposals small enough to be accepted in the narrow direction are far too small to travel along the long direction, so the chains never meet in any practical time. The fix is not more iterations but centring the predictor (or a sampler, like HMC with an adapted mass matrix, that copes with the scales).

  6. How many draws?. Using m_age, estimate the posterior mean probability of re-arrest for a 25-year-old, its posterior standard deviation, and its Monte Carlo standard error. How large would the effective sample size need to be for the MCSE to be below 0.0005?

    posterior_epred(m_age, newdata = tibble(x_c = -1)) gives a column of draws; use mcse_mean() and MCSE≈sd/ESS.

    The posterior mean probability of re-arrest at age 25 is 0.534, with posterior standard deviation 0.0083. Its bulk ESS is about 3,500, so the MCSE is about 0.00014: tiny compared with the posterior uncertainty. For an MCSE below 0.0005 you would need an ESS of only (0.0083/0.0005)2≈280. Four default chains are far more than enough; models with less information or worse mixing are where the MCSE becomes binding.

  7. The duplicated column, fixed. Fit the aggregated binomial model with a single copy of the decile score, k | trials(n) ~ 1 + d1, with a Normal(0, 1) prior. Compare the posterior of its coefficient with the posterior of b1+b2 from m_dup_prior.

    Use update(m_dup_prior, formula = k | trials(n) ~ 1 + d1, newdata = dup).

    With one copy of the score, the coefficient is 0.280 (95% interval 0.261 to 0.300). In m_dup_prior, b1+b2 has posterior mean 0.280 and standard deviation 0.010: the same. The data identify only the sum; the duplicated model spreads it arbitrarily between two coefficients, each with a standard deviation of 0.73 determined almost entirely by the prior. Removing the duplicate gives the same predictions, faster sampling and interpretable coefficients.

  8. A computation paragraph. Write the paragraph describing the computation for the model m_age, as it would appear in the methods section of a paper.

    Software and versions, chains and iterations, convergence criteria and results, divergences, and the fake-data check.

    A model answer: "Models were fitted in R 4.3.3 with brms 2.20.4 and rstan 2.32.5, using four chains of 2,000 iterations each, the first 1,000 of which were warmup, giving 4,000 posterior draws. All parameters had R^≤1.01 (rank-normalised; Vehtari et al., 2021), bulk effective sample sizes of at least 2,700 and tail effective sample sizes of at least 2,400, and there were no divergent transitions. Monte Carlo standard errors of all reported posterior means were below 0.0005. Before fitting the real data, we simulated 20 datasets from the model with known parameters and the observed ages; 95% credible intervals covered the true intercept in 19 and the true slope in 18 of them."

Further reading and references

Further Reading

  • McElreath, Statistical Rethinking (2nd ed., 2020), Chapter 9: King Markov, Metropolis, HMC and the folk theorem, told with characteristic clarity.
  • Betancourt, "A conceptual introduction to Hamiltonian Monte Carlo" (2017): the geometry of the typical set and why HMC works, almost without equations.
  • Vehtari, Gelman, Simpson, Carpenter and Bürkner, "Rank-normalization, folding, and localization: an improved R-hat" (2021): the modern diagnostics used by Stan and posterior.
  • Gelman et al., "Bayesian workflow" (2020): fake-data simulation, the folk theorem and the whole cycle of building and checking models.
  • Neal, "MCMC using Hamiltonian dynamics" (2011), in the Handbook of Markov Chain Monte Carlo: the definitive technical account of HMC.
  1. Betancourt, M. (2017). A conceptual introduction to Hamiltonian Monte Carlo. arXiv:1701.02434.
  2. Duane, S., Kennedy, A. D., Pendleton, B. J. and Roweth, D. (1987). Hybrid Monte Carlo. Physics Letters B, 195(2), 216–222.
  3. Gelfand, A. E. and Smith, A. F. M. (1990). Sampling-based approaches to calculating marginal densities. Journal of the American Statistical Association, 85(410), 398–409.
  4. Gelman, A., Roberts, G. O. and Gilks, W. R. (1996). Efficient Metropolis jumping rules. In J. M. Bernardo, J. O. Berger, A. P. Dawid and A. F. M. Smith (eds), Bayesian Statistics 5, 599–607. Oxford University Press.
  5. Gelman, A. and Rubin, D. B. (1992). Inference from iterative simulation using multiple sequences. Statistical Science, 7(4), 457–472.
  6. Gelman, A., Vehtari, A., Simpson, D., Margossian, C. C., Carpenter, B., Yao, Y., Kennedy, L., Gabry, J., Bürkner, P.-C. and Modrák, M. (2020). Bayesian workflow. arXiv:2011.01808.
  7. Hoffman, M. D. and Gelman, A. (2014). The No-U-Turn sampler: adaptively setting path lengths in Hamiltonian Monte Carlo. Journal of Machine Learning Research, 15, 1593–1623.
  8. Kruschke, J. K. (2021). Bayesian analysis reporting guidelines. Nature Human Behaviour, 5, 1282–1291.
  9. Metropolis, N., Rosenbluth, A. W., Rosenbluth, M. N., Teller, A. H. and Teller, E. (1953). Equation of state calculations by fast computing machines. Journal of Chemical Physics, 21(6), 1087–1092.
  10. Modrák, M., Moon, A. H., Kim, S., Bürkner, P., Huurre, N., Faltejsková, K., Gelman, A. and Vehtari, A. (2025). Simulation-based calibration checking for Bayesian computation: the choice of test quantities shapes sensitivity. Bayesian Analysis, 20(2), 461–488.
  11. Neal, R. M. (2011). MCMC using Hamiltonian dynamics. In S. Brooks et al. (eds), Handbook of Markov Chain Monte Carlo, 113–162. Chapman and Hall/CRC.
  12. Roberts, G. O., Gelman, A. and Gilks, W. R. (1997). Weak convergence and optimal scaling of random walk Metropolis algorithms. Annals of Applied Probability, 7(1), 110–120.
  13. Stan Development Team (2024). Stan Reference Manual. mc-stan.org.
  14. Talts, S., Betancourt, M., Simpson, D., Vehtari, A. and Gelman, A. (2018). Validating Bayesian inference algorithms with simulation-based calibration. arXiv:1804.06788.
  15. Vehtari, A., Gelman, A., Simpson, D., Carpenter, B. and Bürkner, P.-C. (2021). Rank-normalization, folding, and localization: an improved R-hat for assessing convergence of MCMC. Bayesian Analysis, 16(2), 667–718.