Source code for arviz_stats.loo.lfo_cv

"""Leave-Future-Out Cross-Validation (LFO-CV) for time series models."""

import warnings

import numpy as np
from arviz_base import rcParams

from arviz_stats.loo.lfo_cv_helper import (
    _compute_lfo_approx,
    _compute_lfo_exact,
    _prepare_lfo_inputs,
)
from arviz_stats.utils import ELPDDataLFO

__all__ = ["lfo_cv"]


[docs] def lfo_cv( data, wrapper, min_observations, forecast_horizon, time_dim="time", pointwise=None, var_name=None, method="approx", k_threshold=0.7, ): """Compute leave-future-out cross-validation (LFO-CV) for time series models. Estimates the expected log predictive density (elpd) of M-step-ahead predictions for time series models, where M is set by ``forecast_horizon``. The model is trained on the observations up to each forecast origin and scored on the joint log predictive density of the next M observations, so predictions are never conditioned on data from the future. By default, the posterior is carried between forecast origins with Pareto-smoothed importance sampling (PSIS) and the model is only refit when the approximation becomes unreliable (see ``method``). The PSIS-LFO-CV method is described in [1]_ and builds on the PSIS-LOO-CV method described in [2]_ and [3]_. Parameters ---------- data : DataTree or InferenceData Input data containing the posterior and log_likelihood groups from the full model fit. Must have a time dimension. Will be converted to DataTree. wrapper : SamplingWrapper An instance of :class:`~arviz_stats.SamplingWrapper` (or subclass) handling model refitting. Must implement ``sel_observations``, ``sample``, ``get_inference_data``, and ``log_likelihood__i``. Following the same pattern as :func:`loo_kfold`, ``sel_observations`` receives an array of integer indices to exclude from training and returns ``(modified_observed_data, excluded_observed_data)``. The forecast window is selected from ``excluded_observed_data`` internally. Each refit excludes all observations from the forecast origin through the end. The approximate method also uses later excluded values to compute subsequent importance ratios. min_observations : int Minimum number of observations required before making predictions. The first prediction is made at time min_observations. forecast_horizon : int Forecast horizon: number of steps ahead to predict. time_dim : str, default="time" Name of the time dimension in the data. pointwise : bool, optional If True, return pointwise estimates. Defaults to ``rcParams["stats.ic_pointwise"]``. var_name : str, optional The name of the variable in log_likelihood group storing the pointwise log likelihood data to use for computation. method : str, default="approx" Whether to refit the model at every forecast origin ("exact") or to carry the posterior forward with Pareto-smoothed importance sampling (PSIS) [3]_, refitting only when the importance weights become unreliable ("approx"). k_threshold : float, default=0.7 Pareto k threshold for triggering refit. If k > k_threshold, refit the model. Recommended values: 0.6 (conservative) or 0.7 (default). Returns ------- ELPDDataLFO Object with the following attributes: - **kind**: "lfo_cv" - **elpd**: expected log pointwise predictive density - **se**: standard error of the elpd - **p**: effective number of parameters - **n_samples**: number of posterior samples - **n_data_points**: number of forecast origins evaluated - **scale**: "log" - **warning**: bool - True if more than half of the forecast origins required a refit (only if ``method="approx"``, otherwise False) - **good_k**: float - The k-threshold value used for refit decisions (only if ``method="approx"``, otherwise None) - **elpd_i**: :class:`~xarray.DataArray` with pointwise predictive accuracy, only if ``pointwise=True`` - **pareto_k**: :class:`~xarray.DataArray` with Pareto k diagnostics, only if ``method="approx"`` and ``pointwise=True`` - **p_lfo_i**: :class:`~xarray.DataArray` with pointwise effective number of parameters, only if ``pointwise=True`` - **forecast_horizon**: forecast horizon - **min_observations**: minimum observations - **refits**: array of time indices where refits occurred. For ``method="approx"`` the initial fit at the first forecast origin is not counted as a refit - **n_refits**: number of refits performed Notes ----- When ``forecast_horizon`` is greater than 1, each forecast origin is scored with the joint log predictive density of the next ``forecast_horizon`` observations. If the likelihood depends on lagged values of the response, as in autoregressive models, there is more than one way to evaluate this joint density. ``lfo_cv`` uses the chain rule factorization, where each observation in the block is scored conditional on the observed values of the observations before it. This measures how well the model predicts the block that actually occurred given the training data. The alternative is to score each observation in the block against simulated values of its predecessors drawn from the model, letting predictive uncertainty accumulate across the horizon. This measures how well the model forecasts the block when the intermediate observations are never revealed, and is common in software oriented toward forecast evaluation. The two approaches coincide when ``forecast_horizon=1`` and whenever the likelihood does not depend on lagged values of the response. Otherwise they estimate different quantities, so results computed one way should not be compared with results computed the other way. See Also -------- :func:`loo` : Pareto-smoothed importance sampling LOO-CV. :func:`compare` : Compare models based on their ELPD. References ---------- .. [1] Bürkner et al. *Approximate leave-future-out cross-validation for Bayesian time series models*. Journal of Statistical Computation and Simulation. 90(14) (2020) 2499-2523. https://doi.org/10.1080/00949655.2020.1783262 arXiv preprint https://arxiv.org/abs/1902.06281 .. [2] 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 arXiv preprint https://arxiv.org/abs/1507.04544. .. [3] Vehtari et al. *Pareto Smoothed Importance Sampling*. Journal of Machine Learning Research, 25(72) (2024) https://jmlr.org/papers/v25/19-556.html arXiv preprint https://arxiv.org/abs/1507.02646 """ pointwise = rcParams["stats.ic_pointwise"] if pointwise is None else pointwise method = method.lower() if method not in ("exact", "approx"): raise ValueError( f"method must be 'exact' or 'approx', got '{method}'. " "Use 'exact' for always refitting or 'approx' for PSIS approximation." ) lfo_inputs = _prepare_lfo_inputs( data, var_name, wrapper, min_observations, forecast_horizon, time_dim ) if method == "exact": lfo_results = _compute_lfo_exact(lfo_inputs, wrapper) else: lfo_results = _compute_lfo_approx(lfo_inputs, wrapper, k_threshold) warning = False good_k = k_threshold if method == "approx" else None n_refits = len(lfo_results.refits) if method == "approx": if n_refits > lfo_results.n_data_points / 2: warnings.warn( f"LFO-CV triggered {n_refits} refits out of {lfo_results.n_data_points} forecast " "origins. The importance sampling approximation may be unreliable. " "Consider method='exact'.", UserWarning, ) warning = True return ELPDDataLFO( elpd=lfo_results.elpd, se=lfo_results.se, p=lfo_results.p, n_samples=lfo_inputs.n_samples, n_data_points=lfo_results.n_data_points, scale="log", warning=warning, good_k=good_k, elpd_i=lfo_results.elpd_i if pointwise else None, pareto_k=lfo_results.pareto_k if (pointwise and method == "approx") else None, forecast_horizon=forecast_horizon, min_observations=min_observations, refits=np.asarray(lfo_results.refits), n_refits=n_refits, p_lfo_i=lfo_results.p_lfo_i if pointwise else None, )