learn-bayesian-with-phoebe / Practitioner session 4 of 10
Learn Bayesian with Phoebe · Practitioner session 4 of 10

MCMC and its diagnostics

Session 3 drew 20,000 samples from a posterior you could have written down. Tonight is about the posteriors you cannot. Conjugacy runs out the moment a model has two parameters that interact, and the hierarchical model in session 6 is the first one you will meet. Markov chain Monte Carlo produces draws from such a posterior anyway, by walking through it, and the draws are read exactly as session 3 read them. The price is that a walk can go wrong, silently, and produce a pile of numbers that looks like a posterior and is not. So tonight teaches the sampler and, in the same breath, the three diagnostics that catch it lying: R-hat, effective sample size, divergences. The tools are PyMC 6.3 and ArviZ 1 as they are this September, and the names have changed enough that most of what you will find online is wrong.

🔴 Hardest Data scientists and analysts Anyone who will read a sampler's output and sign it 45 min
0-3 · Welcome 3-24 · Chains, and how they lie 24-40 · Build-along: the Westfield fit 40-45 · Q&A
Part 0

Why sample when the Beta is exact

Because the Beta stops being available. A Beta prior and a binomial likelihood give a Beta posterior, and that is the only reason sessions 1 to 3 could write the answer down. Put a second parameter in the model that interacts with the first, and the product of prior and likelihood is no longer a distribution with a name. It is still a perfectly good posterior; you simply cannot read its mean off a formula. Statistical Rethinking's eighth lecture, "MCMC", is the source for what follows: a sampler is a way to get draws from that nameless posterior, and once you have draws, session 3 already taught you everything else.

Live = presented in the 45 minutes Self-study = the full depth, after class 🔍 Every diagram zooms

★ What you walk out with tonight. A working picture of what a chain is and why you run several. Three diagnostics and what each one catches: R-hat for chains that disagree, effective sample size for draws that are not as many as they look, divergences for geometry the sampler could not follow. The ArviZ 1 names, since the old ones are gone: plot_trace_rank, az.rhat, az.ess, a DataTree in place of InferenceData. And the sentence that stops a green diagnostic from being over-read: it says the sampler explored this model's posterior, and nothing about whether the model is right.

LiveConjugacy runs out; the walk begins4 min▶

Session 6 will put all twelve Halloway outlets in one model: a rate per outlet, and above them two more parameters that say where the outlets cluster and how tightly. Each outlet's rate depends on those two, and those two depend on every outlet's rate. That mutual dependence is what "interact" means, and it is the end of the closed form. There is no grid fine enough either; session 3 already explained why the grid dies with the second parameter.

Markov chain Monte Carlo sidesteps the whole problem. Instead of evaluating the posterior everywhere, it starts somewhere, proposes a step, and accepts or rejects it by comparing the posterior's height at the two points, which only needs the unnormalised product of prior and likelihood, the thing you can always compute. Run that for long enough and the places the walk visits are distributed as the posterior. The walk's record is a chain, and a chain is a set of draws. Read it as session 3 read its draws: mean, quantiles, probabilities above a threshold.

Real world

Halloway's analyst first ran a sampler on the single Westfield rate, where the Beta answer was already known: 0.142 under the flat prior. The sampler gave a mean near 0.142. That was not wasted time. It was the only fit she would ever run where a wrong answer could be recognised on sight, and it is the fit tonight's build-along repeats for the same reason.

Self-studyThe words that will keep coming back4 min▶
  • Chain. One walk through the posterior, recorded step by step. Each step depends only on the one before it, which is the Markov part. The record is a set of draws.
  • Warm-up, tuning. The first stretch of a chain, spent finding the posterior and adjusting the step size. Discarded. Under nutpie in PyMC 6 the default is 400 steps; under PyMC's own NUTS it is 1000.
  • NUTS, nutpie. NUTS is the sampler family PyMC uses. nutpie is a separate implementation of it, and when it is installed PyMC 6 uses it by default. pip install pymc[nutpie].
  • R-hat. Do the chains agree with each other? A ratio that is 1 when they do and grows as they do not.
  • Effective sample size, ESS. How many independent draws the chain is really worth. Consecutive steps of a walk are correlated, so a chain of many draws can carry the information of far fewer.
  • Divergence. A step the sampler had to abandon because the posterior's geometry was too sharp to follow. Each one is a place the sampler could not go.
  • DataTree. The container a fit comes back in: an xarray.DataTree with a group per kind of output. The posterior is dt["posterior"].dataset. InferenceData no longer exists.
  • Rank plot. plot_trace_rank. Every draw across all chains is ranked, and each chain's ranks are histogrammed. Flat means mixed; sloped means a chain is living in one part of the posterior.
Part 1 · covers chains, warm-up and why several

What a chain is, and why one is never enough

10 min live

A single chain cannot tell you it is stuck. From the inside, a walk confined to one corner of the posterior looks exactly like a walk that has found the whole thing: the draws vary, the mean settles, nothing complains. The only way to know is to start several walks from different places and see whether they end up describing the same distribution. That comparison is what R-hat measures, and the figure shows the case it was built for.

Four chains, one posterior. Three found it during warm-up. One is still on its way. warm-up / tuning: discarded 400 steps under nutpie, 1000 under PyMC's NUTS kept draws begin rate draw number, left to right chain 4 started far away and is still separated after warm-up chain 1 chain 2 chain 3 chain 4, the one R-hat is for R-hat compares the chains to each other: 1 when they agree, larger as they disagree. This picture does not pass. From inside chain 4 alone nothing looks wrong. That is why one chain is never enough.
🔍 Click to zoom - three chains agree from the first kept draw; the fourth is still arriving. Only the comparison reveals it.
LiveWarm-up, tuning, and what nutpie changed3 min▶

A chain's first stretch is spent getting from wherever it started to the region where the posterior lives, and adjusting its step size so that it moves through that region efficiently. Those steps are not draws from the posterior and are thrown away; the word for them is warm-up, or tuning. PyMC 6 with nutpie installed makes nutpie the default NUTS sampler and tunes for 400 steps by default, against 1000 under PyMC's own NUTS. Install with pip install pymc[nutpie]. PyMC 6 needs Python 3.12 or later, and it is built on PyTensor 3.0 with numba as the default backend.

The figure's fourth chain is the case warm-up exists for, and the case it does not always fix. It started far from the posterior, and 400 steps was not enough for it to arrive. Kept draws from a chain that has not arrived are not draws from the posterior; they will pull the mean toward wherever the chain has been, and nothing in that chain's own record will say so.

Self-studyWhy several chains, and what R-hat compares3 min▶

Run four chains from four different starting points. If they have all found the posterior, each chain's draws will spread over the same region, and the spread between the chains' means will be small compared with the spread within each chain. If one is stuck, or still travelling, the between-chain spread will be large relative to the within-chain spread. R-hat is that comparison as a single number: 1 when the chains agree, larger as they disagree.

The value of the idea is that it needs nothing from you. You do not have to know where the posterior is, or what it looks like, to notice that four walks meant to describe the same thing are describing different things. What R-hat cannot do is tell you that four chains which agree with each other have found the whole posterior rather than the same part of it, which is why it is one of three diagnostics and not the only one.

Part 2 · covers R-hat, ESS, divergences and the rank plot

Three ways a sampler lies, and the plot that shows mixing

11 min live

A chain can disagree with the others, which R-hat catches. A chain can agree while moving so sluggishly that a thousand draws carry the information of a few dozen, which effective sample size catches. And a sampler can hit a region of the posterior whose geometry it cannot follow and abandon steps there, which shows up as divergences. The plot to read for the first two is the rank plot, plot_trace_rank, and the figure shows what healthy and unhealthy look like on it.

The rank plot. Rank every draw across all chains, then histogram each chain's ranks. Healthy: every chain's ranks are spread evenly chain 1 chain 2 chain 3 chain 4 low ranks high ranks Unhealthy: two chains live in different parts of the posterior chain 1 chain 2 chain 3 chain 4 low ranks high ranks Flat rows mix. Sloped rows do not. In ArviZ 1 this is plot_trace_rank; the old plot_trace is gone, split into plot_trace_dist and plot_trace_rank. Chain 1 on the right owns the low ranks and chain 4 the high ones: they are describing different parts of the same posterior.
🔍 Click to zoom - the rank plot turns "do the chains mix" into a shape you can see at a glance. Flat is good.
LiveThree diagnostics, three failures, the ArviZ 1 calls5 min▶

R-hat asks whether the chains agree. az.rhat(dt). It catches the figure in Part 1: a chain still travelling, or stuck in one region while the others are elsewhere. Effective sample size asks how many independent draws the chains are really worth. az.ess(dt, sample_dims=["chain", "draw"]), or the xarray accessor dt.azstats.ess() after import arviz_stats. A chain moves in small correlated steps, so consecutive draws repeat information; ESS is the count after that repetition is discounted, and it is the number to compare with the 20,000 draws session 3 used, not the raw length of the chain. Divergences are steps the sampler abandoned because the posterior's geometry was too sharp to follow. Each one marks a place the sampler could not go, and a posterior it could not fully visit is a posterior it is not fully reporting.

The plots have new names. plot_trace no longer exists; it split into plot_trace_dist, which shows each parameter's distribution per chain, and plot_trace_rank, which shows the rank histograms in the figure. The rank plot is the one to read for mixing. Every ArviZ 1 plot returns a PlotCollection rather than axes. ArviZ itself is now three libraries under one namespace, arviz-base for data, arviz-stats for diagnostics, arviz-plots for plotting, all reached through import arviz as az.

LiveThe container: a DataTree, not an InferenceData2 min▶

What pm.sample() hands back is an xarray.DataTree: a tree of groups, one per kind of output, each group an xarray Dataset. The posterior draws are at dt["posterior"].dataset, with dimensions chain and draw, which is why az.ess takes sample_dims=["chain", "draw"]. arviz.InferenceData no longer exists; anything you read that returns one is describing a version that is gone.

Old files still open. az.from_netcdf now wraps open_datatree, so a netCDF or zarr file saved from an earlier fit loads into the new container without conversion. The groups are reached the same way.

Self-studyWhat a passing diagnostic proves, and what it does not3 min▶

R-hat near 1, a healthy effective sample size, no divergences: that says the sampler explored the posterior of this model. The draws are a faithful picture of what this prior and this likelihood, taken together, imply about the rate. It is a statement about the computation, and it is the statement you need before you trust a single number that comes out of it.

It says nothing about whether the model is right. A binomial likelihood with a flat prior on a single rate can be sampled perfectly and still be the wrong model for a chain of twelve outlets that differ; the diagnostics will be green and the estimate for Northgate will still be far from 0.192. Checking the model against the data it is meant to describe is a different job, with different tools, and session 8 owns it. Tonight's rule is to keep the two questions apart: did the sampler work, and is the model any good. A green diagnostic answers the first. Nothing in tonight's toolkit answers the second.

Part 3 · build-along

Sample the Westfield rate and read the diagnostics

16 min build-along

The single Westfield rate: Beta(1, 1) prior, binomial likelihood, 127 redemptions in 900. Session 3 gave the exact answer, a posterior mean of 0.142. Tonight the same model goes through PyMC 6.3 and a sampler, so that the one time a wrong answer would be obvious is the time you learn to read the diagnostics. Session 5 builds on this file.

Install. PyMC 6 needs Python 3.12 or later. The nutpie extra makes nutpie the default NUTS sampler.

shell
pip install pymc[nutpie]

Write the model and sample. One rate, a flat prior, the observed count. pm.sample() with no arguments uses the defaults: nutpie NUTS, 400 tuning steps, several chains. It returns a DataTree.

westfield_mcmc.py
import pymc as pm
import arviz as az

# Westfield: 127 redemptions in 900 vouchers, flat Beta(1, 1) prior
with pm.Model() as westfield:
    rate = pm.Beta("rate", alpha=1, beta=1)
    redeemed = pm.Binomial("redeemed", n=900, p=rate, observed=127)
    dt = pm.sample()                    # nutpie NUTS by default; returns a DataTree

print(dt["posterior"].dataset)          # the posterior group: dims chain, draw

Read the three diagnostics. R-hat for agreement between chains, ESS for how many independent draws you really have. Divergences, if any, are reported by the sampler as it runs.

diagnostics.py
print(az.rhat(dt))                                  # chains agree: 1
print(az.ess(dt, sample_dims=["chain", "draw"]))    # independent draws, discounted

# the same ESS through the xarray accessor
import arviz_stats
print(dt.azstats.ess())

Look at the rank plot. Flat rows per chain mean the chains mix. The call returns a PlotCollection, not axes.

rank_plot.py
az.plot_trace_rank(dt)                  # flat = mixed; the old plot_trace is gone

Summarise, and check against the algebra. The mean should land near 0.142, the exact flat-prior answer from session 3. The interval az.summary reports is the ArviZ 1 default, 89 percent equal-tailed, and the page you paste it into must say so.

summary.py
print(az.summary(dt))                   # mean near 0.142; 89 percent equal-tailed interval

Write the diagnostic sentence beside the estimate. "Four chains, nutpie NUTS, 400 tuning steps; R-hat 1, no divergences, rank plot flat; mean near 0.142." Then the second sentence, every time: "the diagnostics say the sampler explored this model's posterior; whether the model is right is session 8's question."

If anything you find online says pymc3, InferenceData, hdi_prob or plot_trace, it is describing a version that no longer exists. pymc3 is a dead import. hdi_prob was renamed prob. McElreath's own 2024 course page still lists "Python with PyMC3"; the ideas are current, that line is not.

Check yourself

Three questions

A colleague runs one long chain on the session 6 hierarchical model, the mean settles, and they call it converged. What is missing?

From inside a single chain a stuck walk and a healthy one look alike. R-hat compares between-chain spread with within-chain spread, which is only possible with several chains. That is why the default runs more than one.

R-hat is 1, ESS is healthy, there are no divergences, and the rank plot is flat. What have you established?

Diagnostics are about the computation. A single-rate model can sample perfectly and still be the wrong model for twelve differing outlets. Keep "did the sampler work" and "is the model any good" as two questions with two toolkits.

A tutorial you found calls az.plot_trace(idata) and describes the result as an InferenceData. Under PyMC 6.3 and ArviZ 1, what do you write instead?

plot_trace is gone, split into plot_trace_dist and plot_trace_rank. InferenceData no longer exists; the container is an xarray.DataTree. plot_posterior itself became plot_dist.

Homework

Before session 5

30 min

Run tonight's Westfield file again for Northgate: 1 redemption in 18, same flat prior. Read R-hat, ESS and the rank plot, then compare az.summary against session 3's exact figures, mean 0.100 and the 89 percent equal-tailed interval 0.020 to 0.222. Write one line under each diagnostic saying what it does and does not tell you about the fit. Session 5 takes this same file and builds the first proper PyMC model on it end to end; bring the file and the lines.

Sources covered

What tonight maps to

✓ Markov chain Monte Carlo as sampling a posterior with no closed form Statistical Rethinking lecture 8, "MCMC". Why conjugacy runs out with two interacting parameters; what a chain is; warm-up; why several chains.
✓ R-hat, effective sample size, divergences What each catches. az.rhat(dt), az.ess(dt, sample_dims=["chain", "draw"]), dt.azstats.ess() after importing arviz_stats. plot_trace_rank for mixing.
✓ PyMC 6.3 and ArviZ 1 as they are in September 2026 nutpie as default NUTS with 400 tuning steps against 1000; pip install pymc[nutpie]; Python 3.12 or later; the DataTree container; InferenceData and plot_trace gone; az.from_netcdf wrapping open_datatree.
◐ The first full PyMC model, end to end Session 5. Tonight's Westfield fit is the scaffold; session 5 builds the model properly and reads az.summary in full.
◐ Whether the model is right Session 8. Prior and posterior predictive checks. Tonight's diagnostics establish that the sampler worked and nothing more.
◐ The first posterior that needs a sampler Session 6. The hierarchical model over twelve outlets, the first model here with parameters that interact.
· Confidence intervals and hypothesis tests Not covered by design. learn-statistics-with-phoebe owns frequentist inference.

Session 4 cheat sheet

Why sampleConjugacy ends with two interacting parameters. MCMC draws from a posterior you cannot write down.
A chain is a walkEach step depends only on the last. Its record is a set of draws; read them as session 3 did.
Warm-up is discarded400 steps under nutpie, 1000 under PyMC's own NUTS. pip install pymc[nutpie].
Several chains, or nothingOne chain cannot tell you it is stuck. R-hat compares them: 1 when they agree.
ESS, not chain lengthCorrelated steps repeat information. az.ess(dt, sample_dims=["chain", "draw"]).
Divergences are places not visitedGeometry the sampler could not follow. A posterior not fully visited is not fully reported.
plot_trace_rank, DataTreeplot_trace is gone; InferenceData is gone. dt["posterior"].dataset holds the draws.
Green means the sampler workedNot that the model is right. That is session 8's question, asked with different tools.