ArviZ: Expert Bayesian Analysis
This guide covers ArviZ like an expert Bayesian modeler uses it: not just what each function does, but when to use it, what to look for, and how to interpret results.
Table of Contents
- The Expert Workflow
- InferenceData Fundamentals
- Phase 1: Immediate Post-Sampling Checks
- Phase 2: Deep Convergence Assessment
- Phase 3: Model Criticism
- Phase 4: Parameter Interpretation
- Phase 5: Model Comparison
- Advanced Techniques
- Common Interpretation Patterns
- Publication-Quality Figures
The Expert Workflow
An expert doesn't randomly try plots—they follow a systematic workflow:
┌──────────────────────────────────────────────────────────────────────┐
│ 1. SAMPLE → 2. DIAGNOSE → 3. CRITICIZE → 4. INTERPRET → 5. COMPARE │
└──────────────────────────────────────────────────────────────────────┘
Phase 1 (Immediate): Did sampling work? Check divergences, R-hat, ESS, trace plots. Phase 2 (Deep): Are chains healthy? Rank plots, energy, autocorrelation, MCSE. Phase 3 (Criticism): Does the model fit? PPC, LOO-PIT, residual analysis. Phase 4 (Interpretation): What did we learn? Posteriors, forest plots, pair plots. Phase 5 (Comparison): Which model is best? LOO-CV, WAIC, stacking.
Critical rule: Never interpret parameters (Phase 4) until Phases 1-3 pass.
InferenceData Fundamentals
ArviZ uses InferenceData—an xarray-based container. Master this to unlock ArviZ's power.
Structure
import arviz as az
# InferenceData groups:
idata.posterior # MCMC samples: (chain, draw, *dims)
idata.posterior_predictive # Predictions at observed points
idata.prior # Prior samples
idata.prior_predictive # Prior predictions
idata.observed_data # The actual data
idata.sample_stats # Sampler diagnostics (divergences, energy, etc.)
idata.log_likelihood # Pointwise log-likelihoods (for LOO/WAIC)
Essential Operations
# Access a parameter (returns xarray.DataArray)
beta = idata.posterior["beta"]
# Convert to numpy
beta_vals = idata.posterior["beta"].values # shape: (chains, draws, *dims)
# Flatten across chains
beta_flat = idata.posterior["beta"].stack(sample=("chain", "draw")).values
# Select specific chains/draws
idata.posterior["beta"].sel(chain=0, draw=slice(500, None))
# Compute statistics
idata.posterior["beta"].mean(dim=["chain", "draw"])
idata.posterior["beta"].quantile([0.025, 0.975], dim=["chain", "draw"])
# Filter variables
idata.posterior[["alpha", "beta"]]
Combining InferenceData Objects
# Add posterior predictive to existing idata
with model:
pm.sample_posterior_predictive(idata, extend_inferencedata=True)
# Or manually extend
idata.extend(pm.sample_posterior_predictive(idata))
# Merge separate idata objects
idata_combined = az.concat([idata1, idata2], dim="chain")
Saving and Loading
# Save (NetCDF format, portable)
idata.to_netcdf("results.nc")
# Load
idata = az.from_netcdf("results.nc")
# Save with compression (for large files)
idata.to_netcdf("results.nc", engine="h5netcdf",
encoding={var: {"zlib": True} for var in idata.posterior.data_vars})
Phase 1: Immediate Post-Sampling Checks
Run these checks immediately after pm.sample() returns. If any fail, do not proceed to interpretation.
The 30-Second Check
def quick_diagnostics(idata, var_names=None):
"""Run immediately after sampling."""
# 1. Divergences (must be 0 or near 0)
n_div = idata.sample_stats["diverging"].sum().item()
n_samples = idata.sample_stats["diverging"].size
div_pct = 100 * n_div / n_samples
print(f"Divergences: {n_div} ({div_pct:.2f}%)")
# 2. Summary with convergence stats
summary = az.summary(idata, var_names=var_names)
# 3. Flag problems
bad_rhat = summary[summary["r_hat"] > 1.01]
low_ess = summary[(summary["ess_bulk"] < 400) | (summary["ess_tail"] < 400)]
if len(bad_rhat) > 0:
print(f"\n⚠️ R-hat > 1.01 for: {list(bad_rhat.index)}")
if len(low_ess) > 0:
print(f"\n⚠️ Low ESS for: {list(low_ess.index)}")
if n_div > 0:
print(f"\n⚠️ {n_div} divergences detected - investigate before interpreting!")
return summary
# Usage
summary = quick_diagnostics(idata, var_names=["~offset"]) # exclude auxiliary
az.summary: The Diagnostic Swiss Army Knife
# Full summary
summary = az.summary(idata)
# Key columns to check:
# - mean, sd: posterior mean and standard deviation
# - hdi_3%, hdi_97%: 94% highest density interval
# - mcse_mean, mcse_sd: Monte Carlo standard error
# - ess_bulk: effective sample size for the bulk of the distribution
# - ess_tail: effective sample size for the tails (crucial for credible intervals)
# - r_hat: potential scale reduction factor
# Exclude auxiliary parameters (e.g., non-centered offsets)
summary = az.summary(idata, var_names=["~offset", "~raw"])
# Include specific stats only
summary = az.summary(idata, stat_funcs={"median": np.median}, extend=True)
# Custom credible interval
summary = az.summary(idata, hdi_prob=0.90)
Interpretation thresholds:
| Metric | Good | Acceptable | Investigate |
|---|---|---|---|
r_hat |
< 1.01 | < 1.05 | > 1.05 |
ess_bulk |
> 400 | > 100 | < 100 |
ess_tail |
> 400 | > 100 | < 100 |
mcse_mean |
< 5% of SD | < 10% of SD | > 10% of SD |
az.plot_trace: Visual Convergence Check
# Basic trace plot
az.plot_trace(idata, var_names=["beta", "sigma"])
# Compact mode for many parameters
az.plot_trace(idata, compact=True, combined=True)
# Rank-normalized traces (more sensitive to problems)
az.plot_trace(idata, kind="rank_bars")
az.plot_trace(idata, kind="rank_vlines")
What to look for:
✅ Good mixing (left panel): Overlapping density curves for all chains ✅ Stationarity (right panel): Fuzzy caterpillar, no trends, no steps ❌ Bad mixing: Separated densities, one chain stuck in different region ❌ Non-stationarity: Visible trends, sudden jumps, periodic patterns
Checking Divergences
# Count divergences
n_div = idata.sample_stats["diverging"].sum().item()
# Percentage
div_pct = 100 * idata.sample_stats["diverging"].mean().item()
# When did they occur? (during warmup vs sampling)
# Divergences in sample_stats are from sampling phase only
# Visualize where divergences occur in parameter space
az.plot_pair(idata, var_names=["alpha", "sigma"], divergences=True)
Divergence rules:
- 0 divergences: Ideal
- < 0.1% and random: Often acceptable, but investigate
- > 0.1% or clustered: Problem—do not trust results
Phase 2: Deep Convergence Assessment
If Phase 1 passes, these deeper checks validate that the sampler fully explored the posterior.
az.plot_rank: The Modern Convergence Test
Rank plots are more sensitive than trace plots for detecting convergence issues.
az.plot_rank(idata, var_names=["beta", "sigma"])
Interpretation: Each chain should have a roughly uniform distribution of ranks. If one chain consistently has higher or lower ranks, it hasn't converged to the same distribution.
What to look for:
- ✅ Uniform histograms across all chains
- ❌ One chain with ranks concentrated at one end
- ❌ Systematic patterns (periodicity, gaps)
az.plot_ess: Effective Sample Size Evolution
# How ESS grows with more draws
az.plot_ess(idata, var_names=["beta"], kind="evolution")
# ESS across the distribution (quantile-specific)
az.plot_ess(idata, var_names=["beta"], kind="quantile")
# Local ESS (identifies problematic regions)
az.plot_ess(idata, var_names=["beta"], kind="local")
Evolution plot interpretation:
- ✅ Linear growth: Good mixing
- ❌ Plateauing: Poor mixing, more samples won't help
- ❌ Early plateau then growth: Slow adaptation
Quantile plot interpretation:
- ✅ Roughly constant ESS across quantiles
- ❌ Low ESS at tails: Unreliable credible intervals
- ❌ Very low ESS at specific quantiles: Possible multimodality
az.plot_mcse: Monte Carlo Error Visualization
# MCSE across quantiles
az.plot_mcse(idata, var_names=["beta"])
# Local MCSE (error varies across distribution)
az.plot_mcse(idata, var_names=["beta"], kind="local")
Rule of thumb: MCSE should be < 5% of posterior SD for reliable inference.
az.plot_autocorr: Mixing Efficiency
az.plot_autocorr(idata, var_names=["beta", "sigma"])
Interpretation:
- ✅ Rapid decay to zero (within ~20 lags)
- ❌ Slow decay: High autocorrelation → low ESS
- ❌ Negative autocorrelation: Can indicate sampler issues
az.plot_energy: HMC/NUTS Health
az.plot_energy(idata)
This plot shows the marginal energy distribution and the energy transition distribution.
Interpretation:
- ✅ Overlapping distributions: Good exploration
- ❌ Large gap between distributions: Poor exploration, potential bias
- ❌ Marginal energy has long tails: Difficult posterior geometry
Expert tip: Energy plots are especially useful for diagnosing problems that R-hat and ESS miss, like when the sampler explores only part of a multimodal posterior.
az.rhat and az.ess: Programmatic Access
# Get R-hat for all parameters
rhat_values = az.rhat(idata)
# Get ESS
ess_bulk = az.ess(idata, method="bulk")
ess_tail = az.ess(idata, method="tail")
# Find problematic parameters
for var in rhat_values.data_vars:
rhat = rhat_values[var].values
if np.any(rhat > 1.01):
print(f"Warning: {var} has R-hat = {rhat.max():.3f}")
Phase 3: Model Criticism
Convergence doesn't mean the model is good—only that MCMC worked. Now assess whether the model actually fits the data.
az.plot_ppc: Posterior Predictive Checks
The most important model criticism tool. Does the model generate data that looks like the observed data?
# First, generate posterior predictive samples
with model:
pm.sample_posterior_predictive(idata, extend_inferencedata=True)
# Density overlay (default)
az.plot_ppc(idata, kind="kde")
# Cumulative distribution (better for systematic deviations)
az.plot_ppc(idata, kind="cumulative")
# Scatter plot (for continuous outcomes)
az.plot_ppc(idata, kind="scatter")
# Subsample for speed
az.plot_ppc(idata, num_pp_samples=100)
What to look for (density/KDE):
- ✅ Dark line (observed) within the cloud of light lines (predicted)
- ❌ Observed data outside predicted distribution
- ❌ Shape mismatch (e.g., model symmetric but data skewed)
What to look for (cumulative):
- ✅ Observed CDF within the posterior predictive band
- ❌ Systematic deviation (observed consistently above/below)
- ❌ Crossing pattern (model wrong in different directions at different values)
Grouped/Stratified PPC
Check fit across subgroups:
# PPC by group (if using coords)
az.plot_ppc(idata, kind="cumulative", flatten=[])
# For specific observed variable
az.plot_ppc(idata, var_names=["y_obs"], kind="kde")
Custom Posterior Predictive Checks
Sometimes you need to check specific features:
# Define test statistics
def tail_fraction(x):
"""Fraction of values > 95th percentile of observed data"""
threshold = np.percentile(idata.observed_data["y"].values, 95)
return (x > threshold).mean()
def zero_fraction(x):
"""Fraction of zeros (for zero-inflated data)"""
return (x == 0).mean()
# Compute for observed data
obs_stat = tail_fraction(idata.observed_data["y"].values)
# Compute for each posterior predictive draw
pp_stats = []
for i in range(idata.posterior_predictive.dims["draw"]):
pp_sample = idata.posterior_predictive["y"].isel(draw=i).values.flatten()
pp_stats.append(tail_fraction(pp_sample))
# Compare
import matplotlib.pyplot as plt
plt.hist(pp_stats, bins=30, alpha=0.7, label="Posterior predictive")
plt.axvline(obs_stat, color="red", linewidth=2, label="Observed")
plt.xlabel("Tail fraction")
plt.legend()
az.plot_loo_pit: Calibration Diagnostic
The LOO-PIT (Leave-One-Out Probability Integral Transform) checks calibration: are the posterior predictive quantiles uniformly distributed?
az.plot_loo_pit(idata, y="y")
Interpretation:
- ✅ Uniform distribution (flat histogram, diagonal line): Well-calibrated
- ❌ U-shape: Underdispersed (model overconfident)
- ❌ Inverted U-shape: Overdispersed (model underconfident)
- ❌ S-curve: Systematic bias
Expert insight: LOO-PIT is more sensitive than PPC for detecting calibration issues because it evaluates each observation using a model fit without that observation.
az.plot_bpv: Bayesian p-values
# Histogram of Bayesian p-values
az.plot_bpv(idata, kind="p_value")
# Using a test statistic
az.plot_bpv(idata, kind="t_stat")
Interpretation:
- Values near 0.5: Good calibration
- Values near 0 or 1: Model systematically over/under-predicts
Residual Analysis
For regression models, check residuals:
import numpy as np
# Compute posterior mean predictions
y_pred = idata.posterior_predictive["y"].mean(dim=["chain", "draw"])
y_obs = idata.observed_data["y"]
# Residuals
residuals = y_obs - y_pred
# Plot residuals vs fitted
import matplotlib.pyplot as plt
plt.scatter(y_pred, residuals, alpha=0.5)
plt.axhline(0, color="red", linestyle="--")
plt.xlabel("Fitted values")
plt.ylabel("Residuals")
What to look for:
- ✅ Random scatter around zero
- ❌ Funnel shape: Heteroscedasticity
- ❌ Curved pattern: Missing nonlinear term
- ❌ Clusters: Missing grouping structure
Phase 4: Parameter Interpretation
Only after Phases 1-3 pass should you interpret parameter estimates.
az.plot_posterior: Marginal Summaries
# Basic posterior summary
az.plot_posterior(idata, var_names=["beta", "sigma"])
# With reference value (null hypothesis)
az.plot_posterior(idata, var_names=["beta"], ref_val=0)
# With ROPE (Region of Practical Equivalence)
az.plot_posterior(idata, var_names=["beta"], rope=[-0.1, 0.1])
# Custom HDI probability
az.plot_posterior(idata, hdi_prob=0.90)
# Point estimate options
az.plot_posterior(idata, point_estimate="mode") # or "mean", "median"
ROPE interpretation:
- Report % of posterior inside ROPE
- If > 95% inside ROPE: Practically equivalent to null
- If < 5% inside ROPE: Practically different from null
az.plot_forest: Comparing Parameters
Essential for hierarchical models and comparing effects.
# Basic forest plot
az.plot_forest(idata, var_names=["alpha"], combined=True)
# With R-hat coloring (spot convergence issues)
az.plot_forest(idata, var_names=["alpha"], r_hat=True)
# With ESS sizing
az.plot_forest(idata, var_names=["alpha"], ess=True)
# Compare multiple models
az.plot_forest(
[idata_pooled, idata_hierarchical, idata_unpooled],
model_names=["Pooled", "Hierarchical", "Unpooled"],
var_names=["alpha"],
combined=True
)
# Ridgeplot style (better for many groups)
az.plot_forest(idata, var_names=["alpha"], kind="ridgeplot")
az.plot_pair: Parameter Relationships
Critical for understanding parameter correlations and identifying problems.
# Basic pair plot
az.plot_pair(idata, var_names=["alpha", "beta", "sigma"])
# With divergences (essential for debugging)
az.plot_pair(idata, var_names=["alpha", "sigma"], divergences=True)
# Different plot types
az.plot_pair(idata, kind="kde") # Kernel density
az.plot_pair(idata, kind="hexbin") # Better for large samples
az.plot_pair(idata, kind="scatter") # Default
# With marginal distributions
az.plot_pair(idata, marginals=True)
# Reference values (e.g., true values in simulation study)
az.plot_pair(idata, reference_values={"alpha": 1.0, "beta": 0.5})
# Point estimate overlay
az.plot_pair(idata, point_estimate="mean")
What to look for:
- Strong correlations: May indicate non-identifiability or need for reparameterization
- Banana/funnel shapes: Common in hierarchical models, may need non-centered parameterization
- Divergences clustered: Indicates problematic posterior regions
- Multimodality: Multiple clusters suggest mixture or label switching
az.plot_parallel: High-Dimensional Relationships
az.plot_parallel(idata, var_names=["alpha", "beta", "gamma", "sigma"])
Divergent samples shown in different color—look for patterns indicating where the sampler struggles.
az.plot_violin: Distribution Comparison
az.plot_violin(idata, var_names=["alpha"])
Good for comparing distributions across groups when you want quartile information.
az.plot_ridge: Compact Multi-Parameter View
az.plot_ridge(idata, var_names=["alpha"])
Useful when you have many group-level parameters to display compactly.
Phase 5: Model Comparison
Compare models using out-of-sample predictive accuracy.
az.loo: Leave-One-Out Cross-Validation
# Compute LOO (requires log_likelihood in idata)
loo_result = az.loo(idata, pointwise=True)
print(loo_result)
Key outputs:
elpd_loo: Expected log pointwise predictive density (higher is better)se: Standard error of elpd_loop_loo: Effective number of parameters (model complexity)pareto_k: Diagnostic for approximation quality
Pareto k interpretation:
| k value | Interpretation | Action |
|---|---|---|
| < 0.5 | Good | LOO reliable |
| 0.5-0.7 | OK | Probably fine, be cautious |
| 0.7-1.0 | Bad | LOO unreliable for these points |
| > 1.0 | Very bad | Likely model misspecification |
# Check Pareto k values
loo = az.loo(idata, pointwise=True)
print(f"Good k (< 0.5): {(loo.pareto_k < 0.5).sum().item()}")
print(f"OK k (0.5-0.7): {((loo.pareto_k >= 0.5) & (loo.pareto_k < 0.7)).sum().item()}")
print(f"Bad k (> 0.7): {(loo.pareto_k > 0.7).sum().item()}")
az.plot_khat: Visualize Pareto k
az.plot_khat(idata)
Points above the 0.7 line are influential observations where LOO approximation is unreliable. Consider:
- Using moment matching (
az.loo(..., pointwise=True)then refit) - K-fold CV instead
- Investigating why these points are influential
az.waic: Widely Applicable Information Criterion
waic_result = az.waic(idata)
print(waic_result)
WAIC is an alternative to LOO. LOO is generally preferred (more robust), but WAIC is faster for large datasets.
az.compare: Model Comparison Table
comparison = az.compare({
"linear": idata_linear,
"quadratic": idata_quad,
"spline": idata_spline,
}, ic="loo")
print(comparison)
Key columns:
rank: Model ranking (0 is best)elpd_loo: Expected log pointwise predictive densityp_loo: Effective number of parametersd_loo: Difference from best modelweight: Stacking weight (for model averaging)se: Standard errordse: Standard error of difference
Decision rules:
- If
d_loo < 2: Models practically indistinguishable - If
d_loo < dse: Difference not significant - If
d_loo > 4andd_loo > 2*dse: Meaningful difference
az.plot_compare: Visual Model Comparison
az.plot_compare(comparison)
Shows ELPD differences with error bars. Overlapping bars suggest models perform similarly.
az.plot_elpd: Pointwise Comparison
az.plot_elpd({"linear": idata_linear, "quadratic": idata_quad})
Identifies which observations drive model differences—useful for understanding where models disagree.
Model Averaging with Stacking Weights
# Stacking weights from comparison
weights = comparison["weight"]
# Use weights for prediction averaging
y_pred_avg = (
weights["linear"] * idata_linear.posterior_predictive["y"].mean(dim=["chain", "draw"]) +
weights["quadratic"] * idata_quad.posterior_predictive["y"].mean(dim=["chain", "draw"])
)
Advanced Techniques
Working with xarray Directly
import xarray as xr
# Compute custom statistics
posterior = idata.posterior
# Probability of effect > 0
prob_positive = (posterior["beta"] > 0).mean(dim=["chain", "draw"])
# Probability of effect > threshold
threshold = 0.5
prob_gt_threshold = (posterior["beta"] > threshold).mean(dim=["chain", "draw"])
# Posterior contrasts
if "group" in posterior["alpha"].dims:
contrast = posterior["alpha"].sel(group="treatment") - posterior["alpha"].sel(group="control")
az.plot_posterior(contrast.to_dataset(name="treatment_effect"))
Custom Summary Functions
def prob_direction(x):
"""Probability parameter has same sign as mean"""
return (np.sign(x) == np.sign(x.mean())).mean()
def hdi_width(x):
"""Width of 94% HDI"""
hdi = az.hdi(x, hdi_prob=0.94)
return hdi[1] - hdi[0]
# Add to summary
summary = az.summary(
idata,
stat_funcs={"P(same sign)": prob_direction, "HDI width": hdi_width},
extend=True
)
Subsampling for Large InferenceData
# Thin samples (keep every nth)
idata_thin = idata.sel(draw=slice(None, None, 10)) # Keep every 10th
# Random subset
import numpy as np
n_draws = idata.posterior.dims["draw"]
keep_idx = np.random.choice(n_draws, size=500, replace=False)
idata_subset = idata.sel(draw=keep_idx)
Extracting Samples for Custom Analysis
# Get flattened samples for external tools
samples_dict = {
var: idata.posterior[var].stack(sample=("chain", "draw")).values
for var in ["alpha", "beta", "sigma"]
}
# Or use az.extract
samples = az.extract(idata, var_names=["alpha", "beta"])
Combining Results from Multiple Runs
# Concatenate chains from different runs
idata_combined = az.concat([idata1, idata2], dim="chain")
# Be careful: this assumes same model and convergence
Common Interpretation Patterns
Pattern: Multimodal Posterior
Symptoms:
- Bimodal density in
plot_trace - Separated clusters in
plot_pair - Very high R-hat
Diagnosis:
az.plot_pair(idata, var_names=["theta"], marginals=True)
az.plot_trace(idata, var_names=["theta"], compact=False)
Causes and fixes:
- Label switching in mixtures: Add ordering constraint
- Multiple solutions: May be valid—interpret carefully or use additional constraints
- Insufficient warmup: Increase
tune
Pattern: Funnel Geometry
Symptoms:
- Divergences clustered at low scale parameter values
- Funnel shape in pair plot of location vs scale
- Low ESS for scale parameters
Diagnosis:
az.plot_pair(idata, var_names=["mu_group", "sigma_group"], divergences=True)
Fix: Use non-centered parameterization (see gotchas.md)
Pattern: Poor Tail Sampling
Symptoms:
ess_tailmuch lower thaness_bulk- Credible intervals unreliable
- Autocorrelation high at extremes
Diagnosis:
az.plot_ess(idata, kind="quantile") # Check ESS at different quantiles
Fixes:
- Increase samples
- Increase
target_accept(e.g., 0.95) - Reparameterize
Pattern: Scale Parameter at Boundary
Symptoms:
- Scale parameter (σ) posterior piled up near zero
- May indicate overfitting/overparameterization
Diagnosis:
az.plot_posterior(idata, var_names=["sigma"])
Interpretation:
- If σ → 0: Group effects explain almost no variance—simplify model
- Check if prior is appropriate
Pattern: Strong Parameter Correlations
Symptoms:
- Nearly linear relationship in pair plot
- Can indicate non-identifiability
Diagnosis:
az.plot_pair(idata, var_names=["alpha", "beta"])
Fixes:
- Center predictors
- Use sum-to-zero constraints
- Consider if parameters are actually identifiable
Publication-Quality Figures
Style Configuration
import arviz as az
import matplotlib.pyplot as plt
# Set ArviZ style
az.style.use("arviz-darkgrid") # or "arviz-whitegrid", "arviz-white"
# Custom style
az.rcParams["plot.max_subplots"] = 40
az.rcParams["stats.hdi_prob"] = 0.94
az.rcParams["stats.ic_scale"] = "log" # for LOO/WAIC
Figure Sizing and Layout
# Control figure size
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
az.plot_posterior(idata, var_names=["beta"], ax=axes.flatten())
plt.tight_layout()
# Or let ArviZ handle it
axes = az.plot_posterior(idata, var_names=["beta"], figsize=(12, 4))
Combining Multiple Plots
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
fig = plt.figure(figsize=(14, 10))
gs = GridSpec(2, 2, figure=fig)
# Trace plot
ax1 = fig.add_subplot(gs[0, :])
az.plot_trace(idata, var_names=["beta"], compact=True, combined=True, ax=ax1)
# Posterior
ax2 = fig.add_subplot(gs[1, 0])
az.plot_posterior(idata, var_names=["beta"], ax=ax2)
# PPC
ax3 = fig.add_subplot(gs[1, 1])
az.plot_ppc(idata, kind="cumulative", ax=ax3)
plt.tight_layout()
Saving Figures
# Save with high resolution
fig = az.plot_posterior(idata, var_names=["beta"])
plt.savefig("posterior.png", dpi=300, bbox_inches="tight")
plt.savefig("posterior.pdf", bbox_inches="tight") # Vector format
LaTeX Labels
import matplotlib.pyplot as plt
plt.rcParams["text.usetex"] = True
# Use LaTeX in labels
az.plot_posterior(
idata,
var_names=["beta"],
labeller=az.labels.MapLabeller(var_name_map={"beta": r"$\beta$"})
)
Quick Reference: Which Plot When?
| Question | Plot | Function |
|---|---|---|
| Did MCMC converge? | Trace, Rank | plot_trace, plot_rank |
| Are there divergences? | Pair with divergences | plot_pair(..., divergences=True) |
| Is ESS adequate? | ESS evolution | plot_ess(kind="evolution") |
| Is mixing efficient? | Autocorrelation | plot_autocorr |
| Is HMC healthy? | Energy | plot_energy |
| Does model fit? | PPC | plot_ppc |
| Is model calibrated? | LOO-PIT | plot_loo_pit |
| What are the estimates? | Posterior | plot_posterior |
| Compare group effects? | Forest | plot_forest |
| Parameters correlated? | Pair | plot_pair |
| Which model is best? | Compare | plot_compare |
| Which points influential? | Pareto k | plot_khat |
| Prior sensible? | Prior predictive | plot_ppc(..., group="prior") |