Skip to main content
A prediction interval is a claim, and conformalforecast.diagnostics is how you check it. This page covers the two ways to run an evaluation, the metrics worth looking at, and the one measurement that will lie to you.

The two modes

Both take the fitted object a method’s calibrate / calibrate_cv returns.
  • evaluate(fitted, histories, truths) — roll the fitted object over held-out windows it has never seen. This is the honest one.
  • evaluate_calibration(fitted) — replay the method’s own calibration data through the state its own calibration implied at each window. Cheap, requires no holdout, and in sample — so it is flattered by construction and does not track the target. Treat it as a plumbing check, never as a result.
Both return a Report.

The optimistic measurement

The two disagree, and the size of the gap is the point. Run both against the same fitted method: evaluate_calibration replays the windows the method calibrated on, evaluate rolls it over a holdout it has never seen.
Nineteen points apart, and the flattering number is the one that needs no holdout. An interval that looks well above target on its own calibration data is under-covering badly on data it has not seen.
evaluate_calibration is a smoke test, not a result It scores the method on the very windows it calibrated on, so it is measuring how well a quantile fits the sample it was taken from. For split conformal the answer is “by construction, about right” — which tells you the plumbing works and nothing about whether the interval will hold next month. Online methods are flattered further still: the replay is in sample with respect to the state that adapted on that same stream. SPCI is the extreme case — its interval is conditional on the residual window immediately before the step, so its replay re-queries each window’s own history rather than broadcasting the final answer. Because its quantile regressors were fit on the very residual rows that replay then queries, the figure can sit near 100% while a holdout puts the same method well below target — so for SPCI in particular, do not read the number as coverage. Windows it cannot reach that way, the first window_size of each series, are excluded rather than counted as misses; Report.n_evaluated_cells says how many cells the numbers came from. Use it to check that a pipeline runs. Report evaluate.

What a report contains

summary() is the human-readable digest. The Report object also carries the raw intervals, truths and points arrays, so any metric the module exposes can be recomputed from it — which is what the rest of this page does.

Coverage, sliced

Marginal coverage is one number over every cell. It is the headline, and it hides the two things most likely to be wrong.
This is what “marginal, not conditional” means in practice. The 76.7% headline is an average over wildly unequal parts: steps 1 to 5 and 9 to 12 are at or near 100%, while steps 7 and 8 cover 20%. Nothing about the marginal figure would have told you that the interval essentially fails at the summer peak. If your decision depends on a specific lead time, that lead time’s bar is your coverage — not the headline.

Width, and why coverage alone is useless

A very large interval can cover everything, so any coverage number has to be read next to a width.
The Winkler score puts both on one scale: it charges the width of the interval, plus a penalty of 2/alpha times how far outside it the truth landed. Lower is better, and unlike coverage it cannot be gamed by widening.

Comparing methods

The point of a single-number score is that it ranks. Here are three methods on the same holdout.
Read this plot the way you would read a Pareto front: up is valid, left is useful, and the dashed line is the promise you made. All three sit below it here, which is the honest outcome — AirPassengers trends hard, five years of holdout is a genuine distribution shift, and no method on this page gets a free pass from that. The ranking is still informative, and nothing dominates: split conformal is the narrowest (86.9) and covers worst (76.7%), CQR covers best (88.3%) and is the widest (108.1), and ACI sits between them on both. ACI wins the Winkler score because a 5% width premium over split conformal bought it a 3-point coverage gain, and Winkler prices that trade for you. That is the argument for a scalar score: the scatter shows you there is a trade-off, and the score tells you which side of it to be on.

Coverage over time

For an online method the question is not “what was the coverage” but “when was it wrong”. rolling_coverage answers that, with one shape subtlety worth knowing first.
It reduces the samples axis and leaves the horizon axis alone: (n_series, n_samples - window + 1, horizon). Five holdout windows with a window of 3 therefore gives 3 values for each of the 12 horizon steps, not one line. What we want is calendar time. The five holdout windows are contiguous, non-overlapping 12-month blocks in chronological order, so their 60 cells are the last 60 months of the series. Reshaping to one month per sample turns the rolling window into a rolling year.
Coverage sits in the low 80s for the first year and a half, touches 100% around months 20 to 28, then falls away to 50% by month 40 before recovering. The 80% marginal figure is the average of all of that, and it would never have told you that there is a stretch in the middle of the holdout where half the observations landed outside a 90% interval. That is the shape a method losing its calibration makes: fine while the future resembles the calibration set, worse as the trend pulls away. The obvious response is to raise ACI’s learning rate so it reacts faster, and the ACI page measures what that buys and what it costs: pushed far enough, alpha_t is driven to zero, the interval widens without bound, and the coverage is bought by predicting “somewhere between minus and plus infinity”. AgACI is the sturdier answer — it runs several learning rates at once and lets an aggregator discard the ones that blow up.
This view mixes lead times Flattening blends the 1-month-ahead cell of each block with its 12-month-ahead cell, so a dip may be a bad month or a bad lead time. That is the right view for “how did the deployed interval behave over the year”, which is what a pipeline re-forecasting annually actually experiences, and the wrong view for isolating a horizon step — coverage_by_horizon is for that. When every prediction shares one lead time, as on the online updating page, the distinction disappears and rolling_coverage needs no reshape.

Everything else in the module

Not shown above, but available and documented in the diagnostics API reference:
  • coverage_per_cell — the full (n_series, horizon) grid behind the two reductions used here.
  • coverage_by_magnitude_bin — coverage against the size of the truth, which finds the “we only miss on big values” failure.
  • coverage_by_group — coverage against any grouping you supply, for panels with categories.
  • pinball_loss — per-quantile loss, for comparing quantile forecasts directly.
  • method_state and its per-method siblings aci_state, agaci_state, nexcp_state, spci_state — the internal state of an online method, used on the ACI and AgACI pages.

Next