| Title: | Bayesian Error Propagation and Forecast Uncertainty Decomposition |
|---|---|
| Description: | Provides a full pipeline from regularized or standard regression models (elastic net, linear models, generalized linear models, random forests) to informed Bayesian priors, structured forecast uncertainty decomposition (parameter / environmental / residual, plus a temporal component when the model carries an autocorrelation term), and forecast shelf life analysis (the quantification of when a forecast becomes uninformative). Designed for ecological and genomic forecasting with climate or environmental covariates. Methods build on Bürkner (2017) <doi:10.18637/jss.v080.i01> for Bayesian regression via 'Stan', Friedman, Hastie, and Tibshirani (2010) <doi:10.18637/jss.v033.i01> for elastic net regularization, Wright and Ziegler (2017) <doi:10.18637/jss.v077.i01> for random forests, and Vehtari, Gelman, and Gabry (2017) <doi:10.1007/s11222-016-9696-4> for leave-one-out cross-validation. |
| Authors: | Luis Javier Madrigal-Roca [aut, cre], John Kelly [aut] |
| Maintainer: | Luis Javier Madrigal-Roca <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 1.4.1 |
| Built: | 2026-10-28 23:28:19 UTC |
| Source: | https://github.com/cran/ErrorTracer |
Returns a data.frame with the uncertainty decomposition stored
inside an et_prediction object:
Variance of the posterior linear predictor — captures uncertainty in fitted regression coefficients.
Variance arising from measurement or prediction
uncertainty in the predictor values (estimated via perturbation in
et_predict). A genuine sub-share of total_var
(see below), not an add-on. Zero when env_noise = NULL.
Posterior mean of (or its
family-specific analogue) — biological process noise, unmeasured
drivers, and drift. For autocorrelation models this is the
innovation variance, not the stationary marginal variance;
the autocorrelated accumulation is reported separately in
temporal_var. For a non-identity link it is evaluated at the
perturbed mean, , because that is
the mean the predictive draws are generated at; it therefore depends on
env_noise through the variance function. For Gaussian,
is independent of the mean and the distinction
does not arise.
(Only present when the model formula contains an
autocorrelation term such as ar(), ma(),
arma(), cosy(), unstr(), sar(), or
car().) Variance attributable to residual temporal or spatial
dependence beyond the named channels, computed as the signed remainder
total_var - (param_var + env_var + residual_var). This captures
the AR/MA/ARMA innovation accumulation that a single-innovation
residual_var cannot represent. It is signed: a
negative value means autocorrelated errors reduce predictive
variance relative to the independent-innovation baseline, which is a
real possibility for a covariance term. Being a remainder, it also
absorbs Monte Carlo error from the other channels, so read a
temporal_var of the same order as v_env_mcse as noise.
(Only present for iid models.)
total_var - (param_var + env_var + residual_var): how far the
named channels fall short of the measured posterior predictive variance.
It is Monte Carlo error for a correctly specified iid model, of the order
of v_env_mcse. A systematically large value means a channel is
mis-estimated.
Total predictive variance: the sampled variance
of the environmentally inclusive posterior predictive draws, which is
the distribution the reported credible interval is drawn from. The
driver perturbation is applied before the predictive draw, so
env_var is a genuine share of this total.
decompose_uncertainty(predictions, ...)decompose_uncertainty(predictions, ...)
predictions |
An |
... |
Unused. |
Changed in 1.4.0. total_var used to be defined as the sum of
the other components, which made the shares reconcile to 100% by
construction and made env_var an add-on to a predictive variance that
did not contain it. It is now measured, and the sum is recovered as a
result: the channels add up to total_var exactly for
autocorrelation models (where temporal_var is the remainder) and up to
Monte Carlo error for iid models (reported as closure_gap).
param_var, env_var and residual_var remain
non-negative; temporal_var and closure_gap are signed.
A data.frame with columns
obs_id, param_var, env_var, residual_var, total_var
(plus temporal_var for autocorrelation models or
closure_gap for iid ones, and a leading group column for
grouped predictions).
set.seed(1) df <- data.frame(y = rnorm(20), x1 = rnorm(20)) fit <- et_fit(y ~ x1, data = df, chains = 1, iter = 500, warmup = 250, cores = 1, refresh = 0) new_df <- data.frame(x1 = rnorm(5)) pred <- et_predict(fit, newdata = new_df, env_noise = list(x1 = 0.2), n_draws = 200, n_perturb = 50) decomp <- decompose_uncertainty(pred) head(decomp)set.seed(1) df <- data.frame(y = rnorm(20), x1 = rnorm(20)) fit <- et_fit(y ~ x1, data = df, chains = 1, iter = 500, warmup = 250, cores = 1, refresh = 0) new_df <- data.frame(x1 = rnorm(5)) pred <- et_predict(fit, newdata = new_df, env_noise = list(x1 = 0.2), n_draws = 200, n_perturb = 50) decomp <- decompose_uncertainty(pred) head(decomp)
Computes observed coverage probability at multiple nominal CI levels. A well-calibrated Bayesian model should produce 90% CIs that contain the true value 90% of the time, etc.
et_calibrate(predictions, observed, response_col = NULL, ci_levels = NULL, ...)et_calibrate(predictions, observed, response_col = NULL, ci_levels = NULL, ...)
predictions |
An |
observed |
A |
response_col |
Character. Name of the response column in
|
ci_levels |
Numeric vector. CI levels to assess. Defaults to all
levels present in the |
... |
Unused. |
A data.frame with columns:
Nominal CI level.
Same as ci_level.
Fraction of true values falling inside the CI.
Number of observations used.
Signed difference: observed - nominal. Positive = over-coverage (CIs too wide / conservative). Negative = under-coverage (CIs too narrow / overconfident).
Mean CI width across observations. Sharpness and calibration are complementary: a model can be calibrated but useless if sharpness is poor (very wide CIs).
For grouped predictions, a group column is prepended.
set.seed(1) df <- data.frame(y = rnorm(20), x1 = rnorm(20)) fit <- et_fit(y ~ x1, data = df, chains = 1, iter = 500, warmup = 250, cores = 1, refresh = 0) valid_df <- data.frame(y = rnorm(5), x1 = rnorm(5)) pred <- et_predict(fit, newdata = valid_df, n_draws = 200, n_perturb = 50) cal <- et_calibrate(pred, observed = valid_df, response_col = "y") print(cal)set.seed(1) df <- data.frame(y = rnorm(20), x1 = rnorm(20)) fit <- et_fit(y ~ x1, data = df, chains = 1, iter = 500, warmup = 250, cores = 1, refresh = 0) valid_df <- data.frame(y = rnorm(5), x1 = rnorm(5)) pred <- et_predict(fit, newdata = valid_df, n_draws = 200, n_perturb = 50) cal <- et_calibrate(pred, observed = valid_df, response_col = "y") print(cal)
Computes Rhat, effective sample size ratios, divergent transitions, and
leave-one-out cross-validation (LOO-CV) for a fitted et_model.
et_diagnose(model, loo = TRUE, ...)et_diagnose(model, loo = TRUE, ...)
model |
An |
loo |
Logical. Whether to run LOO-CV (can be slow; default
|
... |
Unused. |
A list with elements:
List: rhat_max, rhat_all_ok,
neff_min, neff_all_ok, n_divergences.
List or NULL: elpd_loo, p_loo,
looic, n_bad_pareto_k, loo_object.
Printed summary from brms::summary().
For et_model_list, a named list of per-group diagnostic lists
plus an aggregated summary data.frame.
Wraps brms::brm() and attaches the prior specification,
training data reference, and configuration for downstream uncertainty
decomposition. Pass priors from extract_priors to
use regularized-model coefficients to set the prior scale (and, with
shrinkage = "estimate", the prior mean); omit it for default
(weakly informative) priors.
et_fit( formula, data, priors = NULL, chains = 4L, iter = 2000L, warmup = floor(iter/2), cores = min(chains, parallel::detectCores()), seed = 42L, adapt_delta = 0.95, max_treedepth = 12L, grouping = NULL, eiv = NULL, ar_prior = c("weakly_informative", "stationary", "flat"), silent = 2L, ... )et_fit( formula, data, priors = NULL, chains = 4L, iter = 2000L, warmup = floor(iter/2), cores = min(chains, parallel::detectCores()), seed = 42L, adapt_delta = 0.95, max_treedepth = 12L, grouping = NULL, eiv = NULL, ar_prior = c("weakly_informative", "stationary", "flat"), silent = 2L, ... )
formula |
An R formula, e.g.\ |
data |
A |
priors |
An |
chains |
Integer. Number of MCMC chains (default 4). |
iter |
Integer. Total iterations per chain, including warmup (default 2000). |
warmup |
Integer. Warmup iterations per chain (default
|
cores |
Integer. Parallel cores (default
|
seed |
Integer. Random seed for reproducibility (default 42). |
adapt_delta |
Numeric. Target acceptance probability for HMC (default 0.95). |
max_treedepth |
Integer. Maximum tree depth (default 12). |
grouping |
Character. Name of a column in |
eiv |
Optional errors-in-variables specification. A named list /
vector mapping predictor names to either a scalar SD or a vector of
per-row SDs (length |
ar_prior |
Character. Prior on autocorrelation parameters
(
A prior the caller supplies for these classes is never overridden.
Regardless of the setting, |
silent |
Integer passed to |
... |
Additional arguments passed to |
An et_model object (or an et_model_list if
grouping is specified).
et_fit() is deliberately a thin wrapper around brms:
the formula, family, and any autocorrelation term (ar()/ma()/
arma()/...) are passed straight through, so ErrorTracer inherits
brms's modelling scope and its Stan back-end. The added value is the
prior pipeline, the structured uncertainty decomposition, forecast skill /
shelf life, and calibration diagnostics — not new model classes.
Two boundaries to keep in mind:
Hierarchical / random-effects models fit through the
formula (y ~ x + (x | group)) and are decomposed correctly, with
the in-sample / out-of-sample distinction handled explicitly. Predicting
for a group the fit has seen uses that group's estimated effect:
the effect is a parameter, its posterior uncertainty is part of
param_var, and there is no separate group channel (conditioning on
a known group reduces predictive variance relative to the
population level). Predicting for a new level integrates over the
group-level distribution and adds a group_var channel carrying the
between-group variance; et_predict detects new levels
automatically. The grouping argument here is a separate
device: it fits one independent model per group (no pooling), not a
single multilevel model.
Convergence is your responsibility. et_fit() emits a
warning on high Rhat or divergent transitions, but you should inspect
et_diagnose before predicting; the defaults
(iter = 2000, 4 chains) are a starting point, not a guarantee.
set.seed(1) df <- data.frame(y = rnorm(20), x1 = rnorm(20), x2 = rnorm(20)) ps <- extract_priors(lm(y ~ x1 + x2, data = df)) fit <- et_fit(y ~ x1 + x2, data = df, priors = ps, chains = 1, iter = 500, warmup = 250, cores = 1, refresh = 0) print(fit)set.seed(1) df <- data.frame(y = rnorm(20), x1 = rnorm(20), x2 = rnorm(20)) ps <- extract_priors(lm(y ~ x1 + x2, data = df)) fit <- et_fit(y ~ x1 + x2, data = df, priors = ps, chains = 1, iter = 500, warmup = 250, cores = 1, refresh = 0) print(fit)
Computes the probability integral transform of held-out observations under
the (reported) posterior predictive distribution — the same distribution
the credible intervals and et_calibrate coverage are built
from, including environmental uncertainty when it was folded in. For a
well-calibrated model the PIT values are .
Systematic departures diagnose how the model is miscalibrated, which
interval coverage alone cannot: a U-shape means the predictive is
too narrow (overconfident), a central hump means it is too wide
(underconfident), and a slope / shift means it is biased. Visualise
with et_plot_pit (a PIT / rank histogram).
et_pit(predictions, observed, response_col = NULL, randomize = TRUE, ...)et_pit(predictions, observed, response_col = NULL, randomize = TRUE, ...)
predictions |
An |
observed |
A |
response_col |
Character. Response column name; inferred from the model
formula when |
randomize |
Logical. Use the randomized PIT (default
|
... |
Unused. |
A data.frame with columns obs_id and pit (plus a
leading group column for grouped predictions).
A well-calibrated model produces points along the 1:1 diagonal. Points above the diagonal indicate over-coverage (conservative); below indicates under-coverage (anti-conservative).
et_plot_calibration(cal, group_col = NULL)et_plot_calibration(cal, group_col = NULL)
cal |
A |
group_col |
Optional character. Name of a column in |
A ggplot2 object.
Compares Bayesian posterior estimates (95% CI) with the regularized coefficient values used as prior means (shown as crosses).
et_plot_coefficients(model)et_plot_coefficients(model)
model |
An |
A ggplot2 object.
Produces a stacked bar chart showing the relative contributions of
parameter, environmental, and residual variance for each observation,
plus a fourth temporal-autocorrelation component when present in
decomp (see decompose_uncertainty).
et_plot_decomposition(decomp, proportional = TRUE, group_col = NULL)et_plot_decomposition(decomp, proportional = TRUE, group_col = NULL)
decomp |
A |
proportional |
Logical. If |
group_col |
Character. Optional name of a grouping column in
|
A ggplot2 object.
Overlays nested credible interval ribbons on the median forecast, with optional observed values for calibration assessment.
et_plot_forecast( predictions, observed = NULL, response_col = NULL, time_col = NULL )et_plot_forecast( predictions, observed = NULL, response_col = NULL, time_col = NULL )
predictions |
An |
observed |
Optional |
response_col |
Character. Name of the response column in
|
time_col |
Character. Column in |
A ggplot2 object.
Histogram of the probability-integral-transform values from
et_pit. Bars level with the dashed uniform reference line
indicate calibration; a U-shape means overconfident (too-narrow)
intervals, a central hump means underconfident (too-wide), and a
tilt means bias. The shaded band is an approximate 95% consistency
region for the bar heights under uniformity.
et_plot_pit(pit, bins = 10L)et_plot_pit(pit, bins = 10L)
pit |
A |
bins |
Integer. Number of histogram bins (default 10). |
A ggplot object.
Overlays prior and posterior density for each predictor coefficient, visualising how much the data update the priors.
et_plot_prior_posterior(model, max_preds = 8L, n_prior_draws = 4000L)et_plot_prior_posterior(model, max_preds = 8L, n_prior_draws = 4000L)
model |
An |
max_preds |
Integer. Maximum number of predictors to show (default 8). Predictors are shown in the order they appear in the prior specification. |
n_prior_draws |
Integer. Number of random draws for the prior density (default 4000). |
A ggplot2 object.
Visualises the output of et_sensitivity_profile: for each
noise grid point, shows how the environmental share of total
variance and the forecast horizon respond. Observed horizons
are drawn as solid points; projected horizons are hollow points;
lower-bound rows are drawn as upward arrows at the last informative time.
et_plot_sensitivity(sens, show = c("horizon", "env_share", "ratio"))et_plot_sensitivity(sens, show = c("horizon", "env_share", "ratio"))
sens |
An |
show |
|
The x-axis is the noise fraction (when the grid was built from
fraction_grid) or the grid step label otherwise. When both a
numeric fraction and a descriptive label exist, the fraction is preferred
for continuous x-positioning.
A ggplot2 object.
Shows how credible interval width grows over the forecast horizon and marks the threshold beyond which the forecast is uninformative.
et_plot_shelf_life(sl, show_ratio = TRUE, max_ratio = NULL)et_plot_shelf_life(sl, show_ratio = TRUE, max_ratio = NULL)
sl |
An |
show_ratio |
Logical. If |
max_ratio |
Numeric. Optional upper limit for the y axis, applied as a
zoom ( |
A ggplot2 object.
Generates posterior predictive draws for new observations, propagates
environmental measurement uncertainty through the model, and computes
credible intervals. The resulting et_prediction object is the
input to decompose_uncertainty, shelf_life,
and the plotting functions.
et_predict( model, newdata, env_noise = NULL, env_cov = NULL, env_dist = NULL, env_ensemble = NULL, n_draws = 2000L, ci_levels = c(0.5, 0.8, 0.9, 0.95), n_perturb = NULL, n_env_draws = 1L, interval_type = c("predictive", "linpred"), include_env_in_ci = NULL, var_trim = 0, seed = NULL, ... )et_predict( model, newdata, env_noise = NULL, env_cov = NULL, env_dist = NULL, env_ensemble = NULL, n_draws = 2000L, ci_levels = c(0.5, 0.8, 0.9, 0.95), n_perturb = NULL, n_env_draws = 1L, interval_type = c("predictive", "linpred"), include_env_in_ci = NULL, var_trim = 0, seed = NULL, ... )
model |
An |
newdata |
A |
env_noise |
Environmental measurement / prediction uncertainty. Can be:
|
env_cov |
Correlation structure of the environmental noise. The
magnitudes of the noise come from
A correlation derived from training data is a working assumption: the structure of the errors is assumed to mirror the structure of the values. When this is implausible, pass a matrix directly. |
env_dist |
Distributional form of the per-predictor noise. The
Distributions:
Correlation ( |
env_ensemble |
Optional pathwise driver ensemble for forecasting under
uncertain future covariates. A named list, one entry per uncertain
driver predictor, where each entry is an |
n_draws |
Integer. Number of posterior draws to use (default 2000; capped at the number of draws available in the fit). |
ci_levels |
Numeric vector. Credible interval levels to compute
(default |
n_perturb |
Integer. Number of posterior draws used for the
environmental perturbation step (default |
n_env_draws |
Integer. Number of independent environmental
perturbations averaged per posterior draw when estimating
|
interval_type |
Character. Which draws to use when computing credible intervals:
The decomposition components and |
include_env_in_ci |
Logical or |
var_trim |
Numeric in Winsorizing is not the right remedy for a near-unit-root
autocorrelation posterior: it masks a diverging forecast variance rather
than fixing it, and leaves the budget describing a different distribution
from the interval. Prefer a stationarity-respecting prior at fit time
( |
seed |
Integer or |
... |
Passed to methods. |
An et_prediction object (list) containing:
posterior_predictMatrix [n_draws x n_obs]: full
posterior predictive draws (parameter + residual uncertainty).
posterior_linpredMatrix [n_draws x n_obs]: linear
predictor draws on the link scale (parameter uncertainty only).
lp_perturbedMatrix [n_perturb x n_obs]: linear
predictor on the link scale computed on environmentally perturbed inputs.
sigma_drawsNumeric vector: posterior draws of sigma
(NA for families without a sigma parameter, e.g.\ Binomial).
credible_intervalsdata.frame with columns
row_id, ci_level, lower, median, upper, width.
decompositiondata.frame with columns
obs_id, total_var, param_var, env_var, v_env_mcse, residual_var,
plus temporal_var for autocorrelation models or
closure_gap for iid ones.
All components are on the response scale.
total_var is the measured variance of the env-inclusive
predictive draws, not the sum of the channels; see
decompose_uncertainty.
v_env_mcse is the Monte Carlo SE of env_var.
residual_var is per-observation for non-Gaussian families
(e.g.\ Binomial: ) and constant
for Gaussian.
newdataThe input newdata.
modelReference to the et_model used.
env_covThe correlation matrix actually
used for perturbation (identity for env_cov = NULL).
env_distNamed character vector mapping each predictor to the distribution actually used for its perturbation.
decompose_uncertainty, shelf_life,
et_calibrate
The standard workflow fits a regularized model and the Bayesian model on the
same rows. Even with shrinkage = "zero", which never reuses a
point estimate as a prior mean, two things still cross over: the prior
scale is set from those rows, and — more consequentially — when the
first-stage model performs variable selection, the surviving predictor
set is chosen using the same data that then forms the likelihood. Reported
coverage is therefore conditional on that selection being correct, which is
easy to satisfy in a simulation with orthogonal nuisance predictors and much
less so in the correlated designs typical of real covariates.
et_priors_split(data, prior_fun, prop = 0.5, seed = NULL, strata = NULL, ...)et_priors_split(data, prior_fun, prop = 0.5, seed = NULL, strata = NULL, ...)
data |
A |
prior_fun |
A function taking a |
prop |
Proportion of rows used for the prior stage (default
|
seed |
Optional integer seed for the split, for reproducibility. |
strata |
Optional column name in |
... |
Passed to |
et_priors_split() removes both by construction: the first-stage model
is fitted on a random subset, and the rows returned for the Bayesian fit are
its complement. Selection and likelihood then share no observations, so the
posterior needs no post-selection caveat.
A list with:
The et_priors object from the prior-stage rows.
Rows to pass to et_fit — disjoint from
the rows used to build the priors.
Rows used for the first stage, returned so the split is auditable rather than hidden.
The realised split.
Splitting costs sample size. With prop = 0.5 the likelihood sees half
the data, so the posterior is genuinely wider — that width is honest rather
than pessimistic, but at small the loss can dominate whatever the
prior contributes. Splitting is worth it when selection stability is in
doubt (many correlated candidate predictors) and not worth it when the
predictor set is fixed a priori, where there is no selection to protect
against and the full sample should go to the likelihood.
extract_priors for the single-sample workflow.
set.seed(1) d <- data.frame(x1 = rnorm(80), x2 = rnorm(80)) d$y <- 1 + 2 * d$x1 + rnorm(80) sp <- et_priors_split(d, function(df) lm(y ~ x1 + x2, data = df), seed = 1) fit <- et_fit(y ~ x1 + x2, data = sp$fit_data, priors = sp$priors, chains = 1, iter = 500, refresh = 0)set.seed(1) d <- data.frame(x1 = rnorm(80), x2 = rnorm(80)) d$y <- 1 + 2 * d$x1 + rnorm(80) sp <- et_priors_split(d, function(df) lm(y ~ x1 + x2, data = df), seed = 1) fit <- et_fit(y ~ x1 + x2, data = sp$fit_data, priors = sp$priors, chains = 1, iter = 500, refresh = 0)
Sweeps a grid of noise magnitudes, re-runs et_predict for
each, and summarises how the environmental variance component and the
forecast shelf life respond. This turns the subjective choice of
env_noise into an auditable curve: instead of reporting a
single shelf life at one (often hand-picked) measurement-error level, the
user sees how the horizon degrades as assumed noise grows.
et_sensitivity_profile( model, newdata, response_scale, fraction_grid = NULL, absolute_grid = NULL, env_cov = NULL, env_dist = NULL, include_env_in_ci = TRUE, ci_level = 0.9, ci_levels = c(0.5, 0.8, 0.9, 0.95), threshold = 1, time_col = NULL, n_draws = 2000L, n_perturb = NULL, max_extrapolation_factor = 10, verbose = TRUE, plausible_range = NULL )et_sensitivity_profile( model, newdata, response_scale, fraction_grid = NULL, absolute_grid = NULL, env_cov = NULL, env_dist = NULL, include_env_in_ci = TRUE, ci_level = 0.9, ci_levels = c(0.5, 0.8, 0.9, 0.95), threshold = 1, time_col = NULL, n_draws = 2000L, n_perturb = NULL, max_extrapolation_factor = 10, verbose = TRUE, plausible_range = NULL )
model |
An |
newdata |
|
response_scale |
Two-element numeric vector |
fraction_grid |
Optional numeric vector of scalar noise fractions. |
absolute_grid |
Optional |
env_cov, env_dist
|
Passed through to |
include_env_in_ci |
Logical. If |
ci_level |
Numeric. Used for shelf life and CI width tracking
(default 0.90). Must be in |
ci_levels |
Numeric vector of CI levels computed per iteration
(default |
threshold |
Numeric. Shelf-life threshold (default 1.0). |
time_col |
Character. Optional time column for shelf life. |
n_draws, n_perturb
|
Passed to |
max_extrapolation_factor |
Passed to |
verbose |
Logical. If |
plausible_range |
Deprecated. Use |
Three argument styles are supported for the grid:
fraction_grid: scalar fractions of each predictor's SD in
newdata. These are passed straight to the scalar form of
et_predict's env_noise. Use this for an apples-
to-apples sweep that scales all predictors together.
absolute_grid: a list of named numeric lists /
vectors; each list element becomes a single env_noise argument
(so you can sweep absolute SDs for one predictor while holding others
fixed).
Neither supplied (NULL): a default log-spaced fraction grid
c(0, 0.01, 0.025, 0.05, 0.1, 0.2, 0.4, 0.8) is used.
Each iteration calls et_predict(..., env_noise = g) then
shelf_life and records:
Mean parameter / environmental / residual / total variance across forecast observations.
The horizon description ("observed", "projected", or
"lower_bound") and numeric horizon if any.
Mean / max CI width / plausible-range ratio.
An et_sensitivity object: a data.frame with one row
per grid point and columns
grid_id1-indexed step.
labelShort descriptor of the env_noise used
(e.g.\ "fraction = 0.05").
fractionScalar fraction when applicable; NA
for absolute-grid rows.
param_var, env_var, residual_var, total_varMean across forecast observations.
env_shareenv_var / total_var — the fraction
of total predictive variance attributable to predictor noise.
total_var is the measured variance of the env-inclusive
posterior predictive draws, so env_var is a genuine sub-share
of it and this sits in up to Monte Carlo error.
ci_width_mean, ci_width_maxAt ci_level.
ratio_mean, ratio_maxCI width / plausible range.
horizon_typeOne of "observed", "projected",
"lower_bound".
horizonNumeric horizon (observed or projected) or
NA for lower-bound rows.
horizon_descriptionThe long description returned by
shelf_life.
The returned object carries the original call and response_scale
as attributes for use by et_plot_sensitivity.
et_predict, shelf_life,
et_plot_sensitivity
A shelf life read off a single forecast origin is a property of that training window as much as of the system: it inherits whatever was idiosyncratic about the final training years. Established practice in ecological forecast evaluation is to issue forecasts repeatedly from rolling origins and summarise skill indexed by both origin and lead time (Dietze 2017; Thomas et al. 2020; Petchey et al. 2015). This function does the pooling half of that: given the horizons obtained at several origins, it returns a horizon with an interval rather than a point.
et_shelf_life_pool(horizons, conf_level = 0.95)et_shelf_life_pool(horizons, conf_level = 0.95)
horizons |
Either a list of |
conf_level |
Confidence level for the Kaplan–Meier interval on the
median horizon (default |
A data.frame, one row per group (or a single row when
ungrouped), with columns:
Group label, when supplied.
Number of forecast origins contributing.
Origins at which the horizon was reached in-window.
Origins right-censored (still informative at the end).
Pooled horizon in lead time. NA when every
origin is censored — which is itself a result: the forecast never
became uninformative from any origin.
Confidence bounds on the median.
Range of the observed horizons.
"kaplan-meier" or "median (uncensored only)".
Later origins have fewer lead times available, so a long horizon may not be observed before the data run out — the forecast is still informative at the last available lead. That is right-censoring, not a missing value. Dropping those origins would bias the pooled horizon downward (only the short horizons survive), and treating the last lead as the horizon would bias it downward too. Both are avoided by treating the pooled horizon as a time-to-event problem and estimating it with Kaplan–Meier, which uses the censored origins as the partial information they are.
When survival is available the median horizon and its confidence
interval come from survival::survfit(). Otherwise the function falls
back to the median of the observed horizons and says so in the
method field — a fallback that is biased low whenever anything is
censored, which is why the interval is reported as NA there.
Dietze, M. C. (2017) Prediction in ecology: a first-principles framework. Ecological Applications 27:2048–2060.
Petchey, O. L. et al. (2015) The ecological forecast horizon, and examples of its uses and determinants. Ecology Letters 18:597–611.
shelf_life for the single-origin horizon.
# Horizons from five origins; the last was censored. h <- data.frame(lead = c(2, 3, 2, 3, 5), censored = c(FALSE, FALSE, FALSE, FALSE, TRUE)) et_shelf_life_pool(h)# Horizons from five origins; the last was censored. h <- data.frame(lead = c(2, 3, 2, 3, 5), censored = c(FALSE, FALSE, FALSE, FALSE, TRUE)) et_shelf_life_pool(h)
A named list containing training data, forecast predictors, validation responses, and ground-truth parameters for two synthetic SNP clusters (A and B) at a hypothetical mountain plant site. The dataset is designed to exercise every step of the ErrorTracer pipeline and to support parameter-recovery validation (true coefficients are known).
et_simet_sim
A named list with seven elements:
traindata.frame with 100 rows and 6 columns (2 clusters
50 training years, 1970–2019):
Integer. Calendar year.
Character. SNP cluster identifier: "A" or
"B".
Numeric. Standardised mean growing-season temperature (original unit: °C; standardised using training-period mean and SD).
Numeric. Standardised total growing-season precipitation (original unit: mm).
Numeric. Standardised peak snow water equivalent (original unit: mm).
Numeric. Simulated allele-frequency change on the
arcsin-sqrt scale ( transformation).
This transformation is unbounded and has no fixed
[, 1] constraint; the plausible range should be derived
from the training data or from the theoretical bounds
(\ on the arcsin scale).
forecastdata.frame with 30 rows and 5 columns (2 clusters
15 forecast years, 2020–2034). Same columns as train except
z_diff is absent — these are the prediction targets. Predictors
are standardised using the training-period statistics stored in
standardization.
validationdata.frame with 30 rows and 3 columns (year,
cluster_id, z_diff). True response values for the
forecast period; used with et_calibrate to assess
posterior predictive coverage.
true_paramsNamed list with one element per cluster. Each element is a named
numeric vector of the true data-generating parameters:
intercept, Tmean, PPT, SWE, and
sigma (residual SD).
intercept = 0.00, Tmean = 0.50,
PPT = -0.30, SWE = 0.20, sigma = 0.30
intercept = 0.10, Tmean = 0.30,
PPT = -0.20, SWE = -0.25, sigma = 0.35
env_noiseNamed list. Suggested per-predictor environmental noise SDs (on
the standardised scale) for use with the env_noise argument of
et_predict: Tmean = 0.30, PPT = 0.20,
SWE = 0.15.
standardizationNamed list with one element per predictor. Each element is a
named numeric vector c(mean = ..., sd = ...) giving the
training-period statistics used to standardise the data. Needed if you
wish to back-transform predictions to the original (unstandardised)
scale with unstandardize.
descriptionCharacter string. Human-readable description of the dataset and how it was generated.
The climate time series are simulated as AR(1) processes with a warming
trend in Tmean (+0.015 °C yr, cumulating to ~0.30 SD
above the training mean over the 15-year forecast window). SWE
is generated with a weak dependence on Tmean (negative coupling)
and PPT (positive coupling) plus a dominant independent noise
component, so that the three predictors have only mild pairwise
correlation (|R| < 0.2 on the training subset). This makes all
regression coefficients reliably identifiable in a single replicate.
All predictors are standardised using training-period statistics only.
In addition to the three real climate predictors, the dataset includes
ten independent standardised nuisance predictors d1, ..., d10
with zero true effect on the response. They give the regularized-prior
extraction step a real selection job: cv.glmnet (alpha = 0.5) is expected
to identify the three real predictors and shrink the ten dummies toward
zero. Without them, regularization on three well-identified predictors
merely biases the true coefficients without selecting anything.
Responses are generated from a linear model with Gaussian noise:
The true parameters are stored in et_sim$true_params for
parameter-recovery validation.
The dataset was generated with set.seed(111). The full generation
script is at data-raw/generate_et_sim.R.
Generated by data-raw/generate_et_sim.R with
set.seed(111).
data(et_sim) # Inspect structure str(et_sim, max.level = 2) # Training data head(et_sim$train) # True parameters for cluster A et_sim$true_params$A # Suggested noise SDs for et_predict() et_sim$env_noisedata(et_sim) # Inspect structure str(et_sim, max.level = 2) # Training data head(et_sim$train) # True parameters for cluster A et_sim$true_params$A # Suggested noise SDs for et_predict() et_sim$env_noise
Scores a forecast against a null model with the continuous ranked
probability score (CRPS) and returns the per-lead-time skill,
(positive when the
model beats the null). This is the accuracy-relative counterpart to
shelf_life's precision criterion; report both. The
forecast limit — the first lead time beginning a sustained run of
non-skillful periods — is attached as an attribute.
et_skill_score( predictions, observed, response_col = NULL, time_col = NULL, null = c("climatology", "random_walk"), climatology = NULL, rw_sd = NULL, rw_start = NULL, min_run = 2L, ... )et_skill_score( predictions, observed, response_col = NULL, time_col = NULL, null = c("climatology", "random_walk"), climatology = NULL, rw_sd = NULL, rw_start = NULL, min_run = 2L, ... )
predictions |
An |
observed |
A |
response_col |
Character. Response column name; inferred from the model
formula when |
time_col |
Character. Optional time column in |
null |
Character. The null model: |
climatology |
Optional numeric vector giving the climatological reference distribution. Defaults to the training response values. |
rw_sd, rw_start
|
Optional random-walk per-step innovation SD and start
value. Default to |
min_run |
Integer. Number of consecutive non-skillful periods
required before the forecast limit is declared (default |
... |
Unused. |
A data.frame with columns obs_id, time,
crps_model, crps_null, skill, and skillful
(skill > 0); a leading group column for grouped input. The
attribute "forecast_limit" holds a list with the null-relative
horizon, its type ("observed" or "lower_bound"), and
the diagnostics n_below, first_below and min_run;
or is per-group for grouped input.
An optional, order-independent alternative to the additive budget from
decompose_uncertainty. Decomposes the variance of the
response-scale predictive mean into first-order
parameter and environmental (driver) Sobol indices plus
their interaction, by independently resampling the parameter draws
and the predictor perturbations (Saltelli 2010 first-order; Jansen
total-order). Unlike the sequential additive split, this is insensitive to
the order in which factors are added and makes any parameterdriver
interaction explicit — the general case a non-linear link or correlated
drivers can produce.
et_sobol(predictions, n_sobol = 1024L, seed = NULL, ...)et_sobol(predictions, n_sobol = 1024L, seed = NULL, ...)
predictions |
An |
n_sobol |
Integer. Base sample size for the Sobol estimator (default
1024). Total model evaluations per observation are |
seed |
Optional integer for reproducible sampling. |
... |
Unused. |
A data.frame with one row per forecast observation:
Observation index.
First-order Sobol indices (fraction of the predictive-mean variance from parameters / drivers alone).
Total-order indices (including interactions).
1 - S_param - S_env: the share of
predictive-mean variance attributable to the parameterdriver
interaction.
Variance of the predictive mean (the quantity being decomposed).
Mean within-draw family variance (the irreducible
part; var_mu + residual_var approximates the total predictive
variance).
decompose_uncertainty (the default additive budget).
Minimal ggplot2 theme for ErrorTracer plots
et_theme(base_size = 12)et_theme(base_size = 12)
base_size |
Base font size (default 12). |
A ggplot2 theme object.
Converts a fitted model object into a brms prior specification
suitable for et_fit. The regularized or standard fit sets the
scale of each coefficient's prior (its SD); the shrinkage
argument controls the prior mean. With the default
shrinkage = "zero" the priors are centred at zero — a regularizing
(ridge-style) prior whose width is informed by the fit but whose location
carries no reused point estimate. With shrinkage = "estimate" the
coefficient estimates are used as informative prior means, so the Bayesian
model starts close to the regularized solution.
extract_priors( model, multiplier = 2, min_sd = 0.1, shrinkage = c("zero", "estimate"), intercept_prior_sd = NULL, sigma_prior_scale = 1, ... ) ## S3 method for class 'lm' extract_priors( model, multiplier = 2, min_sd = 0.1, shrinkage = c("zero", "estimate"), intercept_prior_sd = NULL, sigma_prior_scale = 1, ... ) ## S3 method for class 'glm' extract_priors( model, multiplier = 2, min_sd = 0.1, shrinkage = c("zero", "estimate"), intercept_prior_sd = NULL, sigma_prior_scale = 1, ... ) ## S3 method for class 'cv.glmnet' extract_priors( model, multiplier = 2, min_sd = 0.1, shrinkage = c("zero", "estimate"), intercept_prior_sd = NULL, sigma_prior_scale = 1, lambda = "lambda.min", ... ) ## S3 method for class 'glmnet' extract_priors( model, multiplier = 2, min_sd = 0.1, shrinkage = c("zero", "estimate"), intercept_prior_sd = NULL, sigma_prior_scale = 1, s = 1L, ... ) ## S3 method for class 'ranger' extract_priors( model, multiplier = 2, min_sd = 0.1, shrinkage = c("zero", "estimate"), intercept_prior_sd = NULL, sigma_prior_scale = 1, ... )extract_priors( model, multiplier = 2, min_sd = 0.1, shrinkage = c("zero", "estimate"), intercept_prior_sd = NULL, sigma_prior_scale = 1, ... ) ## S3 method for class 'lm' extract_priors( model, multiplier = 2, min_sd = 0.1, shrinkage = c("zero", "estimate"), intercept_prior_sd = NULL, sigma_prior_scale = 1, ... ) ## S3 method for class 'glm' extract_priors( model, multiplier = 2, min_sd = 0.1, shrinkage = c("zero", "estimate"), intercept_prior_sd = NULL, sigma_prior_scale = 1, ... ) ## S3 method for class 'cv.glmnet' extract_priors( model, multiplier = 2, min_sd = 0.1, shrinkage = c("zero", "estimate"), intercept_prior_sd = NULL, sigma_prior_scale = 1, lambda = "lambda.min", ... ) ## S3 method for class 'glmnet' extract_priors( model, multiplier = 2, min_sd = 0.1, shrinkage = c("zero", "estimate"), intercept_prior_sd = NULL, sigma_prior_scale = 1, s = 1L, ... ) ## S3 method for class 'ranger' extract_priors( model, multiplier = 2, min_sd = 0.1, shrinkage = c("zero", "estimate"), intercept_prior_sd = NULL, sigma_prior_scale = 1, ... )
model |
A fitted model. Supported classes:
|
multiplier |
Numeric scalar. Prior SD is set to
|
min_sd |
Numeric scalar. Minimum prior SD to avoid degenerate (spike) priors on near-zero coefficients. Default 0.1. |
shrinkage |
Character. Sets the prior mean: |
intercept_prior_sd |
Optional prior SD for the intercept term. The
default |
sigma_prior_scale |
Scale parameter for the half-Cauchy prior on the residual SD sigma. Default 1.0. |
... |
Additional arguments passed to methods. |
lambda |
Which lambda to extract coefficients from when the model is a
|
s |
Index (integer) or value of lambda to extract coefficients at when
the model is a plain |
The ranger path is experimental. Random-forest permutation
importance has no sign and no natural scale in the units of a GLM
coefficient, so the mapping used here — priors centred at zero with SD set
to multiplier * importance_normalised (importance normalised to
[min_sd, 1]; only variables with positive importance retained) — is
a heuristic, not a calibrated correspondence. It is offered for exploratory
feature screening only; for a principled prior prefer glmnet/
lm/glm, whose coefficients are on the model's scale. A signed
importance (e.g. from SHAP values) would be needed to inform the prior mean.
An et_prior_spec list containing:
priorA brmsprior object for use in brms::brm().
pred_namesCharacter vector of included predictor names.
coefsNamed numeric vector of regularized coefficients (NULL for ranger, which uses importance instead).
methodCharacter: the dispatch method used.
multiplier, min_sd
Settings echo.
Centring the priors on the coefficient estimates
(shrinkage = "estimate") and then refitting the Bayesian model on the
same data uses those data twice — once to locate the prior mean and
once through the likelihood — which can make posteriors overconfident,
especially at small . The default shrinkage = "zero" avoids
this by not reusing the estimated location: only the prior scale is
data-informed, which is a standard weakly-informative device. If you use
shrinkage = "estimate", check calibration with
et_calibrate, or build the priors on data held out from the
likelihood fit.
fit_lm <- lm(mpg ~ wt + hp + cyl, data = mtcars) ps <- extract_priors(fit_lm, multiplier = 2, min_sd = 0.1) print(ps)fit_lm <- lm(mpg ~ wt + hp + cyl, data = mtcars) ps <- extract_priors(fit_lm, multiplier = 2, min_sd = 0.1) print(ps)
Quantifies when a forecast becomes uninformative by comparing the
width of credible intervals to a response scale. A forecast is
uninformative when its CI width exceeds threshold * response_scale.
shelf_life( predictions, response_scale = NULL, ci_level = 0.9, threshold = 1, time_col = NULL, skill = NULL, min_slope_for_projection = 1e-04, max_extrapolation_factor = 10, min_run = 2L, projection_alpha = 0.05, ..., plausible_range = NULL )shelf_life( predictions, response_scale = NULL, ci_level = 0.9, threshold = 1, time_col = NULL, skill = NULL, min_slope_for_projection = 1e-04, max_extrapolation_factor = 10, min_run = 2L, projection_alpha = 0.05, ..., plausible_range = NULL )
predictions |
An |
response_scale |
Numeric vector of length 2 ( |
ci_level |
Numeric. The credible interval level to use (default
0.90). Must be present in the |
threshold |
Numeric. CI width / response scale above which the forecast is uninformative (default 1.0). |
time_col |
Character. Optional column in
|
skill |
Optional skill gate. A |
min_slope_for_projection |
Numeric. Minimum linear slope (of
ratio vs.\ time) required to attempt extrapolation when all periods
are informative. Below this value the shelf life is reported as a
lower bound only. Default |
max_extrapolation_factor |
Numeric. Cap on how far the linear
projection may reach beyond the observed window. If the projected
crossing time exceeds
|
min_run |
Integer. Number of consecutive uninformative
periods required before the threshold counts as crossed (default
|
projection_alpha |
Numeric. Significance level for the one-sided
test that the fitted ratio-vs-time slope is positive (default
|
... |
Unused. |
plausible_range |
Deprecated. Use |
Shelf life is a precision criterion on the response scale — a
deliberate complement to accuracy-relative forecast limits (Petchey
et al. 2015; Wesselkamp et al. 2025), not a replacement. It answers "when do
the error bars get too wide to act on?", whereas a forecast limit answers
"when does the model stop beating a null?". Because a forecast can be
precise yet biased, pass a skill table from et_skill_score
to gate the shelf life on skill: a period then counts as
informative only if it is both precise and beats the null model. This
couples the two views and prevents a precise-but-biased forecast from
silently passing.
The function operates in three modes depending on the available data and whether the uninformative threshold is crossed within the forecast window:
The threshold is crossed within the
forecast/validation window and the crossing persists. The
shelf life is the first time point beginning a run of min_run
consecutive periods with ratio >= threshold. Requiring
persistence matters: when the ratio hovers near threshold
without trending, a single noisy period can exceed it by chance, and
the first-exceedance rule used before ErrorTracer 1.3.1 would report
that excursion as a horizon. Isolated exceedances are now reported in
the horizon diagnostics (n_exceedances,
first_exceedance) instead of being promoted to a crossing.
No sustained crossing occurs in-window but the
CI/range ratio is trending upward detectably — the fitted
slope must exceed min_slope_for_projection and be
significantly positive at projection_alpha. A linear trend is
extrapolated to estimate when the threshold would be reached. The
projected crossing time (where
is the threshold, the fitted intercept, the fitted
slope) is reported together with a standard error se_t_star
derived via the delta method.
No sustained crossing and no trend
distinguishable from flat (or a projection beyond the extrapolation
cap). The shelf life is reported as a lower bound:
> last observed time. This is the honest outcome for a
forecast whose precision simply does not degrade in-window.
The intended workflow is:
Fit the model (et_fit).
Predict on held-out data or a future time window
(et_predict).
Call shelf_life() on those predictions.
If the threshold is not crossed in the held-out window, the projected mode extrapolates the horizon automatically.
An et_shelf_life object (a data.frame) with columns:
Observation index.
Time axis value.
Width of the credible interval at ci_level.
Effective response scale (scalar diff).
CI width / response scale.
Logical; TRUE when ratio < threshold.
For grouped predictions a group column is prepended.
The object carries three attributes that drive print():
horizonNamed list with elements value,
type ("observed", "projected", or
"lower_bound"), last_informative,
description, (for projected) se_t_star, and the
exceedance diagnostics n_exceedances,
frac_exceedance, first_exceedance and
min_run. Comparing first_exceedance with
value shows whether the reported horizon rests on a
persistent crossing or whether isolated excursions preceded it.
horizon_by_groupFor grouped objects: named list of per-group horizon lists.
thresholdThe threshold value used.
set.seed(1) df <- data.frame(y = rnorm(20), year = 2001:2020, x1 = rnorm(20)) fit <- et_fit(y ~ x1, data = df, chains = 1, iter = 500, warmup = 250, cores = 1, refresh = 0) new_df <- data.frame(x1 = rnorm(10), year = 2021:2030) pred <- et_predict(fit, newdata = new_df, n_draws = 200, n_perturb = 50) sl <- shelf_life(pred, response_scale = range(df$y), ci_level = 0.90, threshold = 1.0, time_col = "year", max_extrapolation_factor = 10) print(sl)set.seed(1) df <- data.frame(y = rnorm(20), year = 2001:2020, x1 = rnorm(20)) fit <- et_fit(y ~ x1, data = df, chains = 1, iter = 500, warmup = 250, cores = 1, refresh = 0) new_df <- data.frame(x1 = rnorm(10), year = 2021:2030) pred <- et_predict(fit, newdata = new_df, n_draws = 200, n_perturb = 50) sl <- shelf_life(pred, response_scale = range(df$y), ci_level = 0.90, threshold = 1.0, time_col = "year", max_extrapolation_factor = 10) print(sl)
Standardize a numeric vector (mean-centre, unit-variance)
standardize(x)standardize(x)
x |
Numeric vector. |
Standardized numeric vector. Returns zeros if variance is zero.
Reverse standardization
unstandardize(z, mu, s)unstandardize(z, mu, s)
z |
Standardized values. |
mu |
Original mean. |
s |
Original standard deviation. |
Values on the original scale.