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

PyMC, the first model

Four sessions of arithmetic, a grid and a hand-rolled sampler, and tonight the tool takes over. Westfield redeemed 127 vouchers of 900. Under a flat Beta(1, 1) prior the posterior mean is 0.142, and session 1 got there with two additions. The point of tonight is not the number, it is that the same model, written in PyMC 6.3.2 the way the tool is on 18 September 2026, reproduces it: a model block, a sample call that hands the work to nutpie, a result that arrives as an xarray.DataTree, and a summary whose interval is 89 percent equal-tailed because that is what ArviZ 1.3.0 reports unless told otherwise. Every one of those four facts changed within the last sixteen months, and a notebook copied from 2023 gets all four wrong.

🟠 Working depth Data scientists and analysts Anyone moving a Bayesian notebook to PyMC 6 45 min
0-3 · Welcome 3-16 · The stack as it is in 2026 16-40 · Build-along: Westfield end to end 40-45 · Q&A
Part 0

Where tonight sits

Session 1 wrote the posterior by hand: a + y and b + n - y. Session 3 turned the posterior into samples and read summaries off them as quantiles. Session 4 explained why a sampler is needed at all and what R-hat and effective sample size are checking. Tonight puts all of that behind one call to pm.sample() and asks you to trust it only as far as the diagnostics allow. Session 6 uses the same tool on twelve outlets at once. The running example is still Halloway Bakeries, and tonight's outlet is Westfield, the flagship, because 900 vouchers is the case where the prior matters least and the tooling matters most.

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

★ What you walk out with tonight. One complete PyMC 6 script for the Westfield model, short enough to retype: imports, a model block, prior predictive, sample, diagnostics, summary, plot. The map from every name a 2023 notebook uses to the name the tool uses now: pymc3 to pymc, InferenceData to DataTree, plot_posterior to plot_dist, hdi_prob to prob. And the one default that silently changes every number: the summary interval is 89 percent equal-tailed, cut at the 5.5 and 94.5 percent quantiles, where it used to be a 94 percent HDI.

LiveThe tool as it is on 18 September 20264 min▶

PyMC is at 6.3.2, released on 8 September 2026. The 6.0.0 release shipped on 13 May 2026 and it was a breaking one: PyMC 6 is built on PyTensor 3.0, the default compute backend switched to numba, and if nutpie is installed it is the default NUTS sampler, tuning for 400 steps where PyMC's own NUTS tunes for 1000. It requires Python 3.12 or later. The install that gets you the default sampler is pip install pymc[nutpie].

ArviZ is at 1.3.0 and is no longer one library. It is three, arviz-base for data and converters, arviz-stats for diagnostics, arviz-plots for plotting, all reached through the same import arviz as az. The result container changed with it: arviz.InferenceData no longer exists and what pm.sample() returns is an xarray.DataTree. Existing netCDF and zarr files still load, because az.from_netcdf now wraps open_datatree, and a group is reached as dt["posterior"].dataset.

Real world

Halloway's analyst inherited a voucher notebook that began import pymc3 as pm. On the current tool that line fails at import, which is the kind version of the problem. The unkind version is the notebook that imports cleanly, runs, and reports intervals cut under a default that changed underneath it while the prose still describes the old one. Tonight's session exists so that the second thing does not happen to you.

Self-studyThe words that will keep coming back4 min▶
  • PyTensor 3.0. The tensor library PyMC 6 compiles your model with. Its default backend is numba; pytensor.config.linker = "cvm" reverts to the old one.
  • nutpie. The NUTS sampler PyMC 6 hands the work to when it is installed. 400 tuning steps by default against 1000 for PyMC's own NUTS.
  • xarray.DataTree. The container a sample call returns. Groups such as the posterior are reached as dt["posterior"].dataset. It replaced InferenceData.
  • arviz-base, arviz-stats, arviz-plots. The three libraries that make up ArviZ 1.3.0. You do not import them separately for ordinary work; import arviz as az reaches all three.
  • PlotCollection. What every ArviZ 1.x plot returns. Not axes.
  • prob. The renamed hdi_prob. The interval level argument on summaries and plots.
  • stats.ci_prob and stats.ci_kind. The two rcParams that set the default interval: 0.89 and "eti". They were 0.94 and HDI.
  • Equal-tailed interval. The 5.5 and 94.5 percent quantiles of the posterior draws. Every interval in these sessions is one, and every page says so.
Part 1 · covers the stack from model block to plot

Eight boxes between your model and your picture

7 min live

You write the model block and you read the summary table. Everything in between is other people's software, and in 2026 almost every box in that chain has a new name or a new default. The figure is the chain as it is now. Learn it once and the renames in Part 2 stop being trivia and become a map of where the work moved.

The PyMC 6 stack, September 2026. Top row runs the model; bottom row reads the result. 1 · Your model block with pm.Model(): pm.Beta(1, 1) pm.Binomial(...) observed 127 of n = 900 2 · PyMC 6.3.2 released 8 September 2026 6.0.0 shipped 13 May 2026 Python 3.12 or later 3 · PyTensor 3.0 default backend: numba pytensor.config.linker = "cvm" reverts to the old one 4 · The sampler nutpie when installed 400 tuning steps by default PyMC's own NUTS: 1000 pm.sample() returns 5 · xarray.DataTree the result container dt["posterior"].dataset InferenceData is gone 6 · arviz-base data and converters az.from_netcdf wraps open_datatree 7 · arviz-stats az.rhat(dt), az.ess(...) dt.azstats.ess() ci_prob 0.89, ci_kind "eti" 8 · arviz-plots plot_dist, plot_ppc_dist plot_trace_rank plot_forest, plot_ridge every plot: a PlotCollection All of it is reached through import pymc as pm and import arviz as az. ArviZ 1.3.0 is three libraries under one name.
🔍 Click to zoom - boxes 1 and 5 are the two you touch. Boxes 2, 3 and 4 changed in May 2026; boxes 6, 7 and 8 changed when ArviZ went to 1.x.
LiveWhat PyMC 6.0 broke, and which defaults now decide your run4 min▶

Three of the changes in the 6.0.0 release notes touch every model. The compute backend is numba, through PyTensor 3.0; you can revert to the previous one with pytensor.config.linker = "cvm", and you will want to know that line exists the first time a compile surprises you. The sampler is nutpie whenever it is installed, which is why the install line is pip install pymc[nutpie] rather than plain pymc. And nutpie tunes for 400 steps by default where PyMC's own NUTS tunes for 1000, so a run on the new default is doing less warm-up than the same run a year ago, which is precisely why session 4's diagnostics are read every time rather than trusted once.

Two smaller changes bite specific lines. sample_prior_predictive lost its deprecated samples argument, so a notebook that asks for a particular number of prior draws by that name stops at that line. sample_posterior_predictive gained sample_vars and freeze_vars, which session 8 uses. Several functions were also removed from the root namespace. None of this is exotic; it is the ordinary churn of a tool at a major version, and the fix is to write the model for the tool you have rather than the one the notebook remembers.

LiveThe result is a DataTree, and you read it by group3 min▶

What pm.sample() hands back is an xarray.DataTree. It holds groups, and the one you want most nights is the posterior, reached as dt["posterior"].dataset: an xarray dataset with a chain dimension and a draw dimension, which is exactly the shape session 4 said a set of chains has. The diagnostics are written for that shape. az.rhat(dt) and az.ess(dt, sample_dims=["chain", "draw"]) take the tree directly, and after import arviz_stats the same effective sample size is one accessor away as dt.azstats.ess().

The old container, arviz.InferenceData, is gone from the tool, but the files it wrote are not stranded. az.from_netcdf now wraps open_datatree, so a result saved last year opens as a DataTree this year. The thing that does not survive the move is code that reaches into the old container by its old attributes. The honest rule for a migration is to load the file, look at the tree, and rewrite the reads.

Self-studyThe interval default changed, and it matters for every number4 min▶

The rename everyone notices is hdi_prob to prob. The change that matters is in the defaults behind it. Two rcParams now govern every summary interval: stats.ci_prob is 0.89, where it was 0.94, and stats.ci_kind is "eti", an equal-tailed interval, where it was HDI. The migration guide's reason for the level: "Using 0.89 produces intervals with lower variability, leading to more stable summaries." So a summary table produced today, with no arguments, reports an 89 percent interval cut at the 5.5 and 94.5 percent quantiles of the draws. The same call in 2023 reported a 94 percent highest-density interval.

That is not a cosmetic difference. A third party, the CoursIA repo in its issue #15592, documented two notebooks whose intervals silently became equal-tailed on upgrade while the prose beside them still said HDI. The code ran, the numbers changed, the words did not. Every interval in these sessions is therefore stated as what it is, an 89 percent equal-tailed interval, and the sentence you write for anyone else should carry both the level and the kind. If you need a different interval, pass prob and say so in the text; if you take the default, say that too. What you may not do is leave the reader to assume.

Part 2 · covers the renames and how to read the summary

Every old name, its new name, and the one that changes your numbers

6 min live

Most of the renames are one-for-one and a search will fix them. Two are not. The plot that used to be one call is now two, and the interval that used to be 94 percent HDI is now 89 percent equal-tailed. The left column is what a notebook copied in 2023 says. The right column is the tool.

Old name to new name. Eight rows are a search and replace. Row seven changes your numbers. A notebook copied in 2023 PyMC 6.3.2 and ArviZ 1.3.0, September 2026 import pymc3 as pm import pymc as pm arviz.InferenceData xarray.DataTree, read as dt["posterior"].dataset az.plot_trace(...) az.plot_trace_dist(...), az.plot_trace_rank(...) az.plot_posterior(...), az.plot_density(...) az.plot_dist(...) az.plot_ppc(...) az.plot_ppc_dist(...) hdi_prob=0.94 prob, default from rcParam stats.ci_prob = 0.89 94 percent HDI by default 89 percent equal-tailed; stats.ci_kind = "eti" sample_prior_predictive(samples=...) sample_prior_predictive(), no samples argument plots return matplotlib axes every plot returns a PlotCollection Row one fails at import, the kind failure. Row seven runs and quietly changes every interval, the unkind one.
🔍 Click to zoom - the red column is not wrong code, it is code for a tool that no longer exists. The two rows about the interval are the ones that change what you report.
LiveThe walk: model, prior predictive, sample, diagnose, summarise, plot3 min▶

Six moves, always in this order. Model: inside with pm.Model(), a pm.Beta for the rate with the flat Beta(1, 1) of session 2, two pseudo-observations, and a pm.Binomial with n=900 and observed=127. Prior predictive: pm.sample_prior_predictive() before touching the data, to see what counts out of 900 the prior alone considers plausible; under a flat prior that is every count from 0 to 900 equally, which is what "flat" means and why session 2 counts it as worth only two pseudo-observations. Sample: pm.sample(), which goes to nutpie if it is installed. Diagnose: az.rhat(dt) and az.ess(dt, sample_dims=["chain", "draw"]), read the way session 4 taught, before anything is reported. Summarise: az.summary(dt), one row for the rate. Plot: az.plot_dist, the picture of the posterior.

The number you are checking against is 0.142, the flat-prior posterior mean at Westfield that session 1 got with a + 127 and b + 773. If the summary row says something else, the model is wrong, not the arithmetic. That is the whole reason to run the tool on a case whose answer you already hold: the first model is a test of the tooling, and only after it passes does the tool get to tell you anything you did not already know.

LiveReading the summary row without importing another vocabulary3 min▶

The row az.summary(dt) prints for the rate has three kinds of column, and all of them are statements about the posterior draws. Location and spread: the posterior mean, which at Westfield under the flat prior is 0.142, and the spread of the draws around it. The interval: two columns giving the ends of the default interval, which in ArviZ 1.3.0 is 89 percent equal-tailed, the 5.5 and 94.5 percent quantiles of the draws. Read them as session 3 taught: an 89 percent probability, given this prior and these 900 vouchers, that the rate sits between them. The diagnostics: effective sample size and R-hat, the session 4 checks, telling you how much to trust the columns to their left.

What the row does not contain is anything from the other tradition. There is no confidence interval on it and no p-value, and the interval columns must not be read as either; learn-statistics-with-phoebe owns those words and the procedures behind them. The credible interval is the sentence everybody thinks a confidence interval is, and the summary row is where that sentence's two numbers come from. Say the level and say the kind: "0.142, with an 89 percent equal-tailed interval." Both halves are defaults, and defaults change.

Self-studyThe notebook copied in 20233 min▶

Three words tell you a notebook predates the tool: pymc3, InferenceData, hdi_prob. Any one of them is enough. The first is a dead import and will not run. The second names a container that no longer exists. The third names an argument that was renamed, and its presence means every interval in the notebook was cut under the old default, a 94 percent HDI, so none of its reported intervals matches what the current tool prints for the same model. McElreath's own 2024 course page still lists "Python with PyMC3"; do not copy that either.

The fix is not to patch the three lines. It is to rerun the model on the current tool, read the new summary, and rewrite every sentence that quoted an interval, because the level and the kind both changed. A notebook whose code was patched and whose prose was not is exactly the case the CoursIA issue documented: correct-looking output under a description that is no longer true of it. If you are handed such a notebook, the two-line reply is "this predates PyMC 6 and ArviZ 1; the intervals need recutting and restating before anyone quotes them."

Part 3 · build-along

Westfield, end to end, in one script

24 min build-along

Laptop out. Python 3.12 or later, then pip install pymc[nutpie], which brings ArviZ with it. The script below is the whole session in a form you can retype. Every number in it is in a comment, because the point is that the tool reproduces them, not that you type them in.

Check the versions before you write a line. PyMC should report 6.3.2 or later and ArviZ 1.3.0 or later. If PyMC reports 5-something, the defaults in this session do not apply and the summary interval will not be what the script's comments say.

Write the model block. A Beta(1, 1) prior on the rate, two pseudo-observations, and a Binomial with n = 900 and 127 observed. Nothing else. The first model is a test of the plumbing and the plumbing should have nowhere to hide.

Sample the prior predictive first. With no data in play, what does the model think 900 vouchers could produce? Under the flat prior, any count at all. That is what you asked for, and seeing it is the check that the prior you wrote is the prior you meant.

Sample, then read R-hat and effective sample size before anything else. nutpie has tuned for 400 steps, not 1000. Session 4's checks are the reason you are allowed to trust the next two lines.

Read the summary row. The posterior mean should be 0.142 to three decimals. The interval columns are the 5.5 and 94.5 percent quantiles: 89 percent, equal-tailed, the default. Say both when you quote them.

Plot the posterior with plot_dist. It returns a PlotCollection, not axes, so code that reaches for an axes object afterwards needs changing. Look at the picture; it should be a narrow hill around 0.142, which is what 900 vouchers buy.

★ Run it now (Python) - the Westfield model in PyMC 6# Westfield: 127 redeemed of 900, flat Beta(1, 1) prior # PyMC 6.3.2 on Python 3.12 or later. Install: pip install pymc[nutpie] import pymc as pm import arviz as az print(pm.__version__, az.__version__) # 6.3.2 and 1.3.0 as of 18 September 2026 with pm.Model() as westfield: rate = pm.Beta("rate", 1.0, 1.0) # flat prior, worth 2 pseudo-observations pm.Binomial("redeemed", n=900, p=rate, observed=127) pm.sample_prior_predictive() # PyMC 6: the samples argument is gone dt = pm.sample() # nutpie when installed, 400 tuning steps # session 4's checks come first; the result is an xarray.DataTree print(az.rhat(dt)) print(az.ess(dt, sample_dims=["chain", "draw"])) # one row for the rate: mean 0.142, interval 89 percent equal-tailed by default # (rcParams stats.ci_prob = 0.89, stats.ci_kind = "eti"; the 5.5 and 94.5 percent quantiles) print(az.summary(dt)) posterior = dt["posterior"].dataset # the group, as a dataset pc = az.plot_dist(dt) # replaces plot_posterior; returns a PlotCollection

If the summary does not say 0.142, stop. The flat-prior posterior mean at Westfield is known from session 1, so a different number is a wrong model, a wrong observed count, or a tool that is not the one this session describes. Run print(pm.__version__) before you debug anything else; a notebook that imports cleanly on PyMC 5 will produce a different interval and call it by the wrong name.

Check yourself

Three questions

A colleague sends a voucher notebook whose first line is import pymc3 as pm and whose summaries pass hdi_prob=0.94. What do those two facts tell you?

pymc3 is a dead import, InferenceData no longer exists, and hdi_prob was renamed prob. The rcParams stats.ci_prob and stats.ci_kind moved the default from a 94 percent HDI to an 89 percent equal-tailed interval, so a patched notebook that keeps its old prose is exactly the silent failure the CoursIA issue documented.

You install with pip install pymc[nutpie] and call pm.sample() with no arguments. Which sampler runs, and how long does it tune?

PyMC 6 made nutpie the default when it is present, with 400 tuning steps by default against 1000 for PyMC's NUTS. Less warm-up by default is one more reason R-hat and effective sample size are read on every run.

az.summary(dt) prints two interval columns for Westfield's rate. With no arguments passed, what are they?

The default level is 0.89 and the default kind is equal-tailed, both set by rcParams that changed in ArviZ 1.x. The columns are quantiles of the draws and describe the posterior; they are not a confidence interval, and the summary row does not contain one.

Homework

Before session 6

40 min

Run tonight's script unchanged and confirm 0.142. Then replace 127 and 900 with a count and a total from your own work, a rate you have quoted from a single group, and run it again. Write the one sentence you would send about it, with the level and the kind of interval stated: "an 89 percent equal-tailed interval." Bring the sentence, and bring the twelve-group version of your data if you have one, because session 6 fits all twelve at once and lets the eleven inform the twelfth.

Sources covered

What tonight maps to

✓ PyMC 6.3.2 as released, from the 6.0.0 release notes and PyPI 6.0.0 on 13 May 2026, 6.3.2 on 8 September 2026, Python 3.12 or later. PyTensor 3.0 with numba as default backend; nutpie as the default NUTS sampler when installed, 400 tuning steps against 1000; the sample_prior_predictive and sample_posterior_predictive signature changes; pymc3 as a dead import.
✓ ArviZ 1.3.0, from the 1.x migration guide arviz-base, arviz-stats, arviz-plots under one import; xarray.DataTree in place of InferenceData; plot_dist, plot_ppc_dist, plot_trace_dist and plot_trace_rank; hdi_prob renamed prob; stats.ci_prob 0.89 and stats.ci_kind "eti"; every plot a PlotCollection.
✓ The Westfield model end to end 127 of 900 under Beta(1, 1), posterior mean 0.142, matching session 1's hand arithmetic. Prior predictive, sample, R-hat and ESS, summary, plot_dist.
◐ Twelve outlets in one model Session 6. The same tool with a chain-level centre and pooling strength, learned from the data, and the honest half: six outlets helped, six hurt.
◐ Posterior predictive checks and sample_vars, freeze_vars Session 8, which uses sample_posterior_predictive and plot_ppc_dist to ask whether the model could have produced the data it was fitted to.
· Confidence intervals and hypothesis tests Not covered by design. learn-statistics-with-phoebe owns frequentist inference; the summary row tonight is read as quantiles of the posterior and nothing else.
· PyMC-Marketing and marketing mix models Not covered by design. learn-marketing-attribution-with-phoebe names the tool; here PyMC is the general library and nothing more.

Session 5 cheat sheet

PyMC 6.3.2, ArviZ 1.3.08 September 2026. 6.0.0 shipped 13 May 2026. Python 3.12 or later. pip install pymc[nutpie].
nutpie by defaultWhen installed. 400 tuning steps, against 1000 for PyMC's own NUTS. Read the diagnostics every run.
PyTensor 3.0 on numbapytensor.config.linker = "cvm" reverts the backend if a compile surprises you.
The result is a DataTreedt["posterior"].dataset. InferenceData is gone; az.from_netcdf wraps open_datatree so old files load.
Renamesplot_posterior and plot_density to plot_dist; plot_ppc to plot_ppc_dist; plot_trace to plot_trace_dist and plot_trace_rank. hdi_prob to prob.
89 percent, equal-tailedstats.ci_prob 0.89, stats.ci_kind "eti". Was 0.94 HDI. The 5.5 and 94.5 percent quantiles. Say both halves.
Westfield: 0.142127 of 900 under Beta(1, 1). If the summary disagrees, the model is wrong, not session 1.
Three words that date a notebookpymc3, InferenceData, hdi_prob. Any one means the intervals were cut under the old default. Rerun and restate.