Skip to content

as_forecast_quantile.forecast_sample() broken for asymmetric probs #1196

Description

@nikosbosse

as_forecast_quantile.forecast_sample() produces silently wrong output or errors whenever probs is not symmetric around 0.5 (or contains duplicates).

Reproduction

Samples 1:100, observed = 50, one forecast unit:

library(scoringutils)
samples <- data.table::data.table(
  model = "m", observed = 50, predicted = 1:100, sample_id = 1:100
) |> as_forecast_sample()

# Single asymmetric prob: TWO rows come back, and the 0.6 level
# silently receives the 0.4 quantile via data.table length-1 recycling
as_forecast_quantile(samples, probs = 0.4)
#>    model observed quantile_level predicted
#> 1:     m       50            0.4      40.6
#> 2:     m       50            0.6      40.6   # should be 60.4, or not exist at all

# Multiple asymmetric probs: hard error
as_forecast_quantile(samples, probs = c(0.1, 0.2))
#> Error: Supplied 2 items for column 2 of group 1 which has 4 rows.
#> The RHS length must either be 1 ... or match the LHS length exactly.

# Duplicated probs: error from assert_forecast
as_forecast_quantile(samples, probs = c(0.5, 0.5))
#> Error: ... more than one forecast for the same target

Symmetric prob sets (including the default c(0.05, 0.25, 0.5, 0.75, 0.95)) are unaffected because the symmetrized vector then equals probs elementwise.

Cause

In R/class-forecast-sample.R (lines 110-118) the quantile_level column is built from a symmetrized vector, while quantile() is called with the raw probs:

quantile_level <- unique(round(c(probs, 1 - probs), digits = 10))
...
predicted = quantile(x = predicted, probs = ..probs, ...)

This is a refactoring regression from 4d9ecf7 (#876, Jul 2024): the predecessor sample_to_quantile() used the same symmetrized vector for both the column and the probs argument of quantile(), so it was internally consistent; the refactor updated only the quantile() call.

Intended fix

Per maintainer decision: compute quantiles at exactly the requested probs (matching the current documentation of probs), i.e. use a single quantile_level <- unique(round(probs, digits = 10)) for both the column and the quantile() call — do NOT restore the old symmetrization. For every symmetric prob set (including the default) the output is byte-identical to current behaviour. Additionally, tighten the input check to assert_numeric(probs, min.len = 1, lower = 0, upper = 1) so out-of-range probs fail with a clear checkmate message.

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