Source code for arviz_plots.plots.loo_pava_plot

"""LOO PAV-adjusted calibration plot."""

from collections.abc import Mapping, Sequence
from typing import Any, Literal

import numpy as np
import xarray as xr
from arviz_base.validate import validate_dict_argument, validate_sample_dims
from arviz_stats.loo import loo
from scipy.special import logsumexp

from arviz_plots.plots.pava_calibration_plot import plot_ppc_pava
from arviz_plots.plots.utils import _var_names


[docs] def plot_loo_pava( dt, *, var_names=None, filter_vars=None, group="posterior_predictive", coords=None, # pylint: disable=unused-argument sample_dims=None, data_type="binary", ci_prob=None, plot_collection=None, backend=None, labeller=None, aes_by_visuals: Mapping[ Literal[ "lines", "markers", "reference_line", "credible_interval", "xlabel", "ylabel", "title", ], Sequence[str], ] = None, visuals: Mapping[ Literal[ "lines", "markers", "reference_line", "credible_interval", "xlabel", "ylabel", "title", ], Mapping[str, Any] | bool, ] = None, **pc_kwargs, ): """LOO PAV-adjusted calibration plot. Uses PSIS-LOO-CV to resample the posterior predictive distribution, then applies the pool adjacent violators (PAV) algorithm for isotonic regression. A 45-degree line corresponds to perfect calibration. Details on the PAV-adjusted calibration plot are discussed in [1]_ and [2]_, and PSIS-LOO-CV in [3]_ and [4]_. Parameters ---------- dt : DataTree Input data. It should contain the ``posterior``, ``posterior_predictive``, ``log_likelihood`` and ``observed_data`` groups. var_names : str or list of str, optional One or more variables to be plotted. Currently only one variable is supported. Prefix the variables by ~ when you want to exclude them from the plot. filter_vars : {None, "like", "regex"}, optional, default=None If None (default), interpret var_names as the real variables names. If "like", interpret var_names as substrings of the real variables names. If "regex", interpret var_names as regular expressions on the real variables names. group : str, optional Only "posterior_predictive" is supported. coords : dict, optional Coordinates to plot. CURRENTLY NOT IMPLEMENTED sample_dims : str or sequence of hashable, optional Dimensions to reduce unless mapped to an aesthetic. Defaults to ``rcParams["data.sample_dims"]`` data_type : str Defaults to "binary". Other options are "categorical" and "ordinal". If "categorical", the plot will show the "one-vs-others" calibration and generate one plot per category. If "ordinal", the plot will display cumulative conditional event probabilities and generate (number of categories - 1) plots. ci_prob : float, optional Probability for the credible interval. Defaults to ``rcParams["stats.ci_prob"]``. plot_collection : PlotCollection, optional backend : {"matplotlib", "bokeh", "plotly"}, optional labeller : labeller, optional aes_by_visuals : mapping of {str : sequence of str}, optional Mapping of visuals to aesthetics that should use their mapping in `plot_collection` when plotted. Valid keys are the same as for `visuals`. visuals : mapping of {str : mapping or bool}, optional Valid keys are: * lines -> passed to :func:`~arviz_plots.visuals.line_xy` * markers -> passed to :func:`~arviz_plots.visuals.scatter_xy` * reference_line -> passed to :func:`~arviz_plots.visuals.line_xy` * credible_interval -> passed to :func:`~arviz_plots.visuals.fill_between_y` * xlabel -> passed to :func:`~arviz_plots.visuals.labelled_x` * ylabel -> passed to :func:`~arviz_plots.visuals.labelled_y` * title -> passed to :func:`~arviz_plots.visuals.labelled_title` markers defaults to False, no markers are plotted. Pass an (empty) mapping to plot markers. **pc_kwargs Passed to :class:`arviz_plots.PlotCollection.grid` Returns ------- PlotCollection See Also -------- plot_ppc_pava : PAV-adjusted calibration plot using posterior predictive. Examples -------- Plot the LOO PAVA calibration plot for the anes dataset. .. plot:: :context: close-figs >>> from arviz_plots import plot_loo_pava, style >>> style.use("arviz-variat") >>> from arviz_base import load_arviz_data >>> dt = load_arviz_data('anes') >>> plot_loo_pava(dt, ci_prob=0.90) .. minigallery:: plot_loo_pava References ---------- .. [1] Säilynoja et al. *Recommendations for visual predictive checks in Bayesian workflow*. (2025) arXiv preprint https://arxiv.org/abs/2503.01509 .. [2] Dimitriadis et al *Stable reliability diagrams for probabilistic classifiers*. PNAS, 118(8) (2021). https://doi.org/10.1073/pnas.2016191118 .. [3] Vehtari et al. *Practical Bayesian model evaluation using leave-one-out cross-validation and WAIC*. Statistics and Computing. 27(5) (2017) https://doi.org/10.1007/s11222-016-9696-4 .. [4] Vehtari et al. *Pareto Smoothed Importance Sampling*. Journal of Machine Learning Research, 25(72) (2024) https://jmlr.org/papers/v25/19-556.html """ if group != "posterior_predictive": raise ValueError(f"Group {group} not supported. Only 'posterior_predictive' is supported.") sample_dims = validate_sample_dims(sample_dims, data=dt.posterior_predictive) aes_by_visuals = validate_dict_argument(aes_by_visuals, (plot_loo_pava, "aes_by_visuals")) visuals = validate_dict_argument(visuals, (plot_loo_pava, "visuals")) var_names = _var_names(var_names, dt.observed_data, filter_vars) if var_names is None: var_names = list(dt.observed_data.data_vars) new_dt = _loo_resample(dt, sample_dims, var_names) return plot_ppc_pava( new_dt, var_names=var_names, filter_vars=filter_vars, group="posterior_predictive", coords=coords, sample_dims=None, data_type=data_type, ci_prob=ci_prob, plot_collection=plot_collection, backend=backend, labeller=labeller, aes_by_visuals=aes_by_visuals, visuals=visuals, **pc_kwargs, )
def _loo_resample(dt, sample_dims, resolved_var_names): """Resample the posterior predictive using PSIS-LOO weights.""" resampled_pp = {} rng = np.random.default_rng(31247) for var_name in resolved_var_names: log_weights = loo(dt, pointwise=True, var_name=var_name).log_weights pp = dt.posterior_predictive[var_name] obs_dims = [d for d in pp.dims if d not in sample_dims] pp_flat = pp.stack(sample=sample_dims).transpose("sample", *obs_dims) lw_flat = log_weights.stack(sample=sample_dims).transpose("sample", *obs_dims) lw_vals = lw_flat.values n_draws, n_obs = pp_flat.shape probs = np.exp(lw_vals - logsumexp(lw_vals, axis=0)) resamples = np.empty((n_draws, n_obs), dtype=pp.dtype) for j in range(n_obs): resamples[:, j] = rng.choice(pp_flat[:, j].values, size=n_draws, p=probs[:, j]) resampled_da = xr.DataArray( resamples[None, :, :], dims=["chain", "draw", *obs_dims], coords={ "chain": np.arange(1), "draw": np.arange(n_draws), **{d: pp.coords[d] for d in obs_dims}, }, name=var_name, ) resampled_pp[var_name] = resampled_da pp_ds = xr.Dataset(resampled_pp) obs_ds = xr.Dataset({v: dt.observed_data[v] for v in resolved_var_names}) new_dt = xr.DataTree.from_dict( { "posterior_predictive": pp_ds, "observed_data": obs_ds, } ) return new_dt