R code from this page is also in the companion script. Packages: emdbook, coda, cmdstanr.
This material follows Bolker, Ecological Models and Data in R, sections 7.2.3 through 7.3 (pp. 302-319).
Derivative-based optimizers such as Newton's method and BFGS use the slope of the negative log-likelihood (NLL) to find its minimum. This part covers two optimizers that need no derivatives (Nelder-Mead and simulated annealing), then shows how a small change to simulated annealing turns it from a way to find the best parameter values into a way to sample the whole posterior distribution: the Metropolis algorithm, the basis of Markov chain Monte Carlo (MCMC).

Everything on this page belongs to the FIT step: confronting a model with data. Fitting by maximum likelihood means finding the lowest point on the NLL surface. Methods that follow the slope are fast near the answer but need derivatives, and they can fail far from the answer or on a bumpy surface. Two questions:
The answer to the second question grows out of the answer to the first.
The running example is Bolker's myxomatosis data: virus titers in 27 rabbits (grade 1 virus), modeled with a Gamma distribution.
library(emdbook)
titer <- subset(MyxoTiter_sum, grade == 1)$titer # myxo titers, 27 rabbits
# gamma NLL; Inf outside the allowed range so optimizers can't use it
nll_gamma <- function(p) {
if (any(p <= 0)) return(Inf)
-sum(dgamma(titer, shape = p[1], scale = p[2], log = TRUE))
}
# NLL surface over a shape x scale grid, for the background contours
shape_vec <- seq(10, 90, length.out = 120)
scale_vec <- seq(0.03, 0.32, length.out = 120)
surf <- outer(shape_vec, scale_vec, Vectorize(function(a, s) nll_gamma(c(a, s))))
# contour plot of NLL minus its minimum
plot_surface <- function(...) {
contour(shape_vec, scale_vec, surf - min(surf),
levels = c(1, 2, 5, 10, 20, 50, 100, 200, 500, 1000),
xlab = "shape", ylab = "scale", col = "grey60", labcex = 0.6, ...)
}
Nelder-Mead only compares NLL values; it never computes a slope. With two parameters it moves a triangle (a "simplex"; \(k + 1\) points for \(k\) parameters) across the surface:

The triangle stretches out where the surface falls steadily, turns to follow a valley, and shrinks as it closes in on the minimum.
# hand-coded Nelder-Mead for two parameters; stores every simplex
nelder_mead <- function(f, start, step, n_iter = 80) {
simplex <- rbind(start, start + c(step[1], 0), start + c(0, step[2]))
vals <- apply(simplex, 1, f)
history <- vector("list", n_iter)
moves <- character(n_iter)
for (it in seq_len(n_iter)) {
o <- order(vals) # best first, worst last
simplex <- simplex[o, ]
vals <- vals[o]
history[[it]] <- simplex
centroid <- colMeans(simplex[1:2, ])
refl <- centroid + (centroid - simplex[3, ]) # reflect the worst point
f_r <- f(refl)
if (f_r < vals[1]) {
expd <- centroid + 2 * (centroid - simplex[3, ]) # best yet: try expanding
f_e <- f(expd)
if (f_e < f_r) {
simplex[3, ] <- expd; vals[3] <- f_e; moves[it] <- "expand"
} else {
simplex[3, ] <- refl; vals[3] <- f_r; moves[it] <- "reflect"
}
} else if (f_r < vals[2]) {
simplex[3, ] <- refl; vals[3] <- f_r; moves[it] <- "reflect"
} else {
contr <- centroid + 0.5 * (simplex[3, ] - centroid) # still bad: contract
f_c <- f(contr)
if (f_c < vals[3]) {
simplex[3, ] <- contr; vals[3] <- f_c; moves[it] <- "contract"
} else {
simplex[2:3, ] <- (simplex[2:3, ] + rbind(simplex[1, ], simplex[1, ])) / 2
vals[2:3] <- apply(simplex[2:3, ], 1, f)
moves[it] <- "shrink"
}
}
}
list(best = simplex[which.min(vals), ], value = min(vals),
history = history, moves = moves)
}
nm <- nelder_mead(nll_gamma, start = c(20, 0.05), step = c(5, 0.02))
round(c(shape = nm$best[1], scale = nm$best[2], nll = nm$value), 3)
## shape scale nll
## 49.611 0.140 37.667
table(nm$moves)
##
## contract expand reflect shrink
## 46 8 25 1
# first 30 triangles, colored by move
move_cols <- c(expand = "darkorange", reflect = "steelblue",
contract = "firebrick", shrink = "purple")
plot_surface()
for (i in 1:30) polygon(nm$history[[i]], border = move_cols[nm$moves[i]], lwd = 1.5)
legend("topright", names(move_cols), col = move_cols, lwd = 2, bty = "n")

The triangle expands across the contours, contracts as it turns into the curved valley, then crawls along the valley floor. In the widget below, step a triangle through a curved valley yourself.
Nelder-Mead is optim()'s default method:
fit_nm <- optim(c(20, 0.05), nll_gamma)
fit_nm[c("par", "value", "counts", "convergence")]
## $par
## [1] 49.602640 0.139584
##
## $value
## [1] 37.66714
##
## $counts
## function gradient
## 147 NA
##
## $convergence
## [1] 0
It needed 147 NLL evaluations, more than a derivative-based method would near the answer. In exchange, it can't be misled by a bad derivative, and bumps, kinks, and thresholds in the NLL don't stop it.
Question: Try the widget's "near the minimum" start. Why do the triangles keep shrinking, and how could you use their size to decide when to stop?
Any method that only goes downhill finds whichever valley it starts in. If the NLL has more than one valley, that may not be the deepest one. Simulated annealing (Bolker p. 306) sometimes accepts uphill moves so it can escape:
The name comes from metallurgy: a metal cooled slowly settles into a low-energy crystal structure. At a high temperature the search accepts most uphill moves and explores widely; as it cools it becomes a pure downhill search.
# acceptance probability exp(-delta / T) for a few temperatures and NLL increases
accept_tab <- outer(c(10, 1, 0.1), c(0.5, 2, 10),
function(temp, delta) exp(-delta / temp))
dimnames(accept_tab) <- list(paste("T =", c(10, 1, 0.1)),
paste("NLL worse by", c(0.5, 2, 10)))
signif(accept_tab, 2)
## NLL worse by 0.5 NLL worse by 2 NLL worse by 10
## T = 10 0.9500 8.2e-01 3.7e-01
## T = 1 0.6100 1.4e-01 4.5e-05
## T = 0.1 0.0067 2.1e-09 3.7e-44
In the widget, the search starts in the shallow valley on the right. Try a cold start, a hot start that cools quickly, and a hot start that never cools.
The same algorithm on the myxomatosis surface:
# simulated annealing: accept worse steps w.p. exp(-delta / temp); cool temp over time
anneal <- function(f, start, step_sd, n_steps = 3000, temp0 = 10,
cool = 0.9, every = 100) {
current <- start
f_current <- f(current)
best <- current
f_best <- f_current
temp <- temp0
track <- matrix(NA, n_steps, 4,
dimnames = list(NULL, c("shape", "scale", "nll", "temp")))
for (i in seq_len(n_steps)) {
proposal <- current + rnorm(length(current), 0, step_sd)
f_prop <- f(proposal)
if (is.finite(f_prop) && runif(1) < exp(-(f_prop - f_current) / temp)) {
current <- proposal
f_current <- f_prop
}
if (f_current < f_best) {
best <- current
f_best <- f_current
}
track[i, ] <- c(current, f_current, temp)
if (i %% every == 0) temp <- temp * cool
}
list(best = best, value = f_best, track = track)
}
set.seed(746)
sa <- anneal(nll_gamma, c(20, 0.05), step_sd = c(2, 0.005))
round(c(shape = sa$best[1], scale = sa$best[2], nll = sa$value), 3)
## shape scale nll
## 50.182 0.138 37.668
# left: path on the surface; right: NLL of the current point at each step (log axis)
par(mfrow = c(1, 2))
plot_surface()
lines(sa$track[, "shape"], sa$track[, "scale"], col = adjustcolor("darkorange", 0.5))
plot(sa$track[, "nll"], type = "l", log = "y", xlab = "Step", ylab = "Current NLL")

par(mfrow = c(1, 1))
The NLL drops from about 1,800 to under 100 within the first 50 steps, then bounces above the minimum (37.67) because worse moves are still accepted. As \(T\) falls from 10 to about 0.5 the bounces shrink and the search settles. That took 3,000 NLL evaluations for what Nelder-Mead did in 147. optim() has a built-in annealer:
set.seed(746)
fit_sann <- optim(c(20, 0.05), nll_gamma, method = "SANN")
fit_sann[c("par", "value", "counts", "convergence")]
## $par
## [1] 23.1909794 0.2977502
##
## $value
## [1] 40.7738
##
## $counts
## function gradient
## 10000 NA
##
## $convergence
## [1] 0
Question: With default settings, SANN reports an NLL of 40.77 after 10,000 evaluations, with
convergence = 0; Nelder-Mead reached 37.67. Is SANN broken? Should you trustconvergence = 0?
Keep the annealing rule but change three things, and the chain of visited points becomes a sample from the posterior distribution:
Writing Bayes' rule into the ratio shows why this is practical:
\[\frac{P(\theta_{new} \mid y)}{P(\theta_{old} \mid y)} = \frac{\mathcal{L}(\theta_{new})\, P(\theta_{new}) / P(y)} {\mathcal{L}(\theta_{old})\, P(\theta_{old}) / P(y)}\]
The denominator \(P(y)\) cancels. Computing \(P(y)\) requires integrating the likelihood times the prior over every possible parameter value, which is why a Bayesian fit otherwise needs a grid (feasible for one or two parameters) or a conjugate prior (available only for some models). Metropolis needs only likelihood times prior, at two points at a time.
Why the chain lands on the posterior. Take two parameter values, \(A\) and \(B\), with \(P(A \mid y) = 2\,P(B \mid y)\). From \(B\), a proposal to move to \(A\) is always accepted; from \(A\), a proposal to move to \(B\) is accepted half the time. In the long run the chain sits at \(A\) twice as often as at \(B\). The same holds for every pair of values, so the time the chain spends near any \(\theta\) is proportional to its posterior probability.
Two consequences:
The proposal here is symmetric (a normal jump centered on the current value), so a jump from \(A\) to \(B\) is as likely to be proposed as a jump from \(B\) to \(A\). With an asymmetric proposal, the Metropolis-Hastings rule multiplies the ratio by \(q(\theta_{old} \mid \theta_{new}) / q(\theta_{new} \mid \theta_{old})\), where \(q\) is the proposal density, to correct for the imbalance.
Example: germination. 30 of 40 seeds germinated. With a flat (Beta(1, 1)) prior, the exact posterior is Beta(31, 11), so we can check the sampler against it.
# germination trial: 30 of 40 seeds; flat prior, so log posterior = log likelihood + const
k <- 30
n <- 40
log_post_germ <- function(p) {
if (p <= 0 || p >= 1) return(-Inf)
dbinom(k, n, p, log = TRUE) + dbeta(p, 1, 1, log = TRUE)
}
# Metropolis sampler: accept w.p. min(1, Post(new) / Post(old)); returns the whole chain
metropolis <- function(log_post, start, step_sd, n_steps) {
chain <- matrix(NA, n_steps, length(start), dimnames = list(NULL, names(start)))
current <- start
lp_current <- log_post(current)
n_accept <- 0
for (i in seq_len(n_steps)) {
proposal <- current + rnorm(length(start), 0, step_sd)
lp_prop <- log_post(proposal)
if (log(runif(1)) < lp_prop - lp_current) { # log of the posterior ratio
current <- proposal
lp_current <- lp_prop
n_accept <- n_accept + 1
}
chain[i, ] <- current # a rejected step records the current value again
}
attr(chain, "accept_rate") <- n_accept / n_steps
chain
}
set.seed(746)
chain_germ <- metropolis(log_post_germ, start = 0.5, step_sd = 0.1, n_steps = 5000)
attr(chain_germ, "accept_rate")
## [1] 0.5858
Question: The annealer accepted with
runif(1) < exp(-delta / temp);metropolis()useslog(runif(1)) < lp_prop - lp_current. Are these different rules?
# trace plot, then histogram of the chain vs. the exact posterior, Beta(31, 11)
par(mfrow = c(1, 2))
plot(chain_germ, type = "l", xlab = "Step", ylab = "p")
hist(chain_germ, breaks = 40, freq = FALSE, main = "", xlab = "Germination probability, p")
curve(dbeta(x, 31, 11), add = TRUE, lwd = 2, col = "darkorange")

par(mfrow = c(1, 1))
# posterior summaries from the chain vs. exact
rbind(chain = c(mean = mean(chain_germ), quantile(chain_germ, c(0.025, 0.975))),
exact = c(31 / 42, qbeta(c(0.025, 0.975), 31, 11)))
## mean 2.5% 97.5%
## chain 0.7341027 0.5937651 0.8499169
## exact 0.7380952 0.5969538 0.8577870
The chain accepted 59% of its proposals, and its histogram matches the exact posterior.
Two parameters: myxomatosis. The same function samples the Gamma model's shape and scale, with flat priors on the positive values (so the log posterior is the negative NLL plus a constant):
set.seed(746)
chain_myxo <- metropolis(function(p) -nll_gamma(p),
start = c(shape = 20, scale = 0.05),
step_sd = c(4, 0.01), n_steps = 20000)
attr(chain_myxo, "accept_rate")
## [1] 0.27815
plot_surface()
points(chain_myxo[seq(1, 20000, by = 10), ], pch = 16, cex = 0.3,
col = adjustcolor("steelblue", 0.4))

# 95% credible intervals, dropping the first 2000 steps
apply(chain_myxo[-(1:2000), ], 2, quantile, c(0.025, 0.5, 0.975))
## shape scale
## 2.5% 28.97286 0.08511468
## 50% 49.20264 0.14048690
## 97.5% 81.46669 0.24469750
The draws fill the banana-shaped valley. The first 2,000 steps are dropped because the chain started far from the posterior (see which chains can you trust?). This chain mixes slowly: shape and scale trade off along a narrow, curved ridge, so most independent jumps in the two parameters leave it. Why random walks struggle takes this up.
Samplers compute the log posterior, never the posterior itself. This short section shows why, and what the acceptance rule looks like on the log scale.
A likelihood is a product of one density per observation, and products of many small numbers underflow: they become smaller than the smallest number the computer can store, and R returns 0.
set.seed(1)
y_sim <- rnorm(1000, 10, 2)
prod(dnorm(y_sim, 10, 2)) # the likelihood itself: underflows to 0
## [1] 0
sum(dnorm(y_sim, 10, 2, log = TRUE)) # the log likelihood is fine
## [1] -2147.143
A ratio of two zeros is undefined, so a sampler built on the raw posterior fails. On the log scale:
Only the difference is ever exponentiated (if at all), and a difference of log posteriors is a modest number even when each posterior is astronomically small.
A Metropolis chain gives samples from the posterior eventually, but how do we know it has? This part covers checking chains (burn-in, mixing, R-hat, effective sample size), why random-walk samplers struggle when parameters are correlated, and Hamiltonian Monte Carlo (HMC), the sampler behind the Stan software.
Three chains on the germination posterior (exact mean 0.738), each starting at \(p = 0.05\) and running 1,000 steps, with different proposal SDs:
step_sds <- c(0.005, 0.1, 3)
chains <- lapply(step_sds, function(s) {
set.seed(1)
metropolis(log_post_germ, start = 0.05, step_sd = s, n_steps = 1000)
})
round(rbind(step_sd = step_sds,
accept_rate = sapply(chains, attr, "accept_rate"),
post_mean = sapply(chains, mean)), 3)
## [,1] [,2] [,3]
## step_sd 0.005 0.100 3.000
## accept_rate 0.871 0.591 0.042
## post_mean 0.383 0.733 0.697
Question: The chain with the smallest jumps accepted 87% of its proposals; the one with the largest accepted 4%. A labmate says the first chain is the best, since nearly every proposal was accepted. Which chain would you trust, and what would you check?
par(mfrow = c(3, 1), mar = c(4, 4, 1.5, 0.5))
for (i in seq_along(chains)) {
plot(chains[[i]], type = "l", ylim = c(0, 1), xlab = "Step", ylab = "p",
main = paste("step sd =", step_sds[i]))
abline(h = 31 / 42, lty = 2, col = "darkorange")
}

par(mfrow = c(1, 1), mar = c(5, 4, 4, 2) + 0.1)
Things to check in any chain:
Try these in the widget: change the jump size, drop a burn-in, and turn on three chains to see R-hat.
The autocorrelation function shows how quickly a chain forgets where it was. Compare the well-tuned germination chain with the myxomatosis chain:
par(mfrow = c(1, 2))
acf(chain_germ[-(1:500)], lag.max = 50, main = "Germination, step sd 0.1")
acf(chain_myxo[-(1:2000), "shape"], lag.max = 500, main = "Myxomatosis shape")

par(mfrow = c(1, 1))
library(coda)
c(germination = effectiveSize(chain_germ[-(1:500)]),
myxo_shape = effectiveSize(chain_myxo[-(1:2000), "shape"]))
## germination.var1 myxo_shape.var1
## 913.59262 42.75732
Thinning (keeping every 5th or 10th draw) makes the kept draws less correlated, but it throws information away: the thinned chain has fewer effective draws, not more. Thin to save memory or disk space, not to improve estimates.
The myxomatosis chain kept 18,000 steps but is worth only about 43 independent draws of shape. The problem is the shape of the posterior: a long, narrow, tilted ridge.
A simple test case: two parameters, each Normal(0, 1), with correlation \(\rho = 0.95\).
rho <- 0.95
sigma_inv <- solve(matrix(c(1, rho, rho, 1), 2))
log_post_bvn <- function(p) -0.5 * drop(t(p) %*% sigma_inv %*% p)
grad_bvn <- function(p) -drop(sigma_inv %*% p) # gradient of the log posterior
# contours of the target, for the background of the plots
plot_bvn <- function(main) {
g <- seq(-3, 3, length.out = 100)
z <- outer(g, g, Vectorize(function(a, b) log_post_bvn(c(a, b))))
contour(g, g, z, levels = c(-0.5, -2, -4.5), drawlabels = FALSE, col = "grey60",
xlab = expression(theta[1]), ylab = expression(theta[2]), main = main)
}
set.seed(3)
rw <- metropolis(log_post_bvn, start = c(-2, -2), step_sd = 0.25, n_steps = 200)
effectiveSize(rw[, 1])
## var1
## 6.022575
(drop() turns the 1 \(\times\) 1 and 2 \(\times\) 1 matrices that %*% returns into a plain number and a plain vector.)
Gibbs sampling. The first widely used Bayesian software, BUGS (Bayesian inference Using Gibbs Sampling) and later JAGS, updates one parameter at a time, drawing it from its full conditional distribution: its posterior given the current values of all the other parameters. With conjugate priors those conditionals have known forms, so each draw is exact and nothing is rejected. (When they don't, the software substitutes a Metropolis-type step.) For our target, each conditional is normal: \(\theta_1 \mid \theta_2 \sim \text{Normal}(\rho\,\theta_2, \sqrt{1 - \rho^2})\).
# Gibbs sampler: draw each parameter in turn from its conditional distribution
gibbs <- function(start, rho, n_steps) {
chain <- matrix(NA, n_steps, 2)
x <- start
for (i in seq_len(n_steps)) {
x[1] <- rnorm(1, rho * x[2], sqrt(1 - rho^2))
x[2] <- rnorm(1, rho * x[1], sqrt(1 - rho^2))
chain[i, ] <- x
}
chain
}
set.seed(3)
gb <- gibbs(c(-2, -2), rho, 200)
effectiveSize(gb[, 1])
## var1
## 14.10493
par(mfrow = c(1, 2))
plot_bvn("Random-walk Metropolis, 200 steps")
lines(rw, col = "steelblue")
plot_bvn("Gibbs, 200 sweeps")
# each sweep moves horizontally (theta1 update), then vertically (theta2 update)
gb_path <- cbind(rep(gb[, 1], each = 2), as.vector(rbind(c(-2, gb[-200, 2]), gb[, 2])))
lines(rbind(c(-2, -2), gb_path), col = "steelblue")

par(mfrow = c(1, 1))
Gibbs never rejects a draw, but each move goes parallel to one axis, and only as far as the conditional distribution allows. With correlated parameters the conditionals are narrow (SD \(\sqrt{1 - \rho^2}\) as a share of the marginal SD), so the chain zigzags up the ridge in short steps:
rhos <- c(0.5, 0.95, 0.99)
round(data.frame(rho = rhos, conditional_sd = sqrt(1 - rhos^2),
lag1_autocorrelation = rhos^2,
ess_per_sweep = (1 - rhos^2) / (1 + rhos^2)), 3)
## rho conditional_sd lag1_autocorrelation ess_per_sweep
## 1 0.50 0.866 0.250 0.600
## 2 0.95 0.312 0.902 0.051
## 3 0.99 0.141 0.980 0.010
(For this target each Gibbs sweep's \(\theta_1\) chain is autocorrelated with coefficient \(\rho^2\), which gives the last two columns.) Gibbs works well when parameters are close to independent, however many there are, and poorly when they are strongly correlated. The correlation, not the number of parameters, is what matters.
HMC uses the gradient (slope) of the log posterior to make long, well-aimed proposals. Picture the negative log posterior as a bowl, so "downhill" means toward more plausible parameter values, and slide a frictionless puck across it:

Exact physics would conserve \(H\), so every proposal would be accepted. The leapfrog steps are approximate, and the accept/reject step corrects for their error; with a small \(\epsilon\) nearly everything is accepted. The momentum is drawn fresh each iteration and then discarded, so the recorded draws come from the posterior alone. HMC is still a Metropolis algorithm. What's new is a proposal that travels far and is still likely to be accepted.
# one HMC step: random momentum kick, L leapfrog steps along the gradient, then accept/reject
hmc_step <- function(p, log_post, grad, eps, L) {
m <- rnorm(length(p))
p_new <- p
m_new <- m
path <- matrix(p, nrow = 1)
g <- grad(p_new)
for (i in seq_len(L)) {
m_new <- m_new + 0.5 * eps * g # half step for momentum
p_new <- p_new + eps * m_new # full step for position
g <- grad(p_new) # one new gradient per step; reused by the next half step
m_new <- m_new + 0.5 * eps * g # half step for momentum
path <- rbind(path, p_new)
}
# total energy = -log posterior + kinetic energy m^2 / 2; accept w.p. exp(-change)
log_accept <- log_post(p_new) - 0.5 * sum(m_new^2) - log_post(p) + 0.5 * sum(m^2)
list(p = if (log(runif(1)) < log_accept) p_new else p, path = path)
}
# run n HMC steps; keep the first few trajectories for plotting
hmc <- function(start, log_post, grad, eps, L, n_steps, n_paths = 5) {
chain <- matrix(NA, n_steps, length(start))
paths <- list()
p <- start
for (i in seq_len(n_steps)) {
s <- hmc_step(p, log_post, grad, eps, L)
p <- s$p
chain[i, ] <- p
if (i <= n_paths) paths[[i]] <- s$path
}
list(chain = chain, paths = paths)
}
set.seed(3)
hm <- hmc(c(-2, -2), log_post_bvn, grad_bvn, eps = 0.1, L = 10, n_steps = 200)
c(random_walk = effectiveSize(rw[, 1]), gibbs = effectiveSize(gb[, 1]),
hmc = effectiveSize(hm$chain[, 1]))
## random_walk.var1 gibbs.var1 hmc.var1
## 6.022575 14.104926 182.611011
# side by side: random walk vs. HMC, same number of iterations
par(mfrow = c(1, 2))
plot_bvn("Random walk")
lines(rw, col = "steelblue")
plot_bvn("HMC (orange: first five trajectories)")
points(hm$chain, pch = 16, cex = 0.5)
for (pa in hm$paths) lines(pa, col = "darkorange", lwd = 2)

par(mfrow = c(1, 1))
Question: Each HMC iteration here costs 11 gradient evaluations (one per leapfrog step, plus one at the start), against one density evaluation per random-walk step. Is it still worth it?
Compare all three samplers, and change the correlation, in the widget:
A few details matter in practice:
Stan is a language for writing down a model; it compiles the model to C++ and samples it with NUTS. We run it from R with the cmdstanr package (setup instructions: cmdstanr quick start). Here is the Gamma model for the myxomatosis titers, with flat priors on the positive values as before:
library(cmdstanr)
myxo_code <- "
data {
int<lower=0> N;
vector<lower=0>[N] titer;
}
parameters {
real<lower=0> shape;
real<lower=0> scale;
}
model {
titer ~ gamma(shape, 1 / scale); // Stan's gamma uses shape and rate
}
"
myxo_model <- cmdstan_model(write_stan_file(myxo_code)) # compiles to C++ (slow, once)
fit_stan <- myxo_model$sample(data = list(N = length(titer), titer = titer),
chains = 4, parallel_chains = 4, seed = 746)
A Stan program is made of blocks, always in this order. This model uses three; all are optional in principle.
| Block | What goes there |
|---|---|
functions |
your own functions |
data |
declarations of the inputs passed from R |
transformed data |
quantities computed from the data only |
parameters |
what the sampler explores, with any bounds (<lower=0>) |
transformed parameters |
quantities computed from parameters, saved with each draw |
model |
priors and likelihood (the log posterior) |
generated quantities |
anything computed once per saved draw (derived quantities, simulated data) |
Every variable is declared with a type, lines end in ;, and comments start with //. Each ~ statement adds a log density to the log posterior (Stan calls the running total target), so a model block is a log posterior written out term by term. With no prior statement, a parameter declared <lower=0> gets a flat prior on the positive values.
fit_stan$summary()
## # A tibble: 3 × 10
## variable mean median sd mad q5 q95 rhat ess_bulk
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 lp__ -36.8 -36.4 1.04 0.716 -38.9 -35.8 1.00 866.
## 2 shape 48.1 47.4 13.2 13.2 27.9 71.0 1.00 452.
## 3 scale 0.156 0.146 0.0489 0.0405 0.0978 0.249 1.00 449.
## # ℹ 1 more variable: ess_tail <dbl>
fit_stan$diagnostic_summary()
## $num_divergent
## [1] 0 0 0 0
##
## $num_max_treedepth
## [1] 0 0 0 0
##
## $ebfmi
## [1] 0.8580162 1.1393600 0.9712159 1.1187068
# Stan's 4,000 draws on the NLL surface
draws <- fit_stan$draws(c("shape", "scale"), format = "draws_matrix")
plot_surface()
points(draws[, "shape"], draws[, "scale"], pch = 16, cex = 0.3,
col = adjustcolor("darkorange", 0.4))

# effective draws: 18,000 kept Metropolis steps vs. Stan's 4,000 draws
keep <- chain_myxo[-(1:2000), ]
rbind(metropolis = effectiveSize(keep),
stan = fit_stan$summary(c("shape", "scale"))$ess_bulk)
## shape scale
## metropolis 42.75732 43.43315
## stan 451.90083 448.69306
# 95% credible intervals for shape
rbind(metropolis = quantile(keep[, 1], c(0.025, 0.975)),
stan = quantile(draws[, "shape"], c(0.025, 0.975)))
## 2.5% 97.5%
## metropolis 28.97286 81.46669
## stan 24.96977 75.96623
Four chains of 1,000 draws each (after 1,000 warmup draws per chain) give \(\hat{R}\) = 1.00 and an effective sample size of about 452 for shape, compared with 43 from 18,000 Metropolis steps. The draws fill the same banana.
adapt_delta (for example to 0.95) forces a smaller \(\epsilon\); if that doesn't help, change the model's parameterization.The classic source of divergences is the "funnel" in hierarchical models, where a group-level standard deviation \(\sigma\) controls how far each group effect \(\tau\) can wander from 0. When \(\sigma\) is small, every \(\tau\) is squeezed near 0, so the posterior is wide at the top and very narrow at the neck. A step size that suits the top overshoots in the neck. The usual fix is the non-centered parameterization: sample \(Z \sim \text{Normal}(0, 1)\) and compute \(\tau = \sigma Z\). It describes the same model, but the sampler explores a shape with no neck:

Stan or JAGS? Monnahan, Thorson & Branch (2017, Methods in Ecology and Evolution 8:339-348) compared them on population models from 5 to about 1,100 parameters. For small, simple models the choice made little difference. Stan's advantage grew with the number of parameters and how correlated they were, up to orders of magnitude, and it depended on using the non-centered form for random effects. JAGS remains handy for quick models and for models with discrete latent states.
More to explore: an interactive gallery of MCMC samplers (Chi Feng), the Stan documentation, and Betancourt's conceptual introduction to HMC.