Formatting
Here is a forecast that was created by epidemiologists in Denmark in 2020.
str(denmark2020ens)
#> List of 500
#> $ : num [1:150] 7.42 7.39 7.37 7.36 7.34 ...
#> $ : num [1:150] 6.87 6.82 6.79 6.76 6.74 ...
#> $ : num [1:150] 6.12 5.88 5.64 5.41 5.2 ...
#> $ : num [1:150] 6.82 6.74 6.66 6.59 6.53 ...
#> $ : num [1:150] 6.63 6.42 6.22 6.03 5.84 ...
#> $ : num [1:150] 6.7 6.62 6.55 6.48 6.42 ...
#> $ : num [1:150] 5.89 5.69 5.49 5.31 5.13 ...
#> $ : num [1:150] 5.93 5.73 5.53 5.35 5.17 ...
#> $ : num [1:150] 6.23 6.07 5.93 5.79 5.65 ...
#> $ : num [1:150] 6.69 6.55 6.43 6.31 6.19 ...
#> $ : num [1:150] 5.89 5.64 5.41 5.18 4.96 ...
#> $ : num [1:150] 6.54 6.46 6.4 6.34 6.29 ...
#> $ : num [1:150] 6.78 6.73 6.69 6.65 6.62 ...
#> $ : num [1:150] 6 5.75 5.52 5.3 5.09 ...
#> $ : num [1:150] 6.68 6.57 6.46 6.37 6.27 ...
#> $ : num [1:150] 7.05 6.85 6.66 6.47 6.29 ...
#> $ : num [1:150] 6.84 6.77 6.71 6.66 6.61 ...
#> $ : num [1:150] 5.6 5.43 5.28 5.14 5.01 ...
#> $ : num [1:150] 6.18 6.05 5.93 5.81 5.7 ...
#> $ : num [1:150] 6.24 6.17 6.1 6.04 5.99 ...
#> $ : num [1:150] 5.54 5.3 5.06 4.84 4.62 ...
#> $ : num [1:150] 7.3 7.31 7.32 7.34 7.37 ...
#> $ : num [1:150] 7.25 7.13 7.01 6.9 6.79 ...
#> $ : num [1:150] 7.24 7.22 7.22 7.22 7.22 ...
#> $ : num [1:150] 6.3 6.05 5.8 5.56 5.34 ...
#> $ : num [1:150] 7.22 7.05 6.88 6.72 6.56 ...
#> $ : num [1:150] 7.11 6.94 6.77 6.61 6.45 ...
#> $ : num [1:150] 6.71 6.64 6.57 6.52 6.47 ...
#> $ : num [1:150] 7.18 7.15 7.12 7.1 7.07 ...
#> $ : num [1:150] 6.62 6.56 6.51 6.47 6.43 ...
#> $ : num [1:150] 6.37 6.23 6.11 5.99 5.87 ...
#> $ : num [1:150] 6.73 6.59 6.46 6.33 6.21 ...
#> $ : num [1:150] 6.25 6.11 5.98 5.86 5.75 ...
#> $ : num [1:150] 7.35 7.39 7.43 7.48 7.53 ...
#> $ : num [1:150] 6.37 6.14 5.91 5.69 5.48 ...
#> $ : num [1:150] 7.01 6.83 6.65 6.48 6.31 ...
#> $ : num [1:150] 6.36 6.24 6.14 6.04 5.95 ...
#> $ : num [1:150] 6.9 6.81 6.71 6.63 6.54 ...
#> $ : num [1:150] 6.56 6.43 6.3 6.18 6.07 ...
#> $ : num [1:150] 7.03 6.85 6.68 6.51 6.35 ...
#> $ : num [1:150] 7 6.94 6.89 6.84 6.8 ...
#> $ : num [1:150] 7.45 7.39 7.34 7.29 7.24 ...
#> $ : num [1:150] 6.58 6.44 6.31 6.18 6.07 ...
#> $ : num [1:150] 6.27 6.15 6.03 5.91 5.8 ...
#> $ : num [1:150] 6.47 6.32 6.18 6.04 5.92 ...
#> $ : num [1:150] 6.73 6.53 6.34 6.15 5.96 ...
#> $ : num [1:150] 6.38 6.25 6.14 6.04 5.94 ...
#> $ : num [1:150] 5.92 5.67 5.43 5.2 4.97 ...
#> $ : num [1:150] 6.62 6.53 6.44 6.35 6.27 ...
#> $ : num [1:150] 6.19 6 5.82 5.66 5.5 ...
#> $ : num [1:150] 6.87 6.82 6.77 6.73 6.69 ...
#> $ : num [1:150] 6.3 6.13 5.97 5.82 5.68 ...
#> $ : num [1:150] 5.8 5.58 5.37 5.17 4.98 ...
#> $ : num [1:150] 7.36 7.28 7.2 7.13 7.07 ...
#> $ : num [1:150] 6.07 5.85 5.63 5.43 5.22 ...
#> $ : num [1:150] 6.31 6.16 6.01 5.88 5.75 ...
#> $ : num [1:150] 5.84 5.65 5.47 5.3 5.13 ...
#> $ : num [1:150] 6.97 6.8 6.63 6.46 6.3 ...
#> $ : num [1:150] 6.96 6.84 6.73 6.62 6.51 ...
#> $ : num [1:150] 6.47 6.47 6.47 6.49 6.51 ...
#> $ : num [1:150] 6.7 6.61 6.53 6.46 6.39 ...
#> $ : num [1:150] 6.75 6.68 6.61 6.54 6.48 ...
#> $ : num [1:150] 6.83 6.75 6.68 6.62 6.55 ...
#> $ : num [1:150] 6.42 6.29 6.17 6.06 5.95 ...
#> $ : num [1:150] 7.03 7.02 7.02 7.03 7.05 ...
#> $ : num [1:150] 6.16 5.94 5.73 5.54 5.35 ...
#> $ : num [1:150] 7.08 6.97 6.87 6.77 6.67 ...
#> $ : num [1:150] 5.9 5.71 5.53 5.35 5.19 ...
#> $ : num [1:150] 6.17 6 5.84 5.69 5.55 ...
#> $ : num [1:150] 6.75 6.54 6.33 6.13 5.94 ...
#> $ : num [1:150] 6.88 6.83 6.79 6.75 6.73 ...
#> $ : num [1:150] 6.47 6.3 6.15 6 5.86 ...
#> $ : num [1:150] 7.01 6.97 6.93 6.9 6.88 ...
#> $ : num [1:150] 6.13 6.08 6.05 6.02 6.01 ...
#> $ : num [1:150] 6.29 6.12 5.96 5.8 5.66 ...
#> $ : num [1:150] 6.31 6.11 5.91 5.73 5.55 ...
#> $ : num [1:150] 7.18 7.15 7.13 7.11 7.1 ...
#> $ : num [1:150] 6.14 6.04 5.95 5.87 5.79 ...
#> $ : num [1:150] 7.82 7.67 7.51 7.37 7.22 ...
#> $ : num [1:150] 6.92 6.78 6.64 6.52 6.4 ...
#> $ : num [1:150] 5.64 5.51 5.4 5.29 5.19 ...
#> $ : num [1:150] 7.73 7.62 7.51 7.4 7.29 ...
#> $ : num [1:150] 7.28 7.22 7.16 7.11 7.05 ...
#> $ : num [1:150] 6.29 6.09 5.88 5.69 5.5 ...
#> $ : num [1:150] 7.13 7.14 7.15 7.18 7.2 ...
#> $ : num [1:150] 6.28 6.07 5.87 5.68 5.5 ...
#> $ : num [1:150] 6.69 6.58 6.48 6.38 6.3 ...
#> $ : num [1:150] 7.74 7.54 7.35 7.16 6.97 ...
#> $ : num [1:150] 6.53 6.35 6.17 6.01 5.85 ...
#> $ : num [1:150] 6.28 6.1 5.93 5.77 5.62 ...
#> $ : num [1:150] 5.52 5.33 5.15 4.98 4.82 ...
#> $ : num [1:150] 6.22 6.13 6.04 5.97 5.9 ...
#> $ : num [1:150] 5.48 5.32 5.18 5.05 4.92 ...
#> $ : num [1:150] 5.48 5.3 5.13 4.97 4.82 ...
#> $ : num [1:150] 6.55 6.39 6.23 6.08 5.93 ...
#> $ : num [1:150] 7.24 7.19 7.16 7.13 7.1 ...
#> $ : num [1:150] 6.76 6.65 6.54 6.43 6.33 ...
#> $ : num [1:150] 7.34 7.34 7.35 7.37 7.39 ...
#> $ : num [1:150] 7.03 7.06 7.1 7.14 7.2 ...
#> [list output truncated]
It is an ensemble of 500 different realizations of a simulation, with 150 time points each.
To use this data with casteval we need to provide time points.
library(lubridate)
#>
#> Attaching package: 'lubridate'
#> The following objects are masked from 'package:base':
#>
#> date, intersect, setdiff, union
times <- seq(ymd("2020-05-05"), ymd("2020-10-01"), by="days")
Now we can create a forecast object.
fc <- create_forecast(
list(time=times, vals=denmark2020ens),
name="Denmark 2020 forecast",
forecast_time=ymd("2020-05-05")
)
fc
#> $name
#> [1] "Denmark 2020 forecast"
#>
#> $forecast_time
#> [1] "2020-05-05"
#>
#> $data
#> # A tibble: 75,000 × 3
#> time sim val
#> <date> <int> <dbl>
#> 1 2020-05-05 1 7.42
#> 2 2020-05-06 1 7.39
#> 3 2020-05-07 1 7.37
#> 4 2020-05-08 1 7.36
#> 5 2020-05-09 1 7.34
#> 6 2020-05-10 1 7.33
#> 7 2020-05-11 1 7.31
#> 8 2020-05-12 1 7.30
#> 9 2020-05-13 1 7.29
#> 10 2020-05-14 1 7.28
#> # ℹ 74,990 more rows
Here is a made-up set of observations over roughly the same time period.
obs <- denmark2020obs
obs
#> # A tibble: 204 × 2
#> time val_obs
#> <date> <dbl>
#> 1 2020-03-12 23
#> 2 2020-03-13 29
#> 3 2020-03-14 31
#> 4 2020-03-15 23
#> 5 2020-03-16 27
#> 6 2020-03-17 20
#> 7 2020-03-18 11
#> 8 2020-03-19 17
#> 9 2020-03-20 22
#> 10 2020-03-21 11
#> # ℹ 194 more rows
These simply consist of a time
column and a
val_obs
column.
Visualizing
Let’s visualize the forecast and the observations.
plot_forecast(fc, obs)
Since there are so many realizations, the observations are kind of hard to see. We can tweak the opacity and colors using the modular plotting functions.
NULL |> plot_ensemble(fc, alpha=0.05) |> plot_observations(obs, colour="green")
Scoring
We can pass the data to any scoring function we like.
bias(fc, obs)
#> [1] -0.8089333
log_score(fc, obs, at=ymd("2020-10-01"))
#> [1] -5.570019
crps(fc, obs, after=60)
#> [1] 63.48468
pairs <- list(c(.5,99.5), c(5,95), c(25,75))
accuracy(fc, obs, quant_pairs=pairs)
#> Scoring accuracy using quantile pairs c(0.5, 99.5), c(5, 95), c(25, 75)
#> [1] 0.6066667 0.4000000 0.1866667
The default behaviour is to summarize the data into a single score.
To get the unsummarized scores, we pass
summarize=FALSE
.
bias(fc, obs, summarize=FALSE)
#> # A tibble: 150 × 3
#> time val_obs score
#> <date> <dbl> <dbl>
#> 1 2020-05-05 48 -1
#> 2 2020-05-06 44 -1
#> 3 2020-05-07 47 -1
#> 4 2020-05-08 64 -1
#> 5 2020-05-09 60 -1
#> 6 2020-05-10 52 -1
#> 7 2020-05-11 63 -1
#> 8 2020-05-12 48 -1
#> 9 2020-05-13 44 -1
#> 10 2020-05-14 56 -1
#> # ℹ 140 more rows
crps(fc, obs, after=60, summarize=FALSE)
#> # A tibble: 150 × 3
#> time score val_obs
#> <date> <dbl> <dbl>
#> 1 2020-05-05 41.1 48
#> 2 2020-05-06 37.2 44
#> 3 2020-05-07 40.3 47
#> 4 2020-05-08 57.4 64
#> 5 2020-05-09 53.4 60
#> 6 2020-05-10 45.5 52
#> 7 2020-05-11 56.6 63
#> 8 2020-05-12 41.7 48
#> 9 2020-05-13 37.7 44
#> 10 2020-05-14 49.8 56
#> # ℹ 140 more rows
log_score(fc, obs, at=ymd("2020-10-01"), summarize=FALSE)
#> # A tibble: 150 × 3
#> time score val_obs
#> <date> <dbl> <dbl>
#> 1 2020-05-05 -Inf 48
#> 2 2020-05-06 -Inf 44
#> 3 2020-05-07 -Inf 47
#> 4 2020-05-08 -Inf 64
#> 5 2020-05-09 -Inf 60
#> 6 2020-05-10 -Inf 52
#> 7 2020-05-11 -Inf 63
#> 8 2020-05-12 -Inf 48
#> 9 2020-05-13 -Inf 44
#> 10 2020-05-14 -Inf 56
#> # ℹ 140 more rows
accuracy(fc, obs, quant_pairs=pairs, summarize=FALSE)
#> Scoring accuracy using quantile pairs c(0.5, 99.5), c(5, 95), c(25, 75)
#> # A tibble: 450 × 4
#> time val_obs score pair
#> <date> <dbl> <lgl> <int>
#> 1 2020-05-05 48 FALSE 1
#> 2 2020-05-06 44 FALSE 1
#> 3 2020-05-07 47 FALSE 1
#> 4 2020-05-08 64 FALSE 1
#> 5 2020-05-09 60 FALSE 1
#> 6 2020-05-10 52 FALSE 1
#> 7 2020-05-11 63 FALSE 1
#> 8 2020-05-12 48 FALSE 1
#> 9 2020-05-13 44 FALSE 1
#> 10 2020-05-14 56 FALSE 1
#> # ℹ 440 more rows
Note how for accuracy()
, the score column is a boolean,
not a number.
Custom scoring
Let’s say we want to modify accuracy()
so that it
returns the rate at which numbers fall outside a quantile
interval. We can define our own scoring function to wrap
accuracy()
, and then use it like any other.
accuracy_inverse <- function(fcst, obs, summarize=TRUE, quant_pairs=NULL) {
scores <- accuracy(fcst, obs, summarize=summarize, quant_pairs=quant_pairs)
if(summarize) {
1 - scores
} else {
scores |> dplyr::mutate(score=!score)
}
}
accuracy_inverse(fc, obs, quant_pairs=pairs)
#> Scoring accuracy using quantile pairs c(0.5, 99.5), c(5, 95), c(25, 75)
#> [1] 0.3933333 0.6000000 0.8133333
accuracy_inverse(fc, obs, quant_pairs=pairs, summarize=FALSE)
#> Scoring accuracy using quantile pairs c(0.5, 99.5), c(5, 95), c(25, 75)
#> # A tibble: 450 × 4
#> time val_obs score pair
#> <date> <dbl> <lgl> <int>
#> 1 2020-05-05 48 TRUE 1
#> 2 2020-05-06 44 TRUE 1
#> 3 2020-05-07 47 TRUE 1
#> 4 2020-05-08 64 TRUE 1
#> 5 2020-05-09 60 TRUE 1
#> 6 2020-05-10 52 TRUE 1
#> 7 2020-05-11 63 TRUE 1
#> 8 2020-05-12 48 TRUE 1
#> 9 2020-05-13 44 TRUE 1
#> 10 2020-05-14 56 TRUE 1
#> # ℹ 440 more rows
Visualizing scores
We can visualize the output of our scoring functions alongside the forecast data and observations.
plot_forecast(fc, obs, score=bias)
plot_forecast(fc, obs, score=crps, invert_scale=TRUE)
plot_forecast(fc, obs, score=log_score)
plot_forecast(fc, obs, quant_intervals=pairs, score=accuracy, quant_pairs=c(25,75))
#> Scoring accuracy using quantile pairs c(25, 75)
Notes:
- The
invert_scale
flag lets us flip the color scale, which is useful for CRPS where lower scores are better - The grey dots correspond to scores of negative infinity, which is a common occurence with the logarithmic score
- Quantile intervals can be plotted and made to line up with
accuracy()
using thequant_intervals
parameter