Skip to content

Validation permits conflicting observed values within a forecast unit #1201

Description

@nikosbosse

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions