
Post-hoc evaluation of a rolling-window forecast-validation artifact
Source:R/evaluate_rolling_cv.R
evaluate_rolling_cv.RdReads a run_rolling_cv predictions artifact and computes
out-of-sample forecast skill: per-cell metrics (R2 correlation & SSE, bias
ratio, 50/95% PI coverage, WIS) for the in-sample fit and for cumulative OOS
horizons, plus MAE-skill and WIS-skill versus seasonal-climatology and
persistence baselines, aggregated across cutoffs and countries with bootstrap
CIs (suppressed at small cell counts).
Arguments
- predictions
A
run_rolling_cvoutput directory, a path to apredictions.parquet, or the predictions data frame itself.- horizons_months
Cumulative OOS horizons in months (default
c(1,3,5)).- baselines
Baselines for skill scores (default
c("seasonal","persistence","persistence_last")).- metrics
Metrics to evaluate (default
c("cases","deaths")).- trusted_only
Drop rows with
observed_source == "AI"(default TRUE).- embargo_weeks
Channel-specific OOS embargo, in weeks, after the cutoff before scoring begins, applied on top of the harness embargo (see Details). A scalar applies to all metrics; a named vector (e.g.
c(cases = 4, deaths = 6)) applies per metric, and omitted metrics get 0. Default0.- ess_min
Minimum importance-weight ESS for a cell to enter the summary aggregation. Default
0(gate off). See Details.- min_cells_ci
Minimum number of finite cells in a (model, metric, window) group required to emit a bootstrap CI; below this the CI is suppressed (lo = hi = NA) and
ci_suppressed = TRUE. Default5.- n_boot
Bootstrap resamples over cells for the skill CI (default 1000).
- seed
RNG seed for the bootstrap (default 1).
When the predictions carry a
modelcolumn (ensemble,ensemble_opt,best,medoid), every metric is computed and reported per model; absent that column all rows are treated as a single"ensemble"model (back-compatible).
Value
A list with:
- cells
Per (cutoff x model x iso x metric x window) metrics + per-baseline mae_skill and wis_skill, the cell
essandess_okflag (plusanchor_datewhen the predictions carry it).- summary
Aggregated across
ess_okcells per (model x metric x window):n_cells_used/n_cells_total, mean/median of each metric, and mean baseline skills with bootstrap CI (suppressed when fewer thanmin_cells_cifinite cells), plusess_gatedandci_suppressedflags.
Details
This is the post-hoc companion to run_rolling_cv() — the harness emits
predictions only; all judgement (metrics, baselines, skill) lives here.
Horizons are cumulative (\(\le h\) months): the \(\le h\) window
is every scored OOS date \(d\) with origin < d <= origin +
ceiling(h * 30.4375) days, where origin is the per-metric OOS-scoring
origin below. The origin is fixed by the cutoff and embargo, never by where
observations happen to exist, so a reporting gap just after the cutoff shrinks
a window's n instead of shifting its lead times (windows with fewer
than 3 scorable dates are dropped). The \(\le\)max-horizon window is the
largest scored set; predictions further out are not scored in any window. An
IS window (training fit, \(\le\) cutoff) is always reported.
Scoring uses trusted observed only: rows whose observed_source
is missing or not "AI" (when trusted_only = TRUE).
Embargo and scoring origin. The harness labels the dates in
(cutoff, cutoff + embargo] as segment == "embargo"; those are
never scored. For each metric the scoring origin is
max(last embargo-labelled date (or the cutoff), cutoff_date +
embargo_weeks[metric] * 7) and the scored dates are those strictly after it.
embargo_weeks can therefore only lengthen the harness embargo, per
channel: it may be a scalar (all metrics) or a named vector such as
c(cases = 4, deaths = 6); metrics it omits get 0. With the default
(0) exactly the segment == "OOS" block is scored, from the
harness's own OOS origin.
Baselines, fit per (cutoff, country, metric) on the IS observed
history. "seasonal" (week-of-year climatology) is the primary
baseline and requires \(\ge 2\) years of in-sample history; with
less, the seasonal baseline (and its skill) is NA (no grand-mean
fallback). "persistence" (trailing 4-point mean carried forward) and
"persistence_last" (the single last observed value) are secondary
nulls. Each baseline is also given 50/95% predictive intervals from empirical
quantiles of its in-sample residuals, so a genuine WIS-skill exists:
\(1 - WIS_{model}/WIS_{baseline}\). mae_skill is the analogous
MAE ratio. Positive = model beats the baseline; NA when the baseline
(or its interval) cannot be formed.
Weight-ESS gate. If the predictions carry an ess column (the
cutoff calibration's importance-weight ESS, constant per cutoff), each cell is
flagged ess_ok = (!is.na(ess) & ess >= ess_min). Cells failing the gate
remain in $cells (flagged) but are excluded from the
$summary aggregation. With ess_min = 0 (default) the gate is off
(NA ess still flags ess_ok = FALSE, but the gate threshold 0 admits any
finite ess). When the ess column is absent, ess_ok = TRUE for all
cells (back-compat) and $summary$ess_gated = FALSE.