Sampler Statistics#

import arviz as az
import matplotlib.pyplot as plt
import pymc as pm

print(f"Running on PyMC v{pm.__version__}")
Running on PyMC v5.28.0+58.gf58491a3
az.style.use("arviz-variat")
plt.rcParams["figure.constrained_layout.use"] = False

When checking for convergence or when debugging a badly behaving sampler, it is often helpful to take a closer look at what the sampler is doing. For this purpose some samplers export statistics for each generated sample.

As a minimal example we sample from a standard normal distribution:

model = pm.Model()
with model:
    mu1 = pm.Normal("mu1", mu=0, sigma=1, shape=10)
with model:
    step = pm.NUTS()
    idata = pm.sample(2000, tune=1000, init=None, step=step, chains=4)
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [mu1]

Sampling 4 chains for 1_000 tune and 2_000 draw iterations (4_000 + 8_000 draws total) took 6 seconds.
  • Note: NUTS provides the following statistics (these are internal statistics that the sampler uses, you don’t need to do anything with them when using PyMC, to learn more about them, pymc.NUTS.

idata.sample_stats
<xarray.DataTree 'sample_stats'>
Group: /sample_stats
    Dimensions:                (chain: 4, draw: 2000)
    Coordinates:
      * chain                  (chain) int64 32B 0 1 2 3
      * draw                   (draw) int64 16kB 0 1 2 3 4 ... 1996 1997 1998 1999
    Data variables: (12/18)
        acceptance_rate        (chain, draw) float64 64kB 0.5117 0.8243 ... 0.6885
        n_steps                (chain, draw) float64 64kB 3.0 7.0 7.0 ... 7.0 7.0
        max_energy_error       (chain, draw) float64 64kB 0.9907 0.5716 ... 0.7114
        divergences            (chain, draw) int64 64kB 0 0 0 0 0 0 ... 0 0 0 0 0 0
        index_in_trajectory    (chain, draw) int64 64kB -3 -4 -5 -3 3 ... 2 3 -4 6 4
        energy                 (chain, draw) float64 64kB 22.85 22.25 ... 17.12
        ...                     ...
        tree_depth             (chain, draw) int64 64kB 2 3 3 3 3 3 ... 3 2 3 3 3 3
        reached_max_treedepth  (chain, draw) bool 8kB False False ... False False
        step_size_bar          (chain, draw) float64 64kB 0.8498 0.8498 ... 0.8669
        lp                     (chain, draw) float64 64kB -16.29 -18.61 ... -11.36
        perf_counter_diff      (chain, draw) float64 64kB 0.0003056 ... 0.0003297
        perf_counter_start     (chain, draw) float64 64kB 37.75 37.75 ... 39.12
    Attributes:
        created_at:                 2026-04-25T12:14:28.293981+00:00
        creation_library:           ArviZ
        creation_library_version:   1.1.1dev0
        creation_library_language:  Python
        inference_library:          pymc
        inference_library_version:  5.28.0+58.gf58491a3
        sample_dims:                ['chain', 'draw']
        sampling_time:              5.952891111373901
        tuning_steps:               1000

The sample statistics variables are defined as follows:

  • process_time_diff: The time it took to draw the sample, as defined by the python standard library time.process_time. This counts all the CPU time, including worker processes in BLAS and OpenMP.

  • step_size: The current integration step size.

  • diverging: (boolean) Indicates the presence of leapfrog transitions with large energy deviation from starting and subsequent termination of the trajectory. “large” is defined as max_energy_error going over a threshold.

  • lp: The joint log posterior density for the model (up to an additive constant).

  • energy: The value of the Hamiltonian energy for the accepted proposal (up to an additive constant).

  • energy_error: The difference in the Hamiltonian energy between the initial point and the accepted proposal.

  • perf_counter_diff: The time it took to draw the sample, as defined by the python standard library time.perf_counter (wall time).

  • perf_counter_start: The value of time.perf_counter at the beginning of the computation of the draw.

  • n_steps: The number of leapfrog steps computed. It is related to tree_depth with n_steps <= 2^tree_dept.

  • max_energy_error: The maximum absolute difference in Hamiltonian energy between the initial point and all possible samples in the proposed tree.

  • acceptance_rate: The average acceptance probabilities of all possible samples in the proposed tree.

  • step_size_bar: The current best known step-size. After the tuning samples, the step size is set to this value. This should converge during tuning.

  • tree_depth: The number of tree doublings in the balanced binary tree.

Some points to Note:

  • Some of the sample statistics used by NUTS are renamed when converting to InferenceData to follow ArviZ’s naming convention, while some are specific to PyMC3 and keep their internal PyMC3 name in the resulting InferenceData object.

  • InferenceData also stores additional info like the date, versions used, sampling time and tuning steps as attributes.

idata.sample_stats["tree_depth"].plot(col="chain", ls="none", marker=".", alpha=0.3);
../_images/9b963fe17e4342c054962335e6b0249369db80f4c33ed8693acabe137ea9c422.png
az.plot_dist(
    idata,
    group="sample_stats",
    var_names="acceptance_rate",
    kind="hist",
    visuals={"credible_interval": False},
);
../_images/90aac0f1bcff05078a60fef0717a86217c4bf631eed09e9a67b608c088db2d5c.png

We check if there are any divergences, if yes, how many?

idata.sample_stats["diverging"].sum()
<xarray.DataArray 'diverging' ()> Size: 8B
array(0)

In this case no divergences are found. If there are any, check this notebook for information on handling divergences.

It is often useful to compare the overall distribution of the energy levels with the change of energy between successive samples. Ideally, they should be very similar:

../_images/c2d6a56e6b6aa2572b100daaa2937c5d3bbd69a9429dea5b0cb820f51bfa35c3.png

If the overall distribution of energy levels has longer tails, the efficiency of the sampler will deteriorate quickly.

Multiple samplers#

If multiple samplers are used for the same model (e.g. for continuous and discrete variables), the exported values are merged or stacked along a new axis.

coords = {"step": ["BinaryMetropolis", "Metropolis"], "obs": ["mu1"]}
dims = {"accept": ["step"]}

with pm.Model(coords=coords) as model:
    mu1 = pm.Bernoulli("mu1", p=0.8)
    mu2 = pm.Normal("mu2", mu=0, sigma=1, dims="obs")
with model:
    step1 = pm.BinaryMetropolis([mu1])
    step2 = pm.Metropolis([mu2])
    idata = pm.sample(
        10000,
        init=None,
        step=[step1, step2],
        chains=4,
        tune=1000,
        idata_kwargs={"dims": dims, "coords": coords},
    )
Multiprocess sampling (4 chains in 4 jobs)
CompoundStep
>BinaryMetropolis: [mu1]
>Metropolis: [mu2]

Sampling 4 chains for 1_000 tune and 10_000 draw iterations (4_000 + 40_000 draws total) took 17 seconds.
list(idata.sample_stats.data_vars)
['accepted', 'accept', 'scaling', 'p_jump']

Both samplers export accept, so we get one acceptance probability for each sampler:

az.plot_dist(
    idata,
    group="sample_stats",
    var_names="accept",
    kind="ecdf",
);
../_images/cb8b481aa2b99918f3852bbece8cae57e9894bbec7930cc1a8cb57d516993f96.png

We notice that accept sometimes takes really high values (jumps from regions of low probability to regions of much higher probability).

# Range of accept values
idata.sample_stats["accept"].max("draw") - idata.sample_stats["accept"].min("draw")
<xarray.DataArray 'accept' (chain: 4, accept_dim_0: 2)> Size: 64B
array([[  3.75      , 153.17870672],
       [  3.75      , 734.08661568],
       [  3.75      , 127.786444  ],
       [  3.75      , 252.56104158]])
Coordinates:
  * chain         (chain) int64 32B 0 1 2 3
  * accept_dim_0  (accept_dim_0) int64 16B 0 1

Authors#

  • Updated by Meenal Jhajharia in April 2021 (pymc-examples#95)

  • Updated to v4 by Christian Luhmann in May 2022 (pymc-examples#338)

  • Updated by Osvaldo Martin in Dec 2025

  • Updated by Osvaldo Martin in Apr 2026

Watermark#

%load_ext watermark
%watermark -n -u -v -iv -w
Last updated: Sat, 25 Apr 2026

Python implementation: CPython
Python version       : 3.14.4
IPython version      : 9.12.0

arviz     : 1.1.0
matplotlib: 3.10.8
pymc      : 5.28.0+58.gf58491a3

Watermark: 2.6.0

License notice#

All the notebooks in this example gallery are provided under the MIT License which allows modification, and redistribution for any use provided the copyright and license notices are preserved.

Citing PyMC examples#

To cite this notebook, use the DOI provided by Zenodo for the pymc-examples repository.

Important

Many notebooks are adapted from other sources: blogs, books… In such cases you should cite the original source as well.

Also remember to cite the relevant libraries used by your code.

Here is an citation template in bibtex:

@incollection{citekey,
  author    = "<notebook authors, see above>",
  title     = "<notebook title>",
  editor    = "PyMC Team",
  booktitle = "PyMC examples",
  doi       = "10.5281/zenodo.5654871"
}

which once rendered could look like:

Meenal Jhajharia , Christian Luhmann . "Sampler Statistics". In: PyMC Examples. Ed. by PyMC Team. DOI: 10.5281/zenodo.5654871