Forecast validation currently accepts data where the observed value differs across rows of the same forecast unit. Such data is invalid (a single target can only have one observed value), but it passes as_forecast_quantile(), as_forecast_sample(), as_forecast_nominal(), as_forecast_ordinal() and as_forecast_multivariate_sample() without complaint, and score() then silently produces multiple, wrong score rows for the single forecast unit.
Reproduction (actual output)
library(scoringutils)
library(data.table)
fc <- as_forecast_quantile(data.table(
model = "m1", target = "t1",
quantile_level = c(0.25, 0.5, 0.75),
predicted = 1:3,
observed = c(10, 10, 20) # conflicting observed values, same forecast unit
))
# validates without error; get_duplicate_forecasts(fc) returns 0 rows
score(fc)
#> model target wis overprediction underprediction dispersion ae_median
#> <char> <char> <num> <num> <num> <num> <num>
#> 1: m1 t1 7.666667 0 7.000000 0.6666667 8
#> 2: m1 t1 17.666667 0 17.000000 0.6666667 18
Two score rows for one forecast. The same happens for sample forecasts (observed = c(5, 5, 5, 7) across sample_id 1:4 gives two crps rows: 1.875 and 3.875) and nominal/ordinal forecasts (conflicting observed factor across predicted_label gives two log_score rows).
Cause
- Validation in
assert_forecast_generic() (R/class-forecast.R:110-155) only runs check_duplicates(), which groups by c(forecast_unit, type_cols) where type_cols is quantile_level / sample_id / predicted_label (get_forecast_type_ids()). Rows that differ only in observed never land in the same duplicate group, so nothing is flagged.
score.forecast_quantile (R/class-forecast-quantile.R:141-147), score.forecast_sample (R/class-forecast-sample.R:137-141) and score.forecast_multivariate_sample aggregate with observed = unique(observed) per forecast unit; a 2-element unique() silently recycles the predicted list, yielding two score rows per unit.
Point, binary and multivariate-point forecasts are incidentally protected: they have no type columns, so conflicting-observed rows are caught as duplicate forecasts.
Intended fix
Per maintainer decision: a hard error (not a warning) on conflicting observed values within a forecast unit, with wording mirroring the duplicate-forecasts message and pointing users at the forecast unit. Implemented as a new internal check check_observed_constant() called from assert_forecast_generic(), so as_forecast_*(), assert_forecast(), score() and [.forecast all catch it. Rows with NA observed are ignored by the check, and multivariate forecasts need no special-casing (the forecast unit is the univariate unit, within which observed is legitimately constant).
Part of the bug audit in #1189.
Forecast validation currently accepts data where the
observedvalue differs across rows of the same forecast unit. Such data is invalid (a single target can only have one observed value), but it passesas_forecast_quantile(),as_forecast_sample(),as_forecast_nominal(),as_forecast_ordinal()andas_forecast_multivariate_sample()without complaint, andscore()then silently produces multiple, wrong score rows for the single forecast unit.Reproduction (actual output)
Two score rows for one forecast. The same happens for sample forecasts (
observed = c(5, 5, 5, 7)acrosssample_id1:4 gives twocrpsrows: 1.875 and 3.875) and nominal/ordinal forecasts (conflicting observed factor acrosspredicted_labelgives twolog_scorerows).Cause
assert_forecast_generic()(R/class-forecast.R:110-155) only runscheck_duplicates(), which groups byc(forecast_unit, type_cols)wheretype_colsisquantile_level/sample_id/predicted_label(get_forecast_type_ids()). Rows that differ only inobservednever land in the same duplicate group, so nothing is flagged.score.forecast_quantile(R/class-forecast-quantile.R:141-147),score.forecast_sample(R/class-forecast-sample.R:137-141) andscore.forecast_multivariate_sampleaggregate withobserved = unique(observed)per forecast unit; a 2-elementunique()silently recycles the predicted list, yielding two score rows per unit.Point, binary and multivariate-point forecasts are incidentally protected: they have no type columns, so conflicting-observed rows are caught as duplicate forecasts.
Intended fix
Per maintainer decision: a hard error (not a warning) on conflicting observed values within a forecast unit, with wording mirroring the duplicate-forecasts message and pointing users at the forecast unit. Implemented as a new internal check
check_observed_constant()called fromassert_forecast_generic(), soas_forecast_*(),assert_forecast(),score()and[.forecastall catch it. Rows withNAobserved are ignored by the check, and multivariate forecasts need no special-casing (the forecast unit is the univariate unit, within which observed is legitimately constant).Part of the bug audit in #1189.