Evaluation Metrics#
Source Files
twiga/core/metrics/point.pytwiga/core/metrics/interval.pytwiga/core/metrics/quantile.pytwiga/core/metrics/parametric.pytwiga/core/metrics/stats.py
Twiga provides a comprehensive set of metrics for evaluating point forecasts, prediction intervals, quantile forecasts, and full probabilistic forecasts. All metrics operate on NumPy arrays and are accessible via evaluate_forecast().
Point Forecast Metrics#
Defined in twiga/core/metrics/point.py. Used via evaluate_point_forecast().
Metric |
Key |
Formula |
Description |
|---|---|---|---|
Mean Error |
|
\(\frac{1}{T}\sum(y_t - \hat{y}_t)\) |
Average signed error (bias indicator) |
MAE |
|
\(\frac{1}{T}\sum|y_t - \hat{y}_t|\) |
Mean Absolute Error |
MedAE |
|
\(\text{median}(|y_t - \hat{y}_t|)\) |
Median Absolute Error - robust to outliers |
MSE |
|
\(\frac{1}{T}\sum(y_t - \hat{y}_t)^2\) |
Mean Squared Error |
RMSE |
|
\(\sqrt{MSE}\) |
Root Mean Squared Error |
RMSLE |
|
\(\sqrt{\frac{1}{T}\sum(\log(1+\hat{y}_t)-\log(1+y_t))^2}\) |
Root Mean Squared Log Error - scale-invariant, requires \(y,\hat{y}\geq 0\) |
NRMSE |
|
\(\frac{RMSE}{\text{scale}}\) |
Normalized RMSE (scale: max, range, or mean) |
MAPE |
|
\(\frac{100}{T}\sum\frac{|y_t - \hat{y}_t|}{|y_t|}\) |
Mean Absolute Percentage Error |
sMAPE |
|
\(\frac{200}{T}\sum\frac{|y_t - \hat{y}_t|}{|y_t| + |\hat{y}_t|}\) |
Symmetric MAPE |
WMAPE |
|
\(100 \times \frac{\sum|y_t - \hat{y}_t|}{\sum|y_t|}\) |
Weighted MAPE |
OPE |
|
\(100 \times \frac{|\sum y_t - \sum \hat{y}_t|}{\sum y_t}\) |
Overall Percentage Error |
MASE |
|
\(\frac{MAE}{MAE_{\text{naive}}}\) |
Mean Absolute Scaled Error (naive = lag-1) |
MSSE |
|
\(\frac{MSE}{MSE_{\text{naive}}}\) |
Mean Squared Scaled Error |
RMSSE |
|
\(\frac{RMSE}{RMSE_{\text{naive}}}\) |
Root Mean Squared Scaled Error |
R² |
|
\(1 - \frac{\sum(y_t-\hat{y}_t)^2}{\sum(y_t-\bar{y})^2}\) |
Coefficient of Determination - proportion of variance explained |
Correlation |
|
\(\rho(y, \hat{y})\) |
Pearson correlation coefficient |
Normalized Bias |
|
\(\sum\frac{y_t - \hat{y}_t}{y_t + \hat{y}_t}\) |
Normalized bias error |
Bold entries are new additions aligned with standard libraries (NeuralForecast, Darts, scikit-learn).
Skill Score#
The Skill Score measures relative improvement over a naive baseline:
It is a standalone function (not part of get_pointwise_metrics) because it requires a baseline forecast:
from twiga.core.metrics import skill_score
ss = skill_score(y, y_hat, y_naive=seasonal_naive_forecast, metric="mae")
# ss > 0: model beats the baseline; ss = 0: no improvement; ss < 0: worse
Using Point Metrics#
from twiga.core.metrics.point import evaluate_point_forecast
metrics_df = evaluate_point_forecast(result, metric_names=["mae", "rmse", "r2", "smape", "corr"])
Available metric keys: "res", "mae", "mase", "mse", "msse", "rmse", "rmsse", "mape", "wmape", "smape", "ope", "corr", "nbias", "r2", "rmsle", "medae".
If metric_names is None, all available metrics are computed.
Interval Forecast Metrics#
Defined in twiga/core/metrics/interval.py. Used via evaluate_interval_forecast().
Metric |
Key |
Formula |
Description |
|---|---|---|---|
Winkler Score |
|
\(\frac{1}{n}\!\sum_t\!\left[(U_t\!-\!L_t) + \frac{2}{\alpha}(L_t\!-\!y_t)\mathbf{1}_{y_t < L_t} + \frac{2}{\alpha}(y_t\!-\!U_t)\mathbf{1}_{y_t > U_t}\right]\) |
Penalises both interval width and missed coverage |
Coverage (PICP) |
|
\(\frac{1}{n}\sum_t \mathbf{1}[L_t \leq y_t \leq U_t]\) |
Proportion of true values within prediction intervals |
ACE |
|
\(PICP - (1 - \alpha)\) |
Average Coverage Error: signed deviation from the nominal level |
NMPI |
|
\(\frac{\text{median}(U_t - L_t)}{\text{scale}}\) |
Normalised Median Prediction Interval width |
CWE |
|
\(\lambda \cdot \frac{(1+\beta^2)\,\gamma_{\text{picp}}\,\gamma_{\text{nmpi}}}{\beta^2\,\gamma_{\text{nmpi}} + \gamma_{\text{picp}}}\) |
Coverage–Width–Error: harmonic mean of coverage and width scores, weighted by forecast accuracy |
γ_picp |
- |
\(\frac{e^{-|\text{PICP}-(1-\alpha)|} - m}{1-m},\; m = \min(e^{-(1-\alpha)},\,e^{-\alpha})\) |
Coverage adequacy: peaks at 1 when PICP equals the target; decays symmetrically for both under-coverage and over-coverage |
γ_nmpi |
- |
\(\begin{cases}\frac{e^{-k(p-\hat{p})}-e^{-kp}}{1-e^{-kp}} & \hat{p}<p \\ \frac{e^{-k(\hat{p}-p)}-e^{-k(1-p)}}{1-e^{-k(1-p)}} & \hat{p}\geq p\end{cases}\) |
Width score: peaks at 1 when NMPI equals the reference κ/R; penalises both too-narrow and too-wide intervals |
MSIS |
|
\(\frac{\frac{1}{n}\sum_t\!\left[(U_t\!-\!L_t) + \frac{2}{\alpha}(L_t\!-\!y_t)\mathbf{1}_{y_t < L_t} + \frac{2}{\alpha}(y_t\!-\!U_t)\mathbf{1}_{y_t > U_t}\right]}{\frac{1}{T-s}\sum_{i=s+1}^{T}|y_i - y_{i-s}|}\) |
Mean Scaled Interval Score - M4/M5 competition standard |
MSIS#
The Mean Scaled Interval Score is the standard metric for prediction interval comparison in the M4/M5 competitions. It penalizes width and missed coverage, normalized by the naive seasonal forecast MAE:
from twiga.core.metrics import msis
score = msis(true, lower, upper, y_train=training_series, alpha=0.1, seasonality=24)
CWE - Coverage–Width–Error#
CWE is the primary interval quality metric in Twiga. It uses the F-β harmonic mean of two sub-scores, weighted by point-forecast accuracy λ:
where \(\lambda = 1 - \text{error}\) (e.g. \(1 - \text{NRMSE}\)). When error=None (the default in get_interval_metrics), λ = 1 and the accuracy weight is omitted.
Sub-scores:
Exponential peak at PICP = 1 − α where the score equals 1. The score decays symmetrically toward 0 as PICP moves away from the target in either direction. Both under-coverage (PICP < 1 − α) and over-coverage (PICP > 1 − α) are penalised.
where \(\hat{p}\) = NMPI (normalised), \(p\) = κ/R (reference). The score peaks at 1 when NMPI equals the reference. Both too-narrow (\(\hat{p} < p\)) and too-wide (\(\hat{p} > p\)) intervals are penalised, with the falloff rate differing on each side due to the asymmetric normalisation.
Bonus condition: when NMPI < κ/R and PICP ≥ 1 − α simultaneously (sharper than reference and sufficient coverage), both γ scores are set to 1.0, giving a perfect CWE regardless of how much sharper the interval is.
Harmonic mean handles the tension naturally: a poor score on either dimension pulls CWE toward zero regardless of the other. An over-wide interval is penalised even when PICP is perfect, and under-coverage is penalised even when width looks reasonable.
Reference κ/R: computed from the target distribution as spread ÷ scale. Both are configurable:
from twiga.core.metrics.interval import get_interval_metrics
# Default: std spread, max scale
df = get_interval_metrics(true, lower, upper, alpha=0.1)
# IQR spread - robust to outliers and heavy tails
df = get_interval_metrics(true, lower, upper, alpha=0.1, spread="iqr")
# MAD spread - best for bimodal or heavily skewed targets (e.g. solar with nights)
df = get_interval_metrics(true, lower, upper, alpha=0.1, spread="mad")
# Custom scale denominator
df = get_interval_metrics(true, lower, upper, alpha=0.1, nmpi_scale="range")
# Pass your own κ directly (e.g. domain knowledge on expected width)
df = get_interval_metrics(true, lower, upper, alpha=0.1, true_nmpi=15.0)
|
κ formula |
Default |
Best for |
|---|---|---|---|
|
σ |
Yes |
Approximately Gaussian targets |
|
q₇₅ − q₂₅ |
General; robust to outliers and heavy tails |
|
|
median(‖y − median(y)‖) |
Bimodal or heavily skewed targets |
|
R formula |
Default in |
Default in |
Best for |
|---|---|---|---|---|
|
max |
Yes |
Non-negative data where min ≈ 0 |
|
|
max − min |
Yes |
General; translation-invariant |
|
|
mean |
NMPI as fraction of mean load |
||
|
median |
Robust alternative to mean for skewed data |
Using Interval Metrics#
from twiga.core.metrics.interval import evaluate_interval_forecast
metrics_df = evaluate_interval_forecast(result, alpha=0.1)
# Returns: timestamp | target | res | mae | ... | winkler_score | picp | ace | nmpi | cwe
# Interval-metric tuning with MAD spread
metrics_df = evaluate_interval_forecast(result, alpha=0.1, spread="mad", nmpi_scale="range")
Note
evaluate_interval_forecast() computes both point metrics and interval metrics for each forecast day. The alpha parameter should match the significance level used during conformal calibration. CWE is always in [0, 1] - higher is better.
Quantile Forecast Metrics#
Defined in twiga/core/metrics/quantile.py. Used via evaluate_quantile_forecast().
Metric |
Key |
Formula |
Description |
|---|---|---|---|
Pinball Loss |
|
\(\frac{1}{KT}\sum_{\tau,t}\rho_\tau(y_t, \hat{q}_{\tau,t})\) where \(\rho_\tau(y,q)=\max(\tau(y-q),\,(\tau-1)(y-q))\) |
Mean pinball (check) loss across all quantile levels |
WQL |
|
$\frac{2\sum_\tau\sum_t\rho_\tau(y_t, \hat{q}_{\tau,t})}{ |
\mathcal{T} |
CRPS |
|
\(\int_{-\infty}^{\infty}\!\!(F(z) - \mathbf{1}[z \geq y])^2\,dz\) (piecewise-linear \(F\)) |
CRPS approximated from quantile CDF |
Calibration Error |
|
\(\frac{1}{K}\sum_\tau|\hat{F}(\tau) - \tau|\) |
Mean absolute deviation of empirical coverage from nominal level |
Sharpness |
|
$\frac{1}{ |
\mathcal{P} |
PIT |
- |
histogram of \(\hat{F}(y_t)\) |
Probability Integral Transform - uniform shape indicates calibration |
Weighted Quantile Loss (WQL)#
WQL is the GluonTS/M5 standard aggregate quantile metric, normalized by total observation volume for cross-series comparability:
from twiga.core.metrics import wql
score = wql(true, quantile_preds, quantile_levels=[0.1, 0.5, 0.9], quantile_axis=0)
Using Quantile Metrics#
from twiga.core.metrics.quantile import evaluate_quantile_forecast
metrics_df = evaluate_quantile_forecast(result, alpha=0.1)
# Columns: point metrics + picp | ace | nmpi | cwe | pinball | wql | crps | calibration_error | sharpness
Probabilistic Metrics#
Defined in twiga/core/metrics/parametric.py. Used via evaluate_parametric_forecast().
Metric |
Key |
Formula |
Description |
|---|---|---|---|
CRPS |
|
\(\mathbb{E}_F|X - y| - \tfrac{1}{2}\mathbb{E}_F|X - X'|\) |
Sample-based CRPS - proper scoring rule generalising MAE to distributions |
CRPS (PWM) |
|
(PWM estimator of the same quantity) |
CRPS via Probability Weighted Moments - more stable for small sample counts |
DSS |
|
\(\frac{(y_t - \mu_t)^2}{\sigma_t^2} + 2\log\sigma_t\) |
Dawid-Sebastiani Score - proper rule jointly rewarding calibration and sharpness |
Energy Score |
|
\(\mathbb{E}_F|\mathbf{X}-\mathbf{y}|_2 - \tfrac{1}{2}\mathbb{E}_F|\mathbf{X}-\mathbf{X}'|_2\) |
Multivariate generalisation of CRPS for joint forecast distributions |
Variogram Score |
|
$\sum_{i,j} w_{ij}( |
y_i-y_j |
Brier Score |
|
\(\frac{1}{n}\sum_t(\hat{p}_t - o_t)^2\) |
Probabilistic binary event forecast accuracy |
NLL |
|
\(-\frac{1}{n}\sum_t\log f(y_t\mid\theta_t)\) |
Negative log-likelihood under the predictive distribution |
Dawid-Sebastiani Score (DSS)#
DSS is a proper scoring rule for location-scale parametric forecasts, jointly rewarding sharpness and calibration:
from twiga.core.metrics.parametric import dawid_sebastiani_score
dss = dawid_sebastiani_score(true, loc=predicted_means, scale=predicted_stds)
Energy Score#
The Energy Score generalises CRPS to multivariate (joint horizon) forecast distributions:
from twiga.core.metrics.parametric import energy_score
# samples shape: (n_ensemble_members, forecast_horizon)
es = energy_score(true=true_vector, samples=ensemble_samples)
Variogram Score#
The Energy Score responds mainly to errors in the mean and has limited power against a misspecified correlation, so a model can place the dependence badly and lose very little of it. The Variogram Score compares pairwise differences instead and is sensitive to exactly that term:
from twiga.core.metrics.parametric import variogram_score
# samples shape: (n_ensemble_members, n_targets)
vs = variogram_score(true=true_vector, samples=ensemble_samples, order=0.5)
It is proper but not strictly proper, being invariant to a shift of the whole vector, which is the price of its sensitivity. Report it alongside the Energy Score rather than instead of it: where the two disagree, the disagreement is itself the finding.
order defaults to 0.5, which is robust to heavy tails; weights accepts a non-negative matrix to down-weight pairs whose dependence is not of interest.
Reference: Scheuerer & Hamill (2015), Variogram-based proper scoring rules for probabilistic forecasts of multivariate quantities, Monthly Weather Review 143(4), 1321-1334.
Brier Score#
Measures accuracy of probabilistic binary event forecasts (e.g., exceedance probability):
from twiga.core.metrics.parametric import brier_score
bs = brier_score(true, samples=ensemble_samples, threshold=peak_demand)
Using Probabilistic Metrics#
from twiga.core.metrics.parametric import evaluate_parametric_forecast
# Sample-based forecasts (CRPS + Energy Score)
metrics_df = evaluate_parametric_forecast(samples_result)
# Parametric forecasts (NLL + DSS)
metrics_df = evaluate_parametric_forecast(parametric_result)
Or via the low-level helper for direct array inputs:
from twiga.core.metrics.parametric import get_probabilistic_metrics
# Parametric mode (loc + scale)
metrics_df = get_probabilistic_metrics(true, loc=mu, scale=sigma)
# Sample-based mode
metrics_df = get_probabilistic_metrics(true, samples=ensemble_samples)
Analytical Parametric Scoring#
Defined in twiga/core/metrics/parametric.py. Closed-form implementations for common parametric distributions - no Monte Carlo sampling needed.
Metric |
Key |
Distribution |
Formula |
Description |
|---|---|---|---|---|
Normal NLL |
|
\(\mathcal{N}(\mu,\sigma^2)\) |
\(\frac{1}{n}\sum_t\left[\frac{1}{2}\log(2\pi\sigma_t^2) + \frac{(y_t-\mu_t)^2}{2\sigma_t^2}\right]\) |
Negative log-likelihood under a Normal predictive distribution |
Normal CRPS |
|
\(\mathcal{N}(\mu,\sigma^2)\) |
\(\sigma\!\left[z(2\Phi(z)-1) + 2\phi(z) - \tfrac{1}{\sqrt{\pi}}\right],\; z=\tfrac{y-\mu}{\sigma}\) |
Exact analytical CRPS - no quantile approximation needed |
Laplace NLL |
|
\(\text{Laplace}(\mu,b)\) |
\(\frac{1}{n}\sum_t\left[\log(2b_t) + \frac{|y_t-\mu_t|}{b_t}\right]\) |
Negative log-likelihood under a Laplace predictive distribution |
from twiga.core.metrics.parametric import normal_nll, normal_crps, laplace_nll
nll = normal_nll(true, loc=mu, scale=sigma)
crps = normal_crps(true, loc=mu, scale=sigma) # exact closed form
The analytical normal_crps uses:
Panel Metrics (many series, M5-style)#
When thousands of products are forecast together, raw errors are dominated by the largest series and per-series percentages explode on the smallest. twiga.core.metrics.panel scores a panel the way the M5 competition did. It works directly on the long results frame that backtesting() and the evaluate_* methods return in panel mode (one row per series, timestamp and model):
Function |
What it computes |
|---|---|
|
In-sample error of the (seasonal) naive forecast per series, from its first non-zero value |
|
Each series’ share of value (e.g. revenue) over the last |
|
Per-series MAE, RMSE, MASE, RMSSE and weight |
|
Value-weighted mean, e.g. WRMSSE |
|
WRMSSE at every hierarchy level (total, store, category, …, item), forecasts summed bottom-up |
|
Per-series scaled pinball loss (SPL) of quantile forecasts |
|
WSPL at every hierarchy level, each level scored on its own quantile forecasts: the M5 Uncertainty score |
|
Mean and standard deviation of a score across backtest folds |
With a Folds column, scales and weights are recomputed per fold from the history before that fold’s origin, so nothing from the evaluation window leaks into them.
from twiga.core.metrics import fold_stability, hierarchical_scores, panel_point_scores, weighted_panel_score
preds, _ = forecaster.backtesting(panel) # panel mode: DataPipelineConfig(id_column="id")
history = panel.assign(revenue=panel["sales"] * panel["price"])
scores = panel_point_scores(preds, history, "id", "sales", value_column="revenue", weight_window=12)
wrmsse = weighted_panel_score(scores).reset_index() # per model and fold
print(fold_stability(wrmsse, "wrmsse")) # mean and std across folds
levels = {"total": [], "store": ["store_id"], "category": ["cat_id"], "item-store": ["id"]}
by_level = hierarchical_scores(preds, history, tags, levels, "id", "sales", value_column="revenue", weight_window=12)
m5_score = by_level.groupby(["Model", "Folds"])["wrmsse"].mean() # M5: levels weighted equally
Quantiles per level. Quantiles do not add up: the 95 % quantile of a store’s sales is not the sum of its products’ 95 % quantiles. So the M5 Uncertainty track forecast every level separately, and hierarchical_quantile_scores scores each level on its own quantile forecasts, against the history summed to that level. Forecast the panel aggregated with twiga.hierarchy.aggregate_levels, read any forecast kind at common levels with ForecastResult.quantile_frame(levels) (quantile models are interpolated; parametric and count heads use their distribution family), and stack them with a level column:
from twiga.core.metrics import hierarchical_quantile_scores
M5_LEVELS_Q = (0.005, 0.025, 0.165, 0.25, 0.5, 0.75, 0.835, 0.975, 0.995)
quantiles = result.quantile_frame(M5_LEVELS_Q).assign(level="item-store", series=lambda f: f["id"])
# ... the same for each aggregated level, keyed as aggregate_levels names its groups ...
wspl = hierarchical_quantile_scores(level_quantiles, history, tags, levels, "id", "sales", value_column="revenue")
m5_uncertainty = wspl.groupby(["model", "Folds"])["wspl"].mean() # levels weighted equally
Two M5 lessons are built into these functions:
Report by level. No method won at every aggregation level: the winner ranked 11th–13th at the item levels, and the runner-up ranked near last there. Use
hierarchical_scoresbefore concluding one model is best.Report stability. The winner chose his ensemble on the mean and the standard deviation of cross-validation error.
fold_stabilitygives both.
The panel forecasting tutorial applies these metrics to 500 M5 products aggregated to months.
Statistical Significance Testing#
Defined in twiga/core/metrics/stats.py.
Both tests share the same interface: they accept raw forecast arrays (p_real, p_pred_1, p_pred_2) of shape (n_days, n_steps_per_day) and return a one-sided p-value. A small p-value (< 0.05) means p_pred_2 is significantly more accurate than p_pred_1.
Test |
Key |
Statistic |
Formula |
What it tests |
|---|---|---|---|---|
DM |
|
\(\mathcal{N}(0,1)\) |
\(DM = \frac{\bar{d}}{\sqrt{\hat{\sigma}^2_{\text{HAC}} / T}}\) |
Unconditional equal predictive accuracy |
GW |
|
\(\chi^2(q)\) |
\(T \cdot R^2\) from OLS on instruments \(h_t = [1, d_{t-1}]\) |
Conditional predictive accuracy (exploits information set) |
Diebold-Mariano (DM) Test#
Tests unconditional equal accuracy. The loss differential \(d_t = g(e_{1,t}) - g(e_{2,t})\) is tested against zero using a HAC Bartlett variance estimator to handle serial correlation in multi-step forecasts:
from twiga.core.metrics import diebold_mariano_test
# Multivariate (single test, averaging across steps)
p = diebold_mariano_test(p_real, p_pred_1, p_pred_2, norm=1, version="multivariate", h=24)
# Univariate (one test per step, e.g. per hour in a 24h horizon)
p_per_hour = diebold_mariano_test(p_real, p_pred_1, p_pred_2, norm=1, version="univariate", h=24)
# p_per_hour.shape == (24,)
Giacomini-White (GW) Test#
Tests conditional predictive accuracy - it checks whether one forecast systematically exploits information the other ignores. Uses instruments \(h_t = [1, d_{t-1}]\) in an OLS regression; the test statistic is \(T \cdot R^2 \sim \chi^2(q)\):
from twiga.core.metrics import giacomini_white_test
# Multivariate (single joint test)
p = giacomini_white_test(p_real, p_pred_1, p_pred_2, norm=1, version="multivariate")
# Univariate (per-step tests)
p_per_hour = giacomini_white_test(p_real, p_pred_1, p_pred_2, norm=1, version="univariate")
When to use which test:
Use DM for a straightforward comparison of two forecasts without conditioning on past information.
Use GW when you suspect one model exploits time-varying information (e.g., regime changes, calendar effects) that the other does not - GW is more powerful in these situations.
Comparing more than two conditions#
diebold_mariano_test and giacomini_white_test compare one pair at a time. When there are more
than two models or conditions, twiga/core/metrics/stats.py provides two higher-level functions
built on top of the pairwise primitives, each suited to a different question.
pairwise_dm_table runs the DM test on every unordered pair of per-fold loss arrays and
returns a tidy dataframe, with Benjamini-Hochberg correction applied jointly across all pairs
(controlling the false-discovery rate across the whole comparison, not per pair):
from twiga.core.metrics import pairwise_dm_table
losses = {"FPQR": fpqr_losses, "QR": qr_losses, "BNN": bnn_losses} # each (n_folds, horizon)
table = pairwise_dm_table(losses, alpha=0.05)
# condition_a condition_b p_value p_adjusted significant winner
# 0 FPQR QR 0.021 0.031 True FPQR
# ...
mcs_ranking instead asks “which subset of conditions can be statistically distinguished from
the best one, given the data?” via the Hansen-Lunde-Nason Model Confidence Set procedure, wrapping
model_confidence_set:
from twiga.core.metrics import mcs_ranking
mcs = mcs_ranking(losses, alpha=0.10, n_boot=1000)
mcs.included # conditions statistically indistinguishable from the best
mcs.eliminated # conditions ruled out, in elimination order
mcs.best # the single best-performing condition
Use pairwise_dm_table when every specific pairwise comparison matters (e.g. reporting which
model beats which in a results table); use mcs_ranking when the question is which models survive
as a group, without inflating the false-positive rate across many pairwise tests.
Unified Dispatch#
Use evaluate_forecast() as the single entry point - it dispatches automatically based on result.kind:
from twiga.core.metrics import evaluate_forecast
metrics_df = evaluate_forecast(result) # auto-dispatches
metrics_df = evaluate_forecast(result, alpha=0.1) # for interval/quantile kinds
Supported kind values:
|
Evaluate function |
Key metrics |
|---|---|---|
|
|
MAE, RMSE, R², MedAE, RMSLE, … |
|
|
point metrics + PICP, ACE, NMPI, MSIS, CWE |
|
|
point + interval + pinball, WQL, CRPS, sharpness |
|
|
point metrics + CRPS, Energy Score |
|
|
point metrics + NLL, DSS |
API Reference#
Scale-free, value-weighted metrics for panels of many series (M5-style evaluation).
When thousands of products are forecast together, raw errors are dominated by the largest series and per-series percentages explode on the smallest. The M5 competition scored forecasts instead with:
RMSSE / MASE: each series’ RMSE / MAE divided by the in-sample error of a naive forecast on that series’ own history (from its first non-zero value), so a score of 1 means “as good as the naive forecast”.
WRMSSE: RMSSE averaged with weights proportional to each series’ recent sales value, so the evaluation reflects business impact.
Hierarchy levels: the same score on series aggregated by store, category, total, etc., with the levels averaged equally.
Scaled pinball loss (SPL): the quantile analogue of MASE.
Every function takes the long results frame that
backtesting() and the
evaluate_* methods return in panel mode (one row per series, timestamp,
target and model, with forecast and Actual columns), plus the training
history used for scales and weights. With a Folds column, scales and
weights are recomputed per fold from the history before that fold’s origin,
so nothing from the evaluation window leaks into them.
- twiga.core.metrics.panel.fold_stability(table, metric, by=('Model',), fold_column='Folds')#
Mean and standard deviation of a fold-level score across backtest folds.
The M5 winner chose between candidate ensembles on both: a model that is slightly better on average but much less stable across origins is a worse bet.
- Parameters:
- Return type:
- Returns:
mean,stdandn_foldsper group, sorted by mean.
- twiga.core.metrics.panel.hierarchical_quantile_scores(level_quantiles, history, tags, levels, id_column, target_column, *, level_name_column='level', key_column='series', date_column='timestamp', actual_column='actual', level_column='q_level', quantile_column='quantile_forecast', group_columns=('model',), fold_column='Folds', seasonality=1, value_column=None, weight_window=None)#
Weighted scaled pinball loss (WSPL) at every level of a hierarchy: the M5 Uncertainty score.
Quantiles do not add up: the 95 % quantile of a store’s sales is not the sum of its products’ 95 % quantiles. So, unlike
hierarchical_scores(), each level is scored on its own quantile forecasts (e.g. a model run on the panel aggregated withtwiga.hierarchy.aggregate_levels), against the history summed to that level. Within a level, each series’ scaled pinball loss is weighted by its share of value_column over the weight_window periods before the fold’s origin; the M5 Uncertainty score is the mean ofwsplover the levels (Makridakis, Spiliotis and Assimakopoulos, 2022).- Parameters:
level_quantiles (
DataFrame) – Long quantile forecasts of every level: level_name_column names the level and key_column the group astwiga.hierarchy.level_keynames it ("Total","CA_1/FOODS_1", or the bottom id), with actual_column, level_column and quantile_column.history (
DataFrame) – Bottom-level long history; value_column must be summable.tags (
DataFrame) – One row per bottom series: id_column plus the hierarchy columns.levels (
Mapping[str,Sequence[str]]) – Level name to the columns that define it; empty for the grand total.id_column (
str) – Bottom-level series identifier of history and tags.target_column (
str) – Target column in history.level_name_column (
str) – Level-name column of level_quantiles.key_column (
str) – Group-key column of level_quantiles.date_column (
str) – Timestamp column.actual_column (
str) – Observed values in level_quantiles.level_column (
str) – Quantile-level column.quantile_column (
str) – Predicted quantile column.group_columns (
Sequence[str]) – Columns that identify a forecast set (model, …).fold_column (
str) – Backtest fold column; scales and weights are recomputed per fold.seasonality (
int) – Naive lag of the scale.value_column (
str|None) – Summable column of history defining the weights; the target when None.weight_window (
int|None) – Periods before the origin summed for the weights; all history when None.
- Return type:
- Returns:
One row per forecast set, fold and level with
wsplandn_series. Levels without quantile forecasts are absent.
- twiga.core.metrics.panel.hierarchical_scores(results, history, tags, levels, id_column, target_column, *, date_column='timestamp', actual_column='Actual', forecast_column='forecast', group_columns=('Model',), fold_column='Folds', seasonality=1, value_column=None, weight_window=None)#
WRMSSE at every level of a hierarchy, from bottom-level forecasts summed upwards.
- Parameters:
results (
DataFrame) – Bottom-level long results frame (panel mode).history (
DataFrame) – Bottom-level long history; value_column (if given) must be summable, e.g. revenue rather than price.tags (
DataFrame) – One row per bottom series: id_column plus the hierarchy columns.levels (
Mapping[str,Sequence[str]]) – Level name -> columns to aggregate by, e.g.{"total": [], "store": ["store_id"], "item": ["item_id"], "item-store": ["id"]}. An empty list is the grand total.id_column (
str) – Series identifier column of results, history and tags.target_column (
str) – Target column in history.date_column (
str) – Timestamp column.actual_column (
str) – Observed values in results.forecast_column (
str) – Point forecasts in results.group_columns (
Sequence[str]) – Columns that identify a forecast set (model, …).fold_column (
str) – Backtest fold column; scales and weights are recomputed per fold.seasonality (
int) – Naive lag for the scales.value_column (
str|None) – Summable column of history defining the weights (e.g. revenue); defaults to target_column.weight_window (
int|None) – Periods before the origin summed for the weights; None uses all.
- Return type:
- Returns:
One row per forecast set, fold and level with
wrmsseandn_series. The M5 score is the mean ofwrmsseover levels.
- twiga.core.metrics.panel.naive_scale(history, id_column, target_column, date_column='timestamp', seasonality=1, squared=True, skip_leading_zeros=True)#
In-sample error of the (seasonal) naive forecast for every series.
- Parameters:
history (
DataFrame) – Long-format training history.id_column (
str) – Series identifier column.target_column (
str) – Target column.date_column (
str) – Timestamp column.seasonality (
int) – Naive lag (1 = last value; 12 = same month last year on monthly data).squared (
bool) – True gives the root mean squared naive error (RMSSE denominator); False the mean absolute naive error (MASE and SPL denominator).skip_leading_zeros (
bool) – Start each series at its first non-zero value, as M5 does, so the months before a product launched do not shrink the scale.
- Return type:
- Returns:
Scale per series id. Series with fewer than
seasonality + 1usable values are NaN; a constant series has scale 0.
- twiga.core.metrics.panel.panel_point_scores(results, history, id_column, target_column, *, date_column='timestamp', actual_column='Actual', forecast_column='forecast', group_columns=('Model',), fold_column='Folds', seasonality=1, value_column=None, weight_window=None, exclude_column=None)#
Per-series MAE, RMSE, MASE and RMSSE, with the series’ value weight.
- Parameters:
results (
DataFrame) – Long results frame (panel mode) with the id, timestamp,actual_columnandforecast_columncolumns, plus anygroup_columns(atargetcolumn is added to them when present).history (
DataFrame) – Long-format history covering the period before every forecast origin.id_column (
str) – Series identifier column (in both frames).target_column (
str) – Target column in history.date_column (
str) – Timestamp column (in both frames).actual_column (
str) – Observed values in results.forecast_column (
str) – Point forecasts in results.group_columns (
Sequence[str]) – Columns that identify a forecast set (model, …).fold_column (
str) – Backtest fold column; scales and weights are recomputed per fold.seasonality (
int) – Naive lag for the scales.value_column (
str|None) – Column of history defining the weights (e.g. revenue); defaults to target_column.weight_window (
int|None) – Periods before the origin summed for the weights (M5: the last 28 days); None uses the whole history before the origin.exclude_column (
str|None) – Column of results that is truthy for steps left out of the errors, e.g. censored demand (a stockout: sales understate demand). The fold’s origin, and so the scales and weights, still come from every step.
- Return type:
- Returns:
One row per forecast set, fold and series with
me(mean error, forecast minus actual),mae,rmse,mase,rmsse,scaled_bias(me/ naive MAE) andweight. Series whose naive scale is zero or undefined (constant or too-short history) are dropped, as in M5.
- twiga.core.metrics.panel.panel_quantile_scores(results, history, id_column, target_column, *, date_column='timestamp', actual_column='actual', level_column='q_level', quantile_column='quantile_forecast', group_columns=('model',), fold_column='Folds', seasonality=1, value_column=None, weight_window=None, exclude_column=None)#
Per-series scaled pinball loss (SPL), the M5 Uncertainty score.
The pinball loss averaged over the horizon and quantile levels, divided by the series’ mean absolute naive error.
- Parameters:
results (
DataFrame) – Long quantile frame, one row per series, timestamp and quantile level, e.g.ForecastResult.to_dataframe(fmt="long")in panel mode.history (
DataFrame) – Long-format history covering the period before every forecast origin.id_column (
str) – Series identifier column (in both frames).target_column (
str) – Target column in history.date_column (
str) – Timestamp column (in both frames).fold_column (
str) – Backtest fold column; scales and weights are recomputed per fold.seasonality (
int) – Naive lag for the scale.value_column (
str|None) – Column of history defining the weights; defaults to target_column.weight_window (
int|None) – Periods before the origin summed for the weights; None uses all.actual_column (
str) – Observed values.level_column (
str) – Quantile level column (e.g. 0.05 … 0.95).quantile_column (
str) – Predicted quantile column.group_columns (
Sequence[str]) – Columns that identify a forecast set.exclude_column (
str|None) – Column of results that is truthy for steps left out of the loss (e.g. censored demand); scales and weights still use the fold’s full origin.
- Return type:
- Returns:
One row per forecast set, fold and series with
pinball,splandweight.
- twiga.core.metrics.panel.series_weights(history, id_column, value_column, date_column='timestamp', window=None)#
Relative weight of every series: its share of total value over the last
windowperiods.- Parameters:
history (
DataFrame) – Long-format training history.id_column (
str) – Series identifier column.value_column (
str) – Column to sum, e.g. unit sales, or sales x price for revenue weights.date_column (
str) – Timestamp column.window (
int|None) – Number of most recent distinct timestamps to sum over; None uses all history.
- Return type:
- Returns:
Weights per series id, summing to 1.
- twiga.core.metrics.panel.weighted_panel_score(scores, metric='rmsse', by=('Model', 'Folds'), weight_column='weight')#
Value-weighted mean of a per-series score (e.g. WRMSSE from
rmsse).Weights are renormalised within each group, so series dropped for an undefined scale do not deflate the score.
- Parameters:
scores (
DataFrame) – Output ofpanel_point_scores()orpanel_quantile_scores().metric (
str) – Per-series score column to average.weight_column (
str) – Weight column.
- Return type:
- Returns:
Weighted score per group.
- twiga.core.metrics.evaluate_forecast(result, **kwargs)#
Dispatch forecast evaluation to the kind-appropriate function.
This is the single entry point for evaluation so callers do not need to know or import individual evaluate functions. The correct function is selected from an internal dispatch table keyed on
kind.- Parameters:
result (
ForecastResult) – AForecastResultwhoseground_truthfield is set. Thekindattribute determines which evaluate function is called.**kwargs (
object) – Extra keyword arguments forwarded to the underlying evaluate function (e.g.alphafor interval or quantile forecasts).
- Return type:
- Returns:
DataFrame of per-day, per-target metrics indexed by daily timestamp.
- Raises:
ValueError – If
result.ground_truthisNoneorresult.kindis not one of the supported values.
Examples
>>> result = ForecastResult(kind=ForecastKind.POINT, ground_truth=y, ...) >>> metrics_df = evaluate_forecast(result)
- twiga.core.metrics.point.evaluate_point_forecast(result, metric_names=None, axis=1)#
Evaluate point forecasts by computing daily pointwise metrics.
- Parameters:
result (
ForecastResult) –ForecastResultwithground_truthset,kind=ForecastKind.POINT.metric_names (
list[str] |None) – Metric names to compute. WhenNoneall supported point metrics are computed.axis (
int|None) – Axis along which to compute aggregate metrics. If None, metrics that require an axis will use their default behavior.
- Return type:
- Returns:
DataFrame of per-day, per-target metrics indexed by daily timestamp.
- twiga.core.metrics.point.r_squared(y, y_hat, axis=0)#
Calculate the Coefficient of Determination (R²) between actual and predicted values.
R² measures the proportion of variance in the actual values explained by the predictions: [ R^2 = 1 - frac{sum_{t=1}^{T} (y_t - hat{y}_t)^2}{sum_{t=1}^{T} (y_t - bar{y})^2} ] where \(\bar{y}\) is the mean of the actual values. A perfect forecast yields
R² = 1; a constant mean prediction yieldsR² = 0; worse-than-mean predictions can yield negative values.- Parameters:
- Return type:
- Returns:
R² coefficient, scalar if 1D input, array if multi-dimensional. Returns NaN if the variance of y is zero.
- Raises:
ValueError – If y and y_hat have incompatible shapes.
Examples
>>> import numpy as np >>> y = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) >>> y_hat = np.array([1.1, 2.0, 2.9, 4.1, 5.0]) >>> r_squared(y, y_hat) 0.994
- twiga.core.metrics.point.rmsle(y, y_hat, axis=0)#
Calculate the Root Mean Squared Log Error (RMSLE).
The RMSLE penalises under-predictions more than over-predictions and is scale-invariant on a log scale: [ RMSLE = sqrt{frac{1}{T} sum_{t=1}^{T} bigl(log(1 + hat{y}_t) - log(1 + y_t)bigr)^2} ]
- Parameters:
- Return type:
- Returns:
RMSLE, scalar if 1D input, array if multi-dimensional.
- Raises:
ValueError – If any value in y or y_hat is negative.
Examples
>>> import numpy as np >>> y = np.array([1.0, 2.0, 3.0]) >>> y_hat = np.array([1.1, 1.9, 3.2]) >>> rmsle(y, y_hat) 0.07663...
- twiga.core.metrics.point.medae(y, y_hat, axis=0)#
Calculate the Median Absolute Error (MedAE) between actual and predicted values.
MedAE is robust to outliers, unlike MAE: [ MedAE = text{median}!left(lvert y_t - hat{y}_t rvertright) ]
- Parameters:
- Return type:
- Returns:
Median absolute error, scalar if 1D input, array if multi-dimensional.
- Raises:
ValueError – If y and y_hat have incompatible shapes.
Examples
>>> import numpy as np >>> y = np.array([1.0, 2.0, 3.0, 100.0]) >>> y_hat = np.array([1.1, 2.0, 2.9, 90.0]) >>> medae(y, y_hat) 0.1
- twiga.core.metrics.point.skill_score(y, y_hat, y_naive, metric='mae', axis=0)#
Calculate the Skill Score of a forecast relative to a naive baseline.
The Skill Score quantifies the relative improvement of a model over a naive baseline forecast: [ SS = 1 - frac{text{metric}(y, hat{y})}{text{metric}(y, y_{text{naive}})} ] A positive score indicates improvement over the baseline; 0 means no improvement; negative values indicate the model is worse than the baseline.
- Parameters:
y (
ArrayLike) – Actual values, shape (n_samples,).y_hat (
ArrayLike) – Model predictions, same shape as y.y_naive (
ArrayLike) – Naive baseline predictions (e.g. seasonal naïve, persistence), same shape as y.metric (
str) – Error metric to use for comparison. One of'mae','mse','rmse'. Defaults to'mae'.axis (
int|None) – Axis along which to compute the metric. If None, flatten arrays. Defaults to 0.
- Return type:
- Returns:
Skill score(s). Scalar if 1D input or axis=None, otherwise array.
- Raises:
ValueError – If metric is not one of the supported values.
ValueError – If the naive baseline metric is zero.
Examples
>>> import numpy as np >>> y = np.array([1.0, 2.0, 3.0, 4.0]) >>> y_hat = np.array([1.1, 2.1, 3.0, 4.0]) >>> y_naive = np.array([1.0, 1.0, 1.0, 1.0]) >>> skill_score(y, y_hat, y_naive, metric="mae") 0.9333...
- twiga.core.metrics.interval.evaluate_interval_forecast(result, alpha=0.01, true_nmpi=None, spread='nominal', nmpi_scale='range', axis=1, metric_names=None)#
Evaluate interval forecasts by computing daily point and interval metrics.
- Parameters:
result (
ForecastResult) –ForecastResultwithground_truth,lower, andupperset,kind=ForecastKind.INTERVAL.alpha (
float) – Significance level used for Winkler score and coverage computations. Must be in(0, 1). Defaults to0.01.true_nmpi (
float|None) – Override for κ — absolute spread of the target used as the CWE reference numerator. WhenNone, derived fromspread.spread (
Literal['nominal','iqr','mad','std']) – Spread measure for the CWE reference κ."iqr"(default),"mad", or"std". Seeget_interval_metrics().nmpi_scale (
Literal['range','max','mean','median']) – Denominator R for NMPI and κ/R."range"(default),"max","mean", or"median".axis (
int|None) – Axis along which to compute aggregate metrics.metric_names (
list[str] |None) – List of interval metric names to compute.
- Return type:
- Returns:
DataFrame of per-day, per-target point and interval metrics indexed by daily timestamp.
- twiga.core.metrics.interval.msis(true, lower, upper, y_train, alpha, seasonality=1, axis=None)#
Calculate the Mean Scaled Interval Score (MSIS).
[ text{MSIS} = frac{text{Mean}(W_t)}{text{NaiveMAE}_{text{seasonal}}} ]
- Parameters:
- Return type:
- Returns:
MSIS (scalar or array).
- twiga.core.metrics.quantile.evaluate_quantile_forecast(result, alpha=0.01, true_nmpi=None, metric_names=None, quantile_axis=1, axis=1)#
Evaluate quantile forecasts by computing daily point, interval, and quantile metrics.
This function loops over days and targets (using
_evaluate_loop) and aggregates point metrics (fromget_pointwise_metrics()), interval metrics (fromget_interval_metrics()), and quantile metrics (fromget_quantile_metrics()) into a single DataFrame.- Parameters:
result (
ForecastResult) –ForecastResultwithground_truth,quantiles, andquantile_levelsset, andkind=ForecastKind.QUANTILE.alpha (
float) – Significance level for interval metrics; the outermost quantile pair (first and last) is used as lower/upper bounds. Defaults to0.01.true_nmpi (
float|None) – Optional reference value for NMPI normalisation. WhenNonethe standard deviation of the true values is used.metric_names (
list[str] |None) – Point metric names to compute. IfNone, all supported point metrics are computed.quantile_axis (
int) – Axis ofquantile_predsthat holds the quantiles. IfNone, it is inferred (first axis if its length matches, otherwise second).axis (
int) – Axis or axes passed toget_quantile_metrics()(aggregation over the forecast horizon). Default is0.
- Return type:
- Returns:
DataFrame of per‑day, per‑target metrics indexed by daily timestamp. Columns include point metrics, interval metrics, and quantile metrics.
- twiga.core.metrics.quantile.wql(true, quantile_preds, quantile_levels, quantile_axis=None, axis=1, double_factor=True, normalize_by_volume=True)#
Weighted Quantile Loss (WQL), also known as the scaled pinball loss.
WQL is the GluonTS / M5 standard aggregate quantile metric:
\[\text{WQL} = \frac{2 \mathbf{1}_{\text{double}} \sum_{\tau,t} \rho_\tau(y_t, \hat{q}_{\tau,t})} {|\mathcal{T}| \cdot \sum_t |y_t| \mathbf{1}_{\text{normalize}}}\]where \(\rho_\tau\) is the pinball loss. The factor \(2\) is often included to make the loss at \(\tau=0.5\) equal to the MAE.
- Parameters:
true (
ndarray) – Ground truth values, any shape.quantile_preds (
ndarray) – Predicted quantiles. Must have one axis holding the quantiles; the remaining dimensions must matchtrue.shapeafter axis movement.quantile_levels (
ndarray|list[float] |tuple[float,...]) – Quantile levels \(\tau \in (0,1)\), strictly increasing.quantile_axis (
int|None) – Axis ofquantile_predsthat holds the quantiles. IfNone, it is inferred (first axis if its length matches, otherwise second).axis (
int) – Axis or axes along which to aggregate the WQL. The total volume (sum of \(|y|\)) is computed over this axis, and the pinball loss is summed over the same axis. IfNone, aggregate over all dimensions and return a scalar.double_factor (
bool) – Whether to multiply by \(2\) (defaultTrue).normalize_by_volume (
bool) – Whether to divide by the total absolute volume \(\sum_t |y_t|\) (defaultTrue).
- Return type:
- Returns:
If axis is
None, a scalar WQL. Otherwise, an array with the same shape astrueafter removing the dimensions specified by axis.- Raises:
ValueError – If inputs are empty, levels out of range, shape mismatch, or total observation volume is zero when normalize_by_volume is
True.
References
Alexandrov et al. (2020). GluonTS: Probabilistic and neural time series modeling in Python. Journal of Machine Learning Research, 21(116), 1–6.
- twiga.core.metrics.parametric.evaluate_parametric_forecast(result, metric_names=None)#
Evaluate parametric forecasts by computing daily point and NLL metrics.
The NLL is computed under a Normal distribution assumption when
result.log_likelihoodis absent. Pass pre-computed log-likelihood values via that attribute to override this behaviour.- Parameters:
result (
ForecastResult) –ForecastResultwithground_truthandscaleset,kind=ForecastKind.PARAMETRIC.metric_names (
list[str] |None) – List of probabilistic metric names to compute.
- Return type:
- Returns:
DataFrame of per-day, per-target point metrics and NLL indexed by daily timestamp.
- Raises:
ValueError – If any scale value is non-positive.
- twiga.core.metrics.parametric.get_probabilistic_metrics(true, loc=None, scale=None, samples=None, log_likelihood=None, metric_names=None, axis=1, family=None)#
Compute probabilistic and pointwise metrics for forecasts.
Supports two modes: - Parametric (Normal distribution): provide loc and scale. - Sample‑based (ensemble): provide samples.
If loc is provided, pointwise metrics (MAE, RMSE, etc.) are also computed via
get_pointwise_metrics().- Parameters:
true (
ArrayLike) – Ground‑truth values, any shape.loc (
ArrayLike|None) – Point predictions (e.g., mean of the predictive distribution). Required for parametric mode; if given, pointwise metrics are added.scale (
ArrayLike|None) – Predicted standard deviations (parametric Normal). Must be >0. Required for parametric mode.samples (
ArrayLike|None) – Ensemble/posterior samples, shape(n_samples, *true.shape). Required for sample‑based mode.log_likelihood (
ArrayLike|None) – Pre‑computed log‑likelihood values (optional). If provided, overrides NLL computation in parametric mode.metric_names (
list[str] |None) –List of probabilistic metric names to compute. If
None, all available metrics for the chosen mode are computed. Supported names: - Parametric:'nll','crps','dss'- Sample‑based:'crps','energy_score','variogram_score','crps_pwm','brier_score'axis (
int|None) – Axis along which to aggregate the metrics (e.g., over the forecast horizon). IfNone, aggregate over all dimensions (scalar result). Default is0.family (
str|None) – Predictive familyscalebelongs to in parametric mode.Nonereads it as a normal standard deviation;"laplace"as the Laplace dispersion b, scored with the Laplace NLL and CRPS (and the Dawid-Sebastiani score on its sd, b * sqrt(2)).
- Return type:
- Returns:
Single‑row
DataFramewith columns – - All pointwise metrics (if loc was provided) - Selected probabilistic metrics- Raises:
ValueError – If neither scale nor samples is provided, or if required inputs are missing for a mode, or if scale contains non‑positive values.
- twiga.core.metrics.parametric.dawid_sebastiani_score(true, loc, scale, axis=None)#
Compute the Dawid-Sebastiani Score (DSS) for parametric Normal forecasts.
The Dawid-Sebastiani Score under a Normal distribution is:
\[\text{DSS}(y_t, \mu_t, \sigma_t) = \frac{(y_t - \mu_t)^2}{\sigma_t^2} + 2 \log \sigma_t\]Lower values indicate better forecasts.
- Parameters:
- Return type:
- Returns:
Mean DSS as a NumPy array (scalar when
axis=None).- Raises:
ValueError – If any
scalevalue is non-positive.
References
Dawid & Sebastiani (1999). Coherent dispersion criteria for optimal experimental design. The Annals of Statistics.
- twiga.core.metrics.parametric.energy_score(true, samples, axis=None)#
Compute the Energy Score (ES) for ensemble forecasts.
The Energy Score is the multivariate generalization of CRPS:
\[\text{ES}(F, \mathbf{y}) = \mathbb{E}_F\|\mathbf{X} - \mathbf{y}\|_2 - \tfrac{1}{2} \mathbb{E}_F\|\mathbf{X} - \mathbf{X}'\|_2\]- Parameters:
- Return type:
- Returns:
Energy Score as a NumPy array (scalar when
axis=None). Lower is better.
References
Gneiting & Raftery (2007). Strictly proper scoring rules. JASA.
- twiga.core.metrics.parametric.variogram_score(true, samples, order=0.5, weights=None, axis=None)#
Compute the Variogram Score (VS) for ensemble forecasts.
The Variogram Score compares the expected pairwise differences of the ensemble against those of the observation:
\[\text{VS}_p(F, \mathbf{y}) = \sum_{i,j} w_{ij} \left( |y_i - y_j|^p - \mathbb{E}_F |X_i - X_j|^p \right)^2\]It is proper but not strictly proper, since it is invariant to a shift of the whole vector. That is the price of its purpose: it is far more sensitive to the dependence structure than the Energy Score, which responds mainly to errors in the mean and has limited power against a misspecified correlation. Report both, because a model can lose very little Energy Score while placing the dependence badly.
- Parameters:
true (
ArrayLike) – Ground-truth vector, shape(n_dim,).samples (
ArrayLike) – Ensemble samples, shape(n_samples, n_dim).order (
float) – Exponent \(p\) applied to the pairwise differences.0.5is the usual choice and is robust to heavy tails;1weights large discrepancies more.weights (
ArrayLike|None) – Optional non-negative weights \(w_{ij}\), shape(n_dim, n_dim). Uniform whenNone. Use these to down-weight pairs whose dependence is not of interest.axis (
int|None) – Axis along which to average.None(default) returns a scalar.
- Return type:
- Returns:
Variogram Score as a NumPy array (scalar when
axis=None). Lower is better.- Raises:
ValueError – If
orderis not positive, ifsamplesis not two-dimensional, or ifweightsdoes not match the target dimension.
References
Scheuerer & Hamill (2015). Variogram-based proper scoring rules for probabilistic forecasts of multivariate quantities. Monthly Weather Review, 143(4), 1321-1334.
Examples
>>> rng = np.random.default_rng(0) >>> obs = np.array([1.0, 2.0]) >>> ens = rng.normal(size=(500, 2)) >>> float(variogram_score(obs, ens)) > 0 True
- twiga.core.metrics.parametric.brier_score(true, samples, threshold, axis=None)#
Compute the Brier Score for a probabilistic binary event forecast.
The Brier Score is:
\[\text{BS} = \frac{1}{n} \sum_{t=1}^{n} \left( \hat{p}_t - o_t \right)^2\]where \(\hat{p}_t\) is the predicted probability that \(y_t \leq\) threshold.
- Parameters:
- Return type:
- Returns:
Brier Score as a NumPy array (scalar when
axis=None). Lower is better.
References
Brier, G. W. (1950). Verification of forecasts expressed in terms of probability. Monthly Weather Review.
- twiga.core.metrics.parametric.normal_nll(true, loc, scale, axis=None)#
Negative log-likelihood under a Normal predictive distribution.
The negative log-likelihood (NLL) is:
\[\text{NLL} = \frac{1}{n} \sum_{t=1}^{n} \left[ \frac{1}{2} \log(2\pi \sigma_t^2) + \frac{(y_t - \mu_t)^2}{2\sigma_t^2} \right]\]- Parameters:
- Return type:
- Returns:
Negative log-likelihood as a NumPy array (scalar when
axis=None).- Raises:
ValueError – If any
scalevalue is non-positive.
Examples
>>> import numpy as np >>> normal_nll(np.array([1.0, 2.0]), np.array([1.1, 1.9]), np.array([0.5, 0.5])) array(1.0439...)
- twiga.core.metrics.parametric.normal_crps(true, loc, scale, axis=None)#
Analytical Continuous Ranked Probability Score (CRPS) for a Normal predictive distribution.
The closed-form CRPS under \(\mathcal{N}(\mu, \sigma^2)\) is:
\[\text{CRPS}(\mathcal{N}(\mu, \sigma^2), y) = \sigma \left[ z \left( 2\Phi(z) - 1 \right) + 2\phi(z) - \frac{1}{\sqrt{\pi}} \right]\]where \(z = \frac{y - \mu}{\sigma}\), and \(\Phi\), \(\phi\) are the standard Normal CDF and PDF respectively.
- Parameters:
- Return type:
- Returns:
Mean CRPS as a NumPy array (scalar when
axis=None). Lower is better.- Raises:
ValueError – If any
scalevalue is non-positive.
References
Gneiting, T., Raftery, A. E., Westveld, A. H., & Goldman, T. (2005). Calibrated probabilistic forecasting using ensemble model output statistics and minimum CRPS estimation. Monthly Weather Review, 133(5), 1098–1118.
Examples
>>> import numpy as np >>> normal_crps(np.array([1.0, 2.0]), np.array([1.0, 2.0]), np.array([1.0, 1.0])) array(0.5641...) # σ / sqrt(π) for perfect calibration
- twiga.core.metrics.parametric.laplace_nll(true, loc, scale, axis=None)#
Negative log-likelihood under a Laplace predictive distribution.
The negative log-likelihood (NLL) is:
\[\text{NLL} = \frac{1}{n} \sum_{t=1}^{n} \left[ \log(2b_t) + \frac{|y_t - \mu_t|}{b_t} \right]\]where \(b_t\) is the scale parameter of the Laplace distribution.
- Parameters:
- Return type:
- Returns:
Mean NLL as a NumPy array (scalar when
axis=None).- Raises:
ValueError – If any
scalevalue is non-positive.
Examples
>>> import numpy as np >>> laplace_nll(np.array([1.0, 2.0]), np.array([1.1, 1.9]), np.array([0.5, 0.5])) array(1.1931...)
- twiga.core.metrics.stats.diebold_mariano_test(p_real, p_pred_1, p_pred_2, norm=1, version='multivariate', h=1)#
One-sided Diebold-Mariano (DM) test for equal predictive accuracy.
Tests the null hypothesis H0 that forecast
p_pred_1has equal or lower loss than forecastp_pred_2against the one-sided alternative H1 thatp_pred_2is strictly more accurate. Rejecting H0 (small p-value) meansp_pred_2is significantly more accurate.The loss differential is \(d_t = g(e_{1,t}) - g(e_{2,t})\) where \(g(e) = |e|^{norm}\) (
norm=1→ MAE-based,norm=2→ MSE-based). The test statistic\[DM = \frac{\bar{d}}{\sqrt{\hat{\sigma}^2_{\text{HAC}} / T}} \xrightarrow{d} \mathcal{N}(0,1)\]uses a Bartlett HAC variance estimator with bandwidth
h - 1to account for serial correlation in multi-step forecasts.- Parameters:
p_real (
ndarray) – Observed values, shape(n_days, n_steps)or(T,).p_pred_1 (
ndarray) – Forecast 1 predictions, same shape asp_real.p_pred_2 (
ndarray) – Forecast 2 predictions, same shape asp_real.norm (
int) – Loss function norm.1for MAE-based,2for MSE-based. Defaults to1.version (
str) –Test variant.
"multivariate": single test on the mean loss differential averaged across steps (default)."univariate": independent test per time step (column), returns an array of p-values of lengthn_steps.
h (
int) – Forecast horizon used to set the HAC bandwidth. Set to the number of steps ahead you are forecasting. Defaults to1.
- Return type:
- Returns:
One-sided p-value (float) for
"multivariate", or 1-D array of p-values (one per step) for"univariate".- Raises:
ValueError – If input shapes do not match.
ValueError – If
versionis not"univariate"or"multivariate".ValueError – If
normis not 1 or 2.
References
Diebold, F. X., & Mariano, R. S. (1995). Comparing predictive accuracy. Journal of Business & Economic Statistics, 13(3), 253–263.
Examples
>>> import numpy as np >>> rng = np.random.default_rng(0) >>> real = rng.normal(0, 1, (100, 24)) >>> pred1 = real + rng.normal(0, 0.5, real.shape) >>> pred2 = real + rng.normal(0, 0.3, real.shape) # better forecast >>> p = diebold_mariano_test(real, pred1, pred2, version="multivariate")
- twiga.core.metrics.stats.giacomini_white_test(p_real, p_pred_1, p_pred_2, norm=1, version='multivariate')#
One-sided Giacomini-White (GW) test for Conditional Predictive Accuracy (CPA).
Tests the null hypothesis H0 that forecast
p_pred_1has equal or lower conditional expected loss thanp_pred_2against the one-sided alternative H1 thatp_pred_2is conditionally more accurate. Rejecting H0 (small p-value) meansp_pred_2is significantly more accurate given the available information set.Unlike DM, the GW test conditions on lagged loss differentials, making it more powerful when one model systematically exploits information that the other misses. The instruments are \(h_t = [1, d_{t-1}]\), and the test statistic is \(T \cdot R^2 \sim \chi^2(q)\) where \(q\) is the number of instruments.
- Parameters:
p_real (
ndarray) – Observed values, shape(n_days, n_steps)or(T,).p_pred_1 (
ndarray) – Forecast 1 predictions, same shape asp_real.p_pred_2 (
ndarray) – Forecast 2 predictions, same shape asp_real.norm (
int) – Loss function norm.1for MAE-based,2for MSE-based. Defaults to1.version (
str) –Test variant.
"multivariate": single test on the mean loss differential averaged across steps (default)."univariate": independent test per time step, returns an array of p-values of lengthn_steps.
- Return type:
- Returns:
One-sided p-value (float) for
"multivariate", or 1-D array of p-values (one per step) for"univariate".- Raises:
ValueError – If input shapes do not match or are 1-D (use
(T, 1)for a single-step series).ValueError – If
versionis not"univariate"or"multivariate".ValueError – If
normis not 1 or 2.
References
Giacomini, R., & White, H. (2006). Tests of conditional predictive ability. Econometrica, 74(6), 1545–1578.
Examples
>>> import numpy as np >>> rng = np.random.default_rng(0) >>> real = rng.normal(0, 1, (100, 24)) >>> pred1 = real + rng.normal(0, 0.5, real.shape) >>> pred2 = real + rng.normal(0, 0.3, real.shape) # better forecast >>> p = giacomini_white_test(real, pred1, pred2, version="multivariate")