Resampling, Likelihood and Bayes
MCMC from scratch
Markov chain Monte Carlo explores a probability distribution by wandering through it — build the Metropolis algorithm in twenty lines and check it against an exact answer.
- 8 min read
- 3 reading levels
- Published
Read these first
On this page 5
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
MCMC draws samples from a complicated distribution by taking a long random walk that lingers where probability is high.
A street-food blogger explores a new city with a rule. Each evening she considers a random nearby stall. If it is busier than tonight's, she moves there tomorrow. If quieter, she sometimes moves anyway — small risks keep the tour honest.
Follow her for a year and count the nights: her diary lists every neighbourhood in proportion to how good its food is. Nobody gave her a map. The walk became the map.
Why it exists
The conjugate shortcut gives exact posteriors — for a lucky handful of models. Step outside that handful and the belief distribution you want has no formula anyone can integrate.
Here is the strange loophole. For most models you can cheaply score any single candidate: "how well does this parameter value explain the data?" You still cannot describe the whole landscape. MCMC turns that one weak ability into full exploration. It was invented for hydrogen-bomb physics in 1953; statisticians realised in 1990 that it unlocked almost every Bayesian model, and the field exploded.
The name, unpacked: Monte Carlo = answering by random sampling. Markov chain = a walk whose next step depends only on where you stand now.
How it works
stand at a parameter value
→ propose a small random step
→ score both spots: how well does each explain the data?
uphill (better score)? take the step
downhill (worse score)? take it sometimes —
the worse it is, the rarer
→ repeat 20,000 times
nights spent at each value ≈ your belief in that valueThe "sometimes accept worse" rule is the soul of the method. Always climbing would strand you at one peak. Occasional descents let the walk cross valleys and give every region its fair share of visits — exactly its share, in the long run.
The early steps still remember the arbitrary starting point, so everyone discards them. That throwaway phase is called burn-in.
A real example you have seen
Election-forecast models (like FiveThirtyEight's) run MCMC to combine hundreds of polls into seat probabilities. Hurricane-track cones, drug-dose safety curves, and the black-hole image reconstruction all ran on it. Under the hood, tools named Stan and PyMC carry this walk into thousands of dimensions.
Remember this
- MCMC needs only a scorer — no formula for the whole distribution.
- Uphill steps always; downhill steps sometimes — that balance makes visit-counts match probability.
- Discard the burn-in; the rest of the walk is your posterior.
What to learn next
- Kernel density estimation — turning your chain of samples into a smooth picture.
- Monte Carlo simulation — the counting principle the chain feeds.
- Priors, posteriors and conjugate updating — the exact answers worth checking any sampler against.
Developer — Code and libraries.
Setup
pip install numpy scipyOutputs verified with numpy 1.26 and scipy 1.14, CPU. The chain reproduces with these versions; other versions may shift the third decimal.
Metropolis in twenty lines, with an answer key
The problem: a feature succeeded 9 times in 12 demos; we want the posterior over its success rate (flat prior). We know the exact answer from conjugacy — Beta(10, 4) — which makes this the perfect test track: build the walker, then check it against truth.
import numpy as np
from scipy import stats
rng = np.random.default_rng(8)
def log_posterior(p):
# Score for candidate rate p: 9 successes, 3 failures, flat prior.
if not 0 < p < 1:
return -np.inf # outside (0,1): impossible
return 9 * np.log(p) + 3 * np.log(1 - p)
samples, p = [], 0.5 # start anywhere reasonable
for _ in range(20_000):
proposal = p + rng.normal(0, 0.1) # a small random step
if np.log(rng.uniform()) < log_posterior(proposal) - log_posterior(p):
p = proposal # accept: always if uphill,
samples.append(p) # sometimes if downhill
chain = np.array(samples[2000:]) # drop the burn-in
exact = stats.beta(10, 4)
print(f"MCMC posterior mean: {chain.mean():.3f}")
print(f"exact answer: {exact.mean():.3f}")
print(f"MCMC 95% interval: ({np.quantile(chain, .025):.3f}, {np.quantile(chain, .975):.3f})")
print(f"exact 95% interval: ({exact.ppf(.025):.3f}, {exact.ppf(.975):.3f})")MCMC posterior mean: 0.715 exact answer: 0.714 MCMC 95% interval: (0.465, 0.907) exact 95% interval: (0.462, 0.909)
A random walk, knowing nothing but a scoring function, recovered the true posterior to about two decimal places.
The walkthrough
The acceptance line is the whole algorithm. log(uniform) < score_new - score_old accepts with probability min(1, ratio_of_posteriors). Uphill: the ratio exceeds 1, always accepted. Downhill: accepted with probability equal to the ratio — the blogger's "sometimes".
Everything happens in logs. Posterior ratios become score differences, and tiny probabilities never underflow. Same discipline as maximum likelihood.
The scorer ignores constants. The true posterior includes a normalising constant we never computed — the acceptance ratio cancels it. That cancellation is why MCMC exists: the impossible integral drops out of the algebra.
Step size 0.1 is a tuned choice. Steps of 0.001 accept nearly always but crawl; steps of 2.0 propose absurdities and stall in place. Folklore target: accept roughly a quarter to half of proposals. Track it with a counter when you experiment.
samples.append(p) runs even on rejection — staying put is a legitimate visit and the counts are wrong without it. This is the most common from-scratch implementation bug.
Common mistakes
Keeping the burn-in. The first stretch documents your starting guess, not the posterior. Drop it; for real work, run several chains from scattered starts and confirm they agree (the R-hat diagnostic).
Treating the chain as independent draws. Consecutive samples are correlated — 18,000 steps may hold the information of a few hundred independent ones (the effective sample size). Uncertainty-of-the-estimate calculations must use ESS, not chain length.
Forgetting the bounds check. Without the -np.inf guard, a proposal of 1.02 feeds log(1 - 1.02) and crashes with RuntimeWarning: invalid value encountered in log. Guard the support explicitly.
Hand-rolling Metropolis for serious multi-dimensional work. This lesson's walker is for understanding. Real problems use gradient-guided samplers (Hamiltonian Monte Carlo / NUTS) via PyMC or Stan — same idea, far fewer steps wasted.
Try it yourself
Add an acceptance counter and rerun with step sizes 0.01, 0.1, and 1.0. Record acceptance rate and the estimated mean's error for each. You have reproduced the tuning trade-off every MCMC practitioner lives with.
What to learn next
- Kernel density estimation — turning your chain of samples into a smooth picture.
- Monte Carlo simulation — the counting principle the chain feeds.
- Priors, posteriors and conjugate updating — the exact answers worth checking any sampler against.
Researcher — Mathematics and papers.
The Metropolis–Hastings kernel
Target $\pi(\theta)$ known up to a constant. From state $\theta$, propose $\theta' \sim q(\theta' \mid \theta)$ and accept with probability
$$ \alpha(\theta, \theta') = \min!\left(1, \; \frac{\pi(\theta')\, q(\theta \mid \theta')}{\pi(\theta)\, q(\theta' \mid \theta)}\right) $$
Where:
- $q$ — the proposal density (symmetric $q$ cancels, giving the Metropolis form used above).
- On rejection the chain repeats $\theta$.
The resulting kernel satisfies detailed balance, $\pi(\theta) P(\theta \to \theta') = \pi(\theta') P(\theta' \to \theta)$, hence $\pi$ is stationary. With irreducibility and aperiodicity, the ergodic theorem gives, for integrable $f$:
$$ \frac{1}{N} \sum_{t=1}^{N} f(\theta_t) \;\xrightarrow{a.s.}\; \mathbb{E}_\pi[f] $$
A CLT holds under geometric ergodicity, with asymptotic variance $\sigma_f^2 = \operatorname{Var}_\pi(f) \cdot \tau_f$, where $\tau_f = 1 + 2\sum_{k\geq1} \rho_k$ is the integrated autocorrelation time; effective sample size is $N / \tau_f$.
Tuning theory
For random-walk Metropolis on product targets in high dimension, the optimal acceptance rate is $0.234$, with proposal scale $\propto 2.38\, \sigma / \sqrt{d}$ (Roberts, Gelman and Gilks, 1997) — mixing time grows as $O(d)$ in the step-count sense. Gradient-based samplers improve the scaling: MALA (Langevin proposals) targets $0.574$ acceptance and scales as $O(d^{1/3})$; Hamiltonian Monte Carlo simulates Hamiltonian dynamics with leapfrog integration, suppressing random-walk behaviour and scaling near $O(d^{1/4})$ (Neal, 2011, MCMC using Hamiltonian dynamics; Duane et al., 1987). NUTS (Hoffman and Gelman, 2014) removes HMC's trajectory-length tuning and powers Stan and PyMC defaults.
Other kernels
- Gibbs sampling (Geman and Geman, 1984): cycle through full conditionals $\theta_j \sim \pi(\theta_j \mid \theta_{-j})$ — acceptance-free; a special case of MH with $\alpha \equiv 1$.
- Data augmentation (Tanner and Wong, 1987): introduce latents to make conditionals tractable.
- Parallel tempering: run chains at flattened powers $\pi^{1/T}$ and swap — the standard remedy for well-separated modes, which plain Metropolis crosses exponentially rarely.
- Reversible-jump MCMC (Green, 1995) moves across models of different dimension.
Diagnostics
No finite diagnostic proves convergence; the standard battery: multiple over-dispersed chains with rank-normalised split-$\hat{R} < 1.01$ (Vehtari et al., 2021), ESS per parameter, trace plots, and divergence counts for HMC (which localise regions the sampler cannot enter — often signalling a pathological posterior geometry, cured by reparameterisation).
History and complexity
Metropolis, Rosenbluth, Rosenbluth, Teller and Teller (1953, Equation of state calculations by fast computing machines) — hard-sphere physics on the MANIAC. Hastings (1970) generalised to asymmetric proposals; Gelfand and Smith (1990, Sampling-based approaches to calculating marginal densities) detonated the Bayesian revolution. Cost per step is one posterior evaluation — for big data, subsampling variants (stochastic-gradient MCMC: Welling and Teh, 2011) trade exactness for scalability, and variational inference abandons sampling entirely for optimisation.
What to learn next
- Kernel density estimation — turning your chain of samples into a smooth picture.
- Monte Carlo simulation — the counting principle the chain feeds.
- Priors, posteriors and conjugate updating — the exact answers worth checking any sampler against.