Skip to content

Forecast evaluation ​

How the forecasts on the forecasts page have scored against the data that arrived afterwards. How the outbreak-size and reproduction-number estimates moved from release to release is on the in-sample checks page. Scoring is the continuous ranked probability score against a persistence baseline, defined in the forecast scoring Methods section. The same scoring by province is on the province forecast evaluation page and by health zone on the health-zone forecast evaluation page.

Load packages, data and fitted chains
julia
# Shared setup: packages, observations and the fit registry. See
# `docs/pages/_setup.jl`.
using BVDOutbreakSize
include(joinpath(pkgdir(BVDOutbreakSize), "docs", "pages", "_setup.jl"))
validation_forecast_from (generic function with 1 method)
julia
# The fits this page reads, loaded from the cache here.
chn_joint = load_fit("joint")
frozen_lastweek = load_fit("frozen_validation")
frozen_lastweek_streams = frozen_validation_stream_fits();

Summary ​

The overall bullets come first, then each stream's own scores, from the sections below. Relative skill is the model's CRPS over the persistence baseline's, so a value below one beats the baseline. Coverage is the fraction of forecasts whose observed value falls inside the 90% predictive interval, nominally 0.9.

  • Joint model across releases: beat the baseline on 1 of 5 streams.

  • Frozen joint model: beat the baseline on 1 of 5 streams.

Across releases

  • confirmed cases: relative skill 5.24, 90% coverage 0.87, bias 0.39 over 143 forecasts.

  • confirmed deaths: relative skill 6.86, 90% coverage 0.85, bias 0.08 over 141 forecasts.

  • isolation beds: relative skill 1.67, 90% coverage 0.96, bias 0.14 over 138 forecasts.

  • onset reports: relative skill 0.91, 90% coverage 0.96, bias -0.04 over 27 forecasts.

  • recovered: relative skill 1.61, 90% coverage 1.0, bias 0.12 over 45 forecasts.

Frozen fits

  • confirmed cases: relative skill 2.39, 90% coverage 0.77, bias 0.31 over 308 forecasts.

  • confirmed deaths: relative skill 1.02, 90% coverage 0.83, bias 0.06 over 252 forecasts.

  • isolation beds: relative skill 0.87, 90% coverage 0.98, bias 0.12 over 273 forecasts.

  • onset reports: relative skill 1.03, 90% coverage 0.9, bias -0.16 over 30 forecasts.

  • recovered: relative skill 7.77, 90% coverage 0.62, bias 0.53 over 133 forecasts.

Forecast validation ​

How last week's forecast held up against the data since observed, from the frozen refit in forecast-versus-frozen evaluation. Only the streams the situation reports still update are validated, since a stopped stream repeats its last total and leaves nothing to score. The projected bed occupancy is scored against the beds held a week later, but this check is weak at a one-week-back freeze. The reported occupancy rate starts only on 9 June, so the capacity has no implied-capacity anchor and rides its random walk back to the freeze date.

Fit one week back and validate the one-week-ahead forecast
julia
# frozen_lastweek and frozen_lastweek_streams are computed in the setup
# block above, and `validation_forecast_from` is defined there.
validation_forecast = validation_forecast_from("frozen_validation");

# Each frozen individual (single-stream) fit's own one-week-ahead new-count
# forecast at the same cut-off as `frozen_lastweek`, from
# [`forecast_stream`](@ref) (the same per-stream forecaster
# `stream_forecasts.csv` uses), so the validation plots below can show the
# individual fit alongside the joint rather than the joint alone. Recovered
# has no individual fit and is absent here, as it is throughout this report.
# Only the still-reported streams are fitted at the validation cut-off, so
# a stream the situation reports have stopped updating is absent from
# `frozen_lastweek_streams` and carries no individual series here.
function _validation_individual_new(sid, stream::Symbol)
    haskey(frozen_lastweek_streams, sid) || return nothing
    return Float64.(
        forecast_stream(
            fit_forecast("frozen_validation_$sid"), stream; horizon = 7
        )
    )
end
validation_individual = NamedTuple(
    k => v
        for (k, v) in pairs(
            (;
                cases_new = _validation_individual_new(
                    "cases", :reported_cases
                ),
                deaths_new = _validation_individual_new(
                    "deaths", :suspected_deaths
                ),
                confirmed_new = _validation_individual_new(
                    "confirmed", :confirmed_cases
                ),
                confirmed_deaths_new = _validation_individual_new(
                    "confirmed_deaths", :confirmed_deaths
                ),
            )
        )
        if !isnothing(v)
)
# The frozen individual (treatment-only) fit's own bed-occupancy forecast.
# `nothing` when the beds have stopped being reported, so the treatment fit
# is absent; the bed panel then draws the joint alone.
validation_individual_isolation = haskey(frozen_lastweek_streams, "treatment") ?
    Float64.(
        forecast_stream(
            fit_forecast("frozen_validation_treatment"), :isolation_beds;
            horizon = 7
        )
    ) : nothing

# The observed beds at the current cut-off (the forecast target), so the
# frozen-fit bed forecast is scored against what the beds actually held.
# Held back once the beds stop being reported, since the last count would
# then be carried forward rather than observed at the target date.
_obs_beds = stream_reporting(obs, :isolation_beds) ?
    obs.isolation_history.counts[end] : missing
# Same observed/baseline keying as the plot below, so the table covers every
# fitted count stream (cumulative and new-count rows) plus the bed level.
# A harmonisation-break day between the frozen cut-off and the current one
# puts records into the confirmed cumulative that were never notified in that
# week, so the new-count truth carries a step the forecast was never
# predicting. Take it out, the same correction `score_releases.jl` applies.
# Grid days are relative to a seeding date fixed by the genetic tmrca, so the
# frozen fit's own `n` and the current `obs.n` index the same grid.
validation_breaks = (
    confirmed_cum = confirmed_break_correction(
        obs, frozen_lastweek.o.n, obs.n
    ),
    confirmed_deaths_cum = confirmed_break_correction(
        obs, frozen_lastweek.o.n, obs.n; deaths = true
    ),
)

# Observed cumulative at the target date per stream, keyed by the forecast's
# cumulative column; `baseline` is each stream's origin cumulative (the
# frozen cut-off), so the new count is scored against observed minus origin,
# less any harmonisation the window carries (see `validation_breaks`). Both
# the table and the plot below take the still-reported streams
# (`reporting_cum_cols`, from the setup block): a stream the situation
# reports have stopped updating has an origin and a target reading the same
# repeated total, so its cumulative truth is stale and its new-count truth is
# a guaranteed zero.
validation_observed = (
    cases_cum = obs.reported_cases,
    deaths_cum = obs.total_deaths,
    confirmed_cum = obs.confirmed_cases,
    confirmed_deaths_cum = obs.confirmed_deaths,
    recovered_cum = obs.recovered_cases,
)
validation_baseline = (
    cases_cum = frozen_lastweek.o.reported_cases,
    deaths_cum = frozen_lastweek.o.total_deaths,
    confirmed_cum = frozen_lastweek.o.confirmed_cases,
    confirmed_deaths_cum = frozen_lastweek.o.confirmed_deaths,
    recovered_cum = frozen_lastweek.o.recovered_cases,
)

validation_table = forecast_vs_truth(
    validation_forecast;
    observed = keep_streams(validation_observed, reporting_cum_cols),
    baseline = keep_streams(validation_baseline, reporting_cum_cols),
    breaks = validation_breaks,
    isolation = _obs_beds
);
Forecast-versus-observed validation table
StreamQuantityObservedLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%Within 90% PI
DRC confirmed casescumulative by T+797049645972597759871992510058yes
DRC confirmed casesnew this week492433513563659713846yes
DRC confirmed deathscumulative by T+74687464146704688472047424781yes
DRC confirmed deathsnew this week246200229247279301340yes
DRC recovered among confirmedcumulative by T+72595251427282808291029693146yes
DRC recovered among confirmednew this week14766280360462521698yes
DRC isolation bedsoccupancy at T+710416787668209229891150yes
Last week's frozen forecast against the data since observed, for the streams still reported. As in the scores below, the confirmed new-count rows take out any retrospective harmonisation step in the week, since it reattaches records notified earlier. The cumulative rows are scored against the published total, harmonisation included.
Forecast-versus-observed plot
julia
validation_fig = plot_forecast_vs_truth(
    validation_forecast;
    observed = keep_streams(validation_observed, reporting_cum_cols),
    baseline = keep_streams(validation_baseline, reporting_cum_cols),
    breaks = validation_breaks,
    individual = keep_streams(validation_individual, reporting_cum_cols)
);

The one-week-ahead forecast from the frozen fit for each still-reported count stream, cumulative and new, with the 90% predictive interval shaded and the count observed by the cut-off as a dashed black rule. A stream with its own single-stream fit carries that fit's forecast from the same cut-off as a dotted step outline.
Bed forecast-versus-observed plot
julia
validation_beds_fig = plot_forecast_beds_vs_truth(
    validation_forecast;
    isolation = _obs_beds, individual = validation_individual_isolation
);

Last week's projected bed occupancy against the beds occupied now (dashed), with the treatment-only fit's projection as a dotted step outline.
Forecast-versus-now latent plot
julia
# Current fit's draws of the new latent counts over the past week, the last
# seven days of each cumulative-trajectory deterministic.
function _now_new(chn, key)
    mat = chn[key]
    trajs = [collect(v) for v in vec(collect(mat))]
    return Float64[t[end] - t[max(1, length(t) - 7)] for t in trajs]
end
now_latent = (;
    infections_new = _now_new(chn_joint, :cumulative_infections),
    onsets_new = _now_new(chn_joint, :cumulative_onsets),
    deaths_latent_new = _now_new(chn_joint, :cumulative_expected_deaths),
)

validation_latent_fig = plot_forecast_vs_truth_latent(
    validation_forecast; now = now_latent
);

The frozen fit's forecast of the past week's new infections, onsets and deaths against the current fit's estimate for the same window. These quantities are not observed, so they are scored distribution against distribution.

Streams no longer reported ​

Forecast for the streams no longer reported
julia
# The last-reported date per stopped stream, and the frozen fit's own
# projection for them. `plot_forecast` draws a panel per new-count column
# the frame carries, so passing the stopped streams' columns alone gives the
# projection without the fabricated truth rule the validation figure would
# otherwise draw against a repeated total.
validation_stopped_streams = let s = stream_report_status(obs),
        ids = [stream_id(c) for c in stopped_cum_cols]

    keep = [r.stream in ids for r in eachrow(s)]
    DataFrame(
        "Stream" => s[keep, :label],
        "Last reported" => s[keep, :last_date]
    )
end
_stopped_new_cols = [
    c
        for c in new_cols(stopped_cum_cols)
        if c in propertynames(validation_forecast)
]
validation_stopped_fig = plot_forecast(
    validation_forecast[!, _stopped_new_cols]
);
StreamLast reported
Suspected cases2026-05-26
Suspected deaths2026-05-26
The streams the situation reports no longer update, with the date each was last reported.

The frozen fit's projection for these streams over the same week, with no observed rule, since their counts have not moved since they stopped.

Forecast scoring across releases ​

Every release's saved one- to four-week-ahead forecast is scored against the data observed since, against a persistence baseline and, where one exists, the stream's own individual fit. The tables in this section are the joint model's, one row per stream. Recovered has no individual fit of its own, so it is compared with the baseline only. Reported cases and suspected deaths stopped being updated partway through the outbreak, and the exports detection series is anchored to an earlier cut-off. Exports therefore has no scored forecast, and reported cases and suspected deaths each rest on a single matched forecast.

Only a minority of the daily releases examined contribute a row, each a reconstruction of an earlier model version rather than the current fit. Only the newest few releases carry the current model's individual-stream forecasts. Each row shows the number of matched forecasts it rests on, from one to a handful.

Four things are excluded from the scores, each for a stated reason rather than for scoring badly:

  • One whole reconstruction (results-v1.6.0), whose chain forecasts a near-zero median at every horizon and stream with an occasional five- or six-digit upper tail, the signature of a chain that failed to sample.

  • In the frozen section only, the confirmed-death rows of the fourteen frozen reconstructions cut between 16 July and 15 August 2026. Their forecaster floored that stream at zero, giving a one-week median of zero against an observed 250 to 370.

  • An onset window containing a vintage whose reread total falls, since its increment is not what the situation reports added.

  • A stream that carries no persistence baseline, which is what makes a window scoreable at all.

Nothing is dropped from the archive itself, and the score and overlay files in the data directory record everything that was scored.

The symptom-onset stream is scored on the new reported count each vintage adds, because every vintage rereads the whole figure. Its printed total therefore moves with the scan error as well as with late reporting, and on fourteen vintages the reread total falls. A window containing a falling vintage is left unscored, as the province scores do for a window holding a harmonisation-break day. This bites hardest at the longer horizons: the frozen onset row keeps three of its twenty-nine windows, all at one week, and the cross-release row six of thirty-eight. Read the onset row's skill against the baseline rather than its coverage, and as resting on a handful of windows.

Load and summarise the cross-release forecast scores
julia
forecast_scores_df = _release_data(
    "forecast_scores.csv",
    (;
        release = String, made_date = Date, stream = String, horizon = Int,
        target_date = Date, fit = String, crps = Float64,
        log_crps = Float64, dispersion = Float64, overprediction = Float64,
        underprediction = Float64, coverage_50 = Float64,
        coverage_90 = Float64,
        bias = Float64, n_samples = Int,
        log_rel_to_baseline = Float64,
    )
)
forecast_overlay_df = _release_data(
    "forecast_overlay.csv",
    (;
        release = String, made_date = Date, stream = String, horizon = Int,
        target_date = Date, fit = String, observed = Float64,
        median = Float64, lo30 = Float64, hi30 = Float64, lo60 = Float64,
        hi60 = Float64, lo90 = Float64, hi90 = Float64,
    )
)
# The digitised onset triangle's own per-vintage total, as calendar dates,
# and the windows it makes unscoreable. A vintage that rereads the figure
# lower gives a window an increment the reports did not add, so the window
# is dropped from the scores and the overlay alike (see
# `drop_rescanned_onset_windows`). The current triangle is used to judge
# every release, since the scored truth is read off this one series.
_onset_vintage_dates = grid_date.(obs.onset_report_history.days)
_onset_vintage_totals = obs.onset_report_history.counts
_drop_rescanned(tbl) = drop_rescanned_onset_windows(
    tbl; vintage_dates = _onset_vintage_dates,
    vintage_totals = _onset_vintage_totals
)
forecast_scores_df = _drop_rescanned(forecast_scores_df)
forecast_overlay_df = _drop_rescanned(forecast_overlay_df)

# One row per (stream, fit) pooled over every horizon and release. The
# by-horizon and by-release detail tables carry the same columns at a finer
# grain (see src/scoring.jl). Every fit is kept here, since the
# relative-skill figure below compares the roles against each other. The
# tables rendered in this section select the joint role, and the individual
# fits are tabulated in their own section.
forecast_score_overview_table = forecast_score_overview(forecast_scores_df)
forecast_score_by_horizon_table = forecast_score_by_horizon(forecast_scores_df)
forecast_score_by_release_table = forecast_score_by_release(forecast_scores_df)

joint_score_overview_table = select_fit_role(
    forecast_score_overview_table, "joint"
)
joint_score_by_horizon_table = select_fit_role(
    forecast_score_by_horizon_table, "joint"
)
# The trailing `;` on this last assignment matters: without it, this whole
# setup chunk's last statement (the DataFrame it assigns) is Literate's
# implicitly displayed "result" for the chunk, on top of the deliberate
# display further down -- and a bare DataFrame is html-showable, so it
# goes out as a second, undisplayed-in-source `@raw html` block that (for
# a table this size) can itself hit the PCRE limit described above.
joint_score_by_release_table = select_fit_role(
    forecast_score_by_release_table, "joint"
);
streamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
confirmed casesjoint143747.245.240.2231.980.690.65610.89135.70.650.60.870.39
confirmed deathsjoint141666.866.860.3832.550.920.94562.6143.3360.920.340.850.08
isolation bedsjoint13895.841.670.1111.530.880.7255.7830.179.880.640.960.14
onset reportsjoint27121.430.910.1890.820.650.6270.9212.9137.590.630.96-0.04
recoveredjoint4558.521.610.1461.0645.679.393.460.8210.12
Scores pooled over every horizon and release, one row per stream: the mean CRPS and its decomposition, coverage, bias, and relative skill against the persistence baseline on the natural and log scales. Each row also carries relative skill against the stream's own individual fit where one exists.
julia
forecast_relative_skill_fig = plot_forecast_relative_skill(
    forecast_score_by_horizon_table
);

Relative skill against the baseline by horizon, one panel per stream and one series per fit role, on a log scale with the reference line at one.
julia
forecast_crps_by_horizon_fig = plot_forecast_crps_by_horizon(
    forecast_score_by_horizon_table
);

The mean CRPS by horizon split into its width, overprediction and underprediction, one stacked bar per horizon and fit role.
Scores by horizon
streamhorizonfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
confirmed cases7joint39104.692.570.2172.550.590.5959.9944.010.680.590.870.4
confirmed cases14joint38228.32.520.2172.270.620.63131.0296.590.690.630.870.36
confirmed cases21joint35430.632.520.2171.780.660.66260.5169.140.990.630.860.38
confirmed cases28joint312549.178.420.2471.550.760.742287.78261.220.170.550.90.43
confirmed deaths7joint3964.662.740.4053.981.070.9723.9512.6428.070.410.790.04
confirmed deaths14joint37142.982.40.3612.831.030.9555.7132.0455.240.320.840.08
confirmed deaths21joint34266.022.20.3732.230.950.94131.0756.4278.530.260.850.09
confirmed deaths28joint312489.3911.910.3931.80.820.892318.681.0789.720.350.940.11
isolation beds7joint3771.951.670.0861.570.670.5535.3427.379.240.540.950.19
isolation beds14joint3697.961.830.1121.670.920.755332.0112.950.530.970.13
isolation beds21joint341021.60.1171.450.950.7963.6127.6410.750.760.970.1
isolation beds28joint31115.141.60.1341.4810.8474.8434.166.140.770.970.14
onset reports7joint1374.620.630.2210.670.740.6345.397.6321.60.690.920.02
onset reports14joint8142.571.370.1661.370.620.6278.115.6248.850.621-0.06
onset reports21joint6194.670.960.150.930.580.58116.6720.7757.230.51-0.15
recovered7joint1414.470.870.1220.9610.372.371.730.8610.02
recovered14joint1335.31.280.1331.1726.465.133.720.8510.16
recovered21joint11110.822.670.1691.2892.4815.163.190.8210.16
recovered28joint7107.561.280.1830.8678.3922.286.890.7110.18
julia
forecast_skill_by_cutoff_fig = plot_forecast_skill_by_cutoff(
    forecast_score_by_release_table;
    title = "Relative skill against the baseline, by release"
);

Relative skill against the baseline release by release, so a run of releases that lost to the baseline reads as a run rather than as an average.
Scores by release
made_datestreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
2026-06-07confirmed casesjoint2363.9111.660.9139.51341.0622.860110.17
2026-06-10confirmed casesjoint3534.876.720.7915.07435.6799.20110.27
2026-07-01confirmed casesjoint415862.7876.20.5832.915377.31485.470010.66
2026-07-06confirmed casesjoint4421.281.340.2690.97311.85109.4300.510.49
2026-07-08confirmed casesjoint4390.981.230.2530.96239.5151.4700.2510.52
2026-07-23confirmed casesjoint4335.570.870.1690.661.020.82299.2735.680.62110.13
2026-07-25confirmed casesjoint4222.50.960.1340.950.640.63218.044.460110.07
2026-07-26confirmed casesjoint4235.691.080.1411.070.790.67229.655.680.36110.06
2026-07-27confirmed casesjoint4247.971.210.1421.030.930.83240.087.880110.11
2026-07-31confirmed casesjoint4259.641.880.1491.710.640.68235.7323.760.15110.13
2026-08-01confirmed casesjoint4231.261.950.1391.940.690.66226.844.310.11110.05
2026-08-02confirmed casesjoint4278.452.190.1451.810.940.85263.7714.680110.14
2026-08-03confirmed casesjoint4247.581.30.140.980.850.8237.889.70110.08
2026-08-04confirmed casesjoint4221.562.320.1332.260.630.69220.70.860110.03
2026-08-07confirmed casesjoint4213.542.490.142.030.690.73209.7603.7811-0.09
2026-08-11confirmed casesjoint4197.494.510.1353.730.590.71191.5305.9711-0.13
2026-08-15confirmed casesjoint4387.776.580.1933.831.240.91294.0293.750110.37
2026-08-17confirmed casesjoint4458.716.180.2223.371.530.97290.38168.3300.510.46
2026-08-22confirmed casesjoint4336.094.920.1783.5110.87228.94107.1500.7510.45
2026-08-24confirmed casesjoint4246.113.730.1443.050.780.77118.34127.770010.62
2026-08-25confirmed casesjoint4198.572.940.122.60.660.65108.4890.10010.56
2026-08-30confirmed casesjoint4155.681.890.0891.530.40.4499.6855.9900.7510.38
2026-08-31confirmed casesjoint4143.981.50.0851.350.480.48106.0937.890110.29
2026-09-01confirmed casesjoint4113.161.190.071.050.320.3697.2115.950110.17
2026-09-06confirmed casesjoint4236.661.880.1411.80.670.65116.22120.4400.2510.6
2026-09-10confirmed casesjoint4313.881.720.2051.570.70.78106.15207.730010.79
2026-09-12confirmed casesjoint4711.142.810.4332.180.540.58165.71545.420000.96
2026-09-13confirmed casesjoint4720.073.460.4262.620.60.61207.2512.870000.93
2026-09-15confirmed casesjoint4666.042.920.4312.170.640.66197.5468.54000.250.92
2026-09-16confirmed casesjoint4648.533.050.4192.310.680.68195.04453.49000.250.93
2026-09-19confirmed casesjoint4942.364.850.5853.551.451.21215.69726.670000.97
2026-09-21confirmed casesjoint4110.91.150.1051.390.230.2966.6644.2400.510.53
2026-09-22confirmed casesjoint4135.091.760.122.080.380.4271.2963.800.2510.61
2026-09-25confirmed casesjoint344.010.960.0480.730.140.1741.262.750110.17
2026-09-26confirmed casesjoint3812.20.1052.290.210.2856.2424.7600.6710.4
2026-09-27confirmed casesjoint361.011.190.0670.860.290.3544.78016.2211-0.35
2026-10-02confirmed casesjoint247.310.820.0610.80.380.3935.6111.690110.37
2026-10-06confirmed casesjoint2109.863.390.1292.940.810.847.4462.430010.66
2026-10-10confirmed casesjoint185.952.20.1562.060.90.929.2356.720010.77
2026-06-07confirmed deathsjoint141.491.890.9032.6736.9404.5511-0.2
2026-06-10confirmed deathsjoint264.932.30.7353.1562.70.991.2411-0.03
2026-07-01confirmed deathsjoint417196800.5491.217103.1192.890110.28
2026-07-06confirmed deathsjoint4197.20.90.8712.33110.35086.850.251-0.49
2026-07-08confirmed deathsjoint4177.590.790.7131.8784.29093.30.251-0.61
2026-07-23confirmed deathsjoint4271.491.670.4842.450.391.08172.61098.8811-0.45
2026-07-25confirmed deathsjoint4241.831.830.5043.11.022.15106.460135.3701-0.57
2026-07-26confirmed deathsjoint4268.462.380.5544.331.052.17101.590166.8701-0.64
2026-07-27confirmed deathsjoint4249.472.230.5343.860.82.12109.30140.1701-0.59
2026-07-31confirmed deathsjoint4274.552.30.563.950.881.94108.790165.760.251-0.58
2026-08-01confirmed deathsjoint4313.482.660.6834.511.122.794.480219.0101-0.68
2026-08-02confirmed deathsjoint4258.172.870.5074.531.011.83128.190129.980.251-0.53
2026-08-03confirmed deathsjoint4268.892.060.583.061.22.37123.560145.330.251-0.55
2026-08-04confirmed deathsjoint4298.064.260.6567.971.22.3293.540204.5201-0.67
2026-08-07confirmed deathsjoint4366.295.220.888.581.582.7176.060290.2300.75-0.79
2026-08-11confirmed deathsjoint4301.1715.060.79827.691.232.3796.550204.6101-0.7
2026-08-15confirmed deathsjoint4205.9350.325.150.840.94180.35025.5811-0.26
2026-08-17confirmed deathsjoint4202.493.50.3043.570.811.02187.02015.4711-0.21
2026-08-22confirmed deathsjoint4178.981.880.3052.380.740.63156.16022.8211-0.26
2026-08-24confirmed deathsjoint492.941.260.1051.170.420.1945.4247.5100.2510.57
2026-08-25confirmed deathsjoint4741.020.08410.30.1145.0828.9200.7510.41
2026-08-30confirmed deathsjoint475.880.920.09210.290.138.0537.8300.510.48
2026-08-31confirmed deathsjoint486.270.760.1110.830.350.1441.9844.290010.6
2026-09-01confirmed deathsjoint474.640.770.0950.830.30.143.0231.6100.2510.53
2026-09-06confirmed deathsjoint4126.851.350.1561.40.450.1343.5883.2800.2510.68
2026-09-10confirmed deathsjoint4173.521.320.2461.250.840.3343.19130.34000.50.89
2026-09-12confirmed deathsjoint4273.481.940.3581.582.712.3865.52207.960000.95
2026-09-13confirmed deathsjoint4264.392.20.3421.782.812.3680.9183.49000.250.91
2026-09-15confirmed deathsjoint4237.21.970.3361.663.83.1169.24167.96000.250.91
2026-09-16confirmed deathsjoint4222.672.190.321.823.883.1370.49152.18000.50.91
2026-09-19confirmed deathsjoint4292.383.310.4012.755.394.2271.62220.760000.95
2026-09-21confirmed deathsjoint467.051.170.1361.51.521.722.1944.85000.750.8
2026-09-22confirmed deathsjoint454.741.290.1081.620.80.8924.9329.8100.2510.66
2026-09-25confirmed deathsjoint319.260.630.0420.450.210.1818.041.220110.18
2026-09-26confirmed deathsjoint342.31.760.1272.190.881.1716.9625.3400.330.670.65
2026-09-27confirmed deathsjoint318.50.850.0430.710.30.2916.510.741.25110
2026-10-02confirmed deathsjoint212.730.630.0370.630.290.2612.030.70110.14
2026-10-06confirmed deathsjoint213.030.590.0360.580.270.24130.040110.03
2026-10-10confirmed deathsjoint120.71.020.0831.011.441.2110.779.920010.54
2026-07-01isolation bedsjoint490.191.510.1381.5513.48076.7100-0.99
2026-07-06isolation bedsjoint442.870.620.0660.6421.07021.801-0.64
2026-07-08isolation bedsjoint454.751.560.0731.5611.6743.080010.8
2026-07-23isolation bedsjoint480.062.720.1012.631.441.1429.3950.670010.58
2026-07-25isolation bedsjoint480.842.060.1052.031.561.2832.6548.1900.510.53
2026-07-26isolation bedsjoint473.831.860.0971.791.20.9636.9636.8700.510.46
2026-07-27isolation bedsjoint471.582.630.0932.531.20.9539.5232.0600.510.44
2026-07-31isolation bedsjoint471.051.730.0881.680.860.6751.6919.3600.7510.34
2026-08-01isolation bedsjoint4721.130.0941.090.810.6164.517.480.02110.18
2026-08-02isolation bedsjoint483.691.580.1021.420.990.7252.6731.0200.7510.39
2026-08-03isolation bedsjoint478.831.930.0991.790.990.7460.4218.410110.3
2026-08-04isolation bedsjoint4100.741.560.1371.51.160.9863.4737.2800.7510.38
2026-08-07isolation bedsjoint483.811.250.1221.380.80.7169.03014.7811-0.24
2026-08-11isolation bedsjoint474.710.430.1090.430.730.6472.4802.2311-0.08
2026-08-15isolation bedsjoint480.960.930.0920.830.630.4571.729.240110.17
2026-08-17isolation bedsjoint495.081.270.1061.10.920.6667.3527.740110.33
2026-08-22isolation bedsjoint493.151.750.0981.520.640.4578.9814.180110.23
2026-08-24isolation bedsjoint490.981.870.0961.60.720.5366.324.6700.7510.34
2026-08-25isolation bedsjoint475.541.040.0820.910.570.4371.773.740.02110.08
2026-08-30isolation bedsjoint480.951.840.0891.630.630.5262.6118.3300.7510.3
2026-08-31isolation bedsjoint468.551.60.0791.540.570.4962.975.180.4110.14
2026-09-01isolation bedsjoint481.21.970.1032.160.670.6769.01012.1911-0.2
2026-09-06isolation bedsjoint475.081.510.0891.480.60.5768.210.456.4311-0.12
2026-09-10isolation bedsjoint475.831.490.0851.430.590.5470.160.215.4611-0.1
2026-09-12isolation bedsjoint4177.12.960.1742.591.320.9661.26115.8400.2510.6
2026-09-13isolation bedsjoint4186.883.390.1852.981.541.1870.89115.990010.6
2026-09-15isolation bedsjoint4141.542.590.1372.361.070.8672.6368.9100.510.48
2026-09-16isolation bedsjoint4149.483.740.1433.241.150.8981.0668.4300.510.48
2026-09-19isolation bedsjoint4270.825.180.2574.272.151.7248.64222.18000.750.77
2026-09-21isolation bedsjoint473.081.280.0861.310.540.5657.770.115.2211-0.2
2026-09-22isolation bedsjoint486.571.240.0961.270.470.456.8029.770.751-0.33
2026-09-25isolation bedsjoint388.861.760.111.830.680.7341.9046.960.671-0.56
2026-09-26isolation bedsjoint398.181.240.1231.290.830.8751.01047.170.671-0.52
2026-09-27isolation bedsjoint3155.752.240.2042.421.121.1443.890111.8601-0.76
2026-10-02isolation bedsjoint248.860.890.0540.850.390.3645.9102.9511-0.14
2026-10-06isolation bedsjoint2101.691.410.0961.330.730.5861.2640.4300.510.48
2026-10-10isolation bedsjoint149.311.840.0511.780.40.3544.994.320110.19
2026-08-02onset reportsjoint380.540.590.10.60.760.5977.9102.6311-0.12
2026-08-03onset reportsjoint387.721.330.1190.920.890.7776.4911.230110.22
2026-08-04onset reportsjoint384.340.660.1310.60.610.574.122.327.9111-0.01
2026-08-07onset reportsjoint3278.250.840.3870.850.670.59690209.2500.67-0.83
2026-08-11onset reportsjoint3194.711.140.2871.130.80.6783.450111.260.331-0.6
2026-08-15onset reportsjoint3171.372.050.1561.430.460.56107.0564.3200.3310.43
2026-08-17onset reportsjoint2103.331.680.1341.190.420.5378.8324.50110.28
2026-08-22onset reportsjoint267.730.640.1330.880.560.6367.4400.2911-0.04
2026-08-24onset reportsjoint164.90.950.3061.950.760.7857.507.4111-0.21
2026-08-25onset reportsjoint163.70.270.3150.530.670.6261.192.510110.13
2026-09-21onset reportsjoint145.40.70.1520.490.630.6511.7133.690010.83
2026-09-22onset reportsjoint143.050.730.1440.340.590.6113.1629.890010.82
2026-10-02onset reportsjoint128.570.440.10.451.281.3114.7013.8801-0.55
2026-07-01recoveredjoint3312.696.780.4461.98289.5523.140110.27
2026-07-06recoveredjoint485.760.790.4070.8468.39017.3711-0.33
2026-07-08recoveredjoint463.080.760.2921.0244.51018.5711-0.38
2026-09-15recoveredjoint457.314.20.0942.4940.2817.0300.510.35
2026-09-16recoveredjoint453.242.610.0881.5340.8712.370110.26
2026-09-19recoveredjoint484.684.660.142.8542.242.160.320.2510.43
2026-09-21recoveredjoint413.860.680.0360.7313.390.470110.1
2026-09-22recoveredjoint413.150.830.0350.912.660.460.04110.04
2026-09-25recoveredjoint313.140.780.0450.8510.561.241.3511-0.07
2026-09-26recoveredjoint321.090.540.0750.5410.3310.7600.3310.6
2026-09-27recoveredjoint313.710.410.050.4411.242.470110.27
2026-10-02recoveredjoint218.380.570.1030.618.529.8600.510.55
2026-10-06recoveredjoint211.040.820.0510.768.5302.511-0.3
2026-10-10recoveredjoint18.420.490.0510.466.7901.6311-0.26
Forecasts-versus-now overlay
julia
forecast_overlay_fig = plot_forecast_overlay(
    scored_overlay(forecast_overlay_df)
);

Forecasts made at each release against the value observed since (black), one panel per stream and horizon by the date each forecast was made, with the median and 90% interval coloured by fit role. Each panel's axis is cropped to a small multiple of what the stream reached, and an interval or median past it is clamped and marked with an open triangle.

Frozen-fit forecast evaluation ​

The current model, frozen at earlier data cut-offs (see forecast-versus-frozen evaluation), is scored as the cross-release forecasts above, against the same baseline. The tables in this section are the frozen joint model's, one row per stream. Each stream's own frozen fit is scored only at the one-week-back cut-off for still-reported streams, and appears in the skill figures rather than the tables. The May cut-offs predate the first reported bed occupancy and recoveries, so those windows are left unscored. The baseline snapshot was taken weeks after the frozen cut-off, so it can hold later revisions to earlier days (see forecast scoring against a persistence baseline).

Load and summarise the frozen-fit forecast scores
julia
frozen_scores_df = _release_data(
    "forecast_scores_frozen.csv",
    (;
        release = String, made_date = Date, stream = String, horizon = Int,
        target_date = Date, fit = String, crps = Float64,
        log_crps = Float64, dispersion = Float64, overprediction = Float64,
        underprediction = Float64, coverage_50 = Float64,
        coverage_90 = Float64,
        bias = Float64, n_samples = Int,
        log_rel_to_baseline = Float64,
    )
)
frozen_overlay_df = _release_data(
    "forecast_overlay_frozen.csv",
    (;
        release = String, made_date = Date, stream = String, horizon = Int,
        target_date = Date, fit = String, observed = Float64,
        median = Float64, lo30 = Float64, hi30 = Float64, lo60 = Float64,
        hi60 = Float64, lo90 = Float64, hi90 = Float64,
    )
)
# Rows a superseded forecaster produced are dropped before anything is
# summarised or drawn, from the scores and the overlay alike, so the
# tables and the figures rest on one set of rows. See
# `drop_superseded_forecasts` for the one exclusion in force and why.
frozen_scores_df = _drop_rescanned(
    drop_superseded_forecasts(frozen_scores_df)
)
frozen_overlay_df = _drop_rescanned(
    drop_superseded_forecasts(frozen_overlay_df)
)

# The frozen joint carries `FROZEN_FIT`, so it is named as the joint role
# here and compared against each stream's own frozen fit. A release
# published before the archive had a `fit` column carries joint rows only.
frozen_score_overview_table = forecast_score_overview(
    frozen_scores_df; joint_fit = FROZEN_FIT
)
frozen_score_by_horizon_table = forecast_score_by_horizon(
    frozen_scores_df; joint_fit = FROZEN_FIT
)
frozen_score_by_release_table = forecast_score_by_release(
    frozen_scores_df; joint_fit = FROZEN_FIT
)

# The tables show the frozen joint alone, as the cross-release tables show
# the joint alone, so a row reads as one model at one cut-off rather than a
# stream interleaving two fits. The archive's single-stream frozen fits stay
# in the scored data and in the figures, which compare the roles against
# each other. Selecting the joint role leaves `fit` single-valued, so it is
# dropped and the model named in the prose instead.
_frozen_joint_only(tbl) = drop_degenerate_fit_column(
    select_fit_role(tbl, "joint")
)
frozen_score_overview_display = _frozen_joint_only(
    frozen_score_overview_table
)
frozen_score_by_horizon_display = _frozen_joint_only(
    frozen_score_by_horizon_table
)
frozen_score_by_release_display = _frozen_joint_only(
    frozen_score_by_release_table
)

# One row per release for the cut-offs more than one release forecast.
# See the comment above `joint_score_by_release_table`'s assignment for why
# this setup chunk's last statement needs a trailing `;`.
frozen_score_by_vintage_table = forecast_score_by_vintage(
    frozen_scores_df; joint_fit = FROZEN_FIT
)
frozen_score_by_vintage_display = _frozen_joint_only(
    frozen_score_by_vintage_table
);
streamncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
confirmed cases308305.142.390.3551.160.520.57143.92145.2315.990.360.770.31
confirmed deaths25263.41.020.3370.572.391.9229.729.524.180.510.830.06
isolation beds27397.410.870.1440.620.790.6653.127.7516.560.530.980.12
onset reports30148.161.030.2450.8469.5319.7358.910.570.9-0.16
recovered133363.067.770.5844.49140.76220.052.250.420.620.53
julia
frozen_relative_skill_fig = plot_forecast_relative_skill(
    frozen_score_by_horizon_table
);

Relative skill against the baseline by horizon for the frozen cut-offs.
julia
frozen_crps_by_horizon_fig = plot_forecast_crps_by_horizon(
    frozen_score_by_horizon_table;
    title = "CRPS decomposition by horizon, frozen cut-offs"
);

The mean CRPS by horizon for the frozen cut-offs, split as in the cross-release section above.
Scores by horizon
streamhorizonncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
confirmed cases714091.151.070.3910.770.490.5532.9523.0235.180.380.71-0.07
confirmed cases1469258.753.280.282.940.520.58127.29131.4600.410.860.6
confirmed cases2168583.223.670.3982.570.530.58257.57325.6500.220.740.69
confirmed cases2831764.792.120.2641.440.530.57432.7933200.520.90.54
confirmed deaths712619.130.780.470.581.71.577.133.658.340.50.81-0.34
confirmed deaths145554.611.170.1780.752.021.829.2225.350.040.730.890.37
confirmed deaths2154115.580.880.2240.412.191.8864.1951.380.020.460.890.45
confirmed deaths2817254.251.460.221.313.172.7989.02165.2300.060.650.76
isolation beds77057.071.220.0910.880.540.4629.3822.724.970.640.930.26
isolation beds146983.311.570.1331.140.810.745.6834.313.320.6410.33
isolation beds2168104.880.670.1540.480.890.7459.9923.6921.20.461-0.05
isolation beds2866147.250.750.2010.520.970.7878.9230.437.920.381-0.09
onset reports716109.120.750.2930.748.1812.9248.020.560.88-0.2
onset reports1410146.281.350.1731.2880.817.3848.10.70.9-0.07
onset reports214309.051.360.2381.31126.7152.85129.490.251-0.27
recovered735122.596.450.5464.2541.6679.51.420.40.830.5
recovered1434264.810.590.5696.4688.35174.541.90.410.560.54
recovered2133434.077.690.6014.4155.08276.552.440.420.550.54
recovered2831666.757.280.6243.64294.89368.483.370.450.520.52
julia
frozen_skill_by_cutoff_fig = plot_forecast_skill_by_cutoff(
    frozen_score_by_release_table;
    xlabel = "Frozen cut-off",
    title = "Relative skill against the baseline, by frozen cut-off"
);

Relative skill against the baseline cut-off by cut-off, pooled over the horizons each cut-off forecast.
Scores by frozen cut-off
made_datestreamncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
2026-05-23confirmed cases3529.370.350.1920.3118.61010.750.61-0.37
2026-05-27confirmed cases35141.530.790.9360.7511.910129.6300.09-0.95
2026-06-08confirmed cases105342.36.910.3813.97147.8194.500.250.850.67
2026-07-16confirmed cases4294.680.460.1680.29283.4610.980.24110.07
2026-07-18confirmed cases4361.260.760.1850.5333.8527.410110.15
2026-07-19confirmed cases4439.621.060.2090.7351.8387.790110.3
2026-07-20confirmed cases4385.620.860.1850.55332.653.020110.22
2026-07-24confirmed cases4422.921.360.1920.95343.779.220110.32
2026-07-25confirmed cases4460.722.020.2091.52356.82103.910110.34
2026-07-26confirmed cases4315.561.450.1641.27284.5930.970110.24
2026-07-27confirmed cases4346.391.690.1761.29306.9739.420110.27
2026-07-28confirmed cases4396.781.360.1560.84361.5235.260110.22
2026-07-31confirmed cases4498.383.740.2132.49369.58128.800.7510.38
2026-08-04confirmed cases4281.573.140.1562.89253.9927.590110.18
2026-08-08confirmed cases4253.512.90.152.14246.676.120.72110.03
2026-08-10confirmed cases4244.513.620.152.562411.71.8111-0.01
2026-08-15confirmed cases4623.410.650.2815.59384.1239.300.2510.52
2026-08-17confirmed cases4426.555.830.2343.63166.54260.010010.75
2026-08-18confirmed cases4468.254.070.2762.88155.86312.4000.750.85
2026-08-23confirmed cases4562.748.530.3146.6140.78421.96000.750.88
2026-08-24confirmed cases4437.046.990.2465.65160.51276.53000.750.82
2026-08-25confirmed cases4288.324.330.1763.96113.18175.140010.74
2026-08-30confirmed cases4179.542.160.1061.68110.0569.4900.7510.44
2026-09-03confirmed cases4279.852.380.1612.2899.17180.680010.71
2026-09-05confirmed cases4133.121.030.0841.02104.0628.720.34110.19
2026-09-06confirmed cases4560.624.80.2793.83181.89378.730010.74
2026-09-08confirmed cases4439.233.060.2412.460.380.39185.89253.34000.750.76
2026-09-09confirmed cases4610.513.570.3312.750.520.52205.84404.67000.50.85
2026-09-12confirmed cases4810.133.340.4842.540.610.64191.69618.440000.96
2026-09-14confirmed cases4591.662.540.4172.030.520.6111.41480.250000.97
2026-09-15confirmed cases4641.052.850.452.270.620.68126.77514.280000.97
2026-09-18confirmed cases4433.842.150.3531.950.490.5892.05341.79000.250.95
2026-09-19confirmed cases4410.132.090.3251.970.630.67117.34292.79000.50.88
2026-09-20confirmed cases4175.251.170.1631.30.310.3886.6788.5800.250.750.67
2026-09-25confirmed cases3145.313.40.1382.210.440.4760.1785.140010.71
2026-09-29confirmed cases3111.261.430.0910.870.930.8374.4836.780110.4
2026-10-03confirmed cases266.820.450.0740.320.730.7252.4614.360110.28
2026-10-13confirmed cases164.313.450.1152.960.690.729.0935.220010.67
2026-05-23confirmed deaths357.750.310.3320.243.5304.220.491-0.49
2026-05-27confirmed deaths3526.550.821.0990.922.24024.3100.49-0.91
2026-06-08confirmed deaths10547.320.690.2030.4638.488.380.460.8810.13
2026-08-17confirmed deaths491.871.610.0971.1459.332.5700.510.44
2026-08-18confirmed deaths4115.91.320.1341.0755.9159.990010.63
2026-08-23confirmed deaths4145.191.770.1661.5651.7193.480010.72
2026-08-24confirmed deaths4110.911.520.121.3455.255.7100.2510.53
2026-08-25confirmed deaths487.421.220.11.1844.6542.7700.2510.53
2026-08-30confirmed deaths469.430.790.0870.939.7529.6800.510.5
2026-09-03confirmed deaths494.771.220.1121.1841.453.3700.2510.55
2026-09-05confirmed deaths453.980.590.0740.6740.7712.260.95110.17
2026-09-06confirmed deaths4122.51.30.1421.2645.3276.870.310.510.46
2026-09-08confirmed deaths4165.381.590.1931.452.11.8267.2198.1600.2510.73
2026-09-09confirmed deaths4216.822.170.2521.833.12.5278.5138.310010.79
2026-09-12confirmed deaths4265.951.890.3421.512.482.1368.8197.150000.94
2026-09-14confirmed deaths4216.781.810.3171.623.162.828.06188.720000.99
2026-09-15confirmed deaths4206.511.740.3231.633.793.2727.27179.240000.99
2026-09-18confirmed deaths4157.261.480.2891.513.452.9923.78133.480000.98
2026-09-19confirmed deaths4162.391.780.281.854.13.826.75135.640000.97
2026-09-20confirmed deaths4118.961.630.2191.873.353.1921.7197.250000.96
2026-09-25confirmed deaths334.221.210.0710.830.450.3916.617.630010.58
2026-09-29confirmed deaths319.360.850.0450.80.230.2317.141.940.29110.14
2026-10-03confirmed deaths214.050.240.040.20.250.2213.340.020.6911-0.12
2026-10-13confirmed deaths111.31.050.0440.960.580.5110.760.540110.13
2026-06-08isolation beds14091.720.550.160.4151.0115.2725.440.511-0.04
2026-07-16isolation beds473.222.320.0962.220.0153.220010.6
2026-07-18isolation beds477.782.590.1032.4618.7159.070010.61
2026-07-19isolation beds4100.013.680.1323.5322.3877.630010.64
2026-07-20isolation beds482.233.020.1062.8719.8962.3300.2510.61
2026-07-24isolation beds476.612.730.0962.5610.1466.470010.73
2026-07-25isolation beds4106.432.790.1362.6913.8492.59000.750.73
2026-07-26isolation beds494.882.540.1222.422.6172.2700.250.750.63
2026-07-27isolation beds485.563.090.1132.9938.9246.6500.510.48
2026-07-28isolation beds4153.082.990.2012.6741.15111.9400.50.750.67
2026-07-31isolation beds4119.062.670.1392.4341.3677.700.50.750.57
2026-08-04isolation beds4110.511.510.1531.4663.2647.2400.750.750.42
2026-08-08isolation beds459.170.910.0780.9251.547.150.480.7510.16
2026-08-10isolation beds455.991.050.0791.155.830.020.15110
2026-08-15isolation beds4109.361.290.1151.0874.0735.30110.34
2026-08-17isolation beds4103.62.230.1111.9272.6230.9800.7510.43
2026-08-18isolation beds493.51.310.11.0971.3122.1900.7510.34
2026-08-23isolation beds4145.693.690.1493.1760.6785.020010.66
2026-08-24isolation beds4108.372.220.1131.8968.4739.900.510.46
2026-08-25isolation beds471.11.080.0871.0660.24010.8611-0.27
2026-08-30isolation beds471.871.680.0941.7862.2509.6211-0.19
2026-09-03isolation beds476.350.930.0960.9253.15023.20.751-0.38
2026-09-05isolation beds486.591.820.1122.0167.81018.780.751-0.31
2026-09-06isolation beds4243.914.980.5228.74146.37097.550.751-0.44
2026-09-08isolation beds4106.031.580.1081.420.70.5391.1214.140.76110.12
2026-09-09isolation beds4136.552.120.1321.710.950.6681.8654.6900.7510.37
2026-09-12isolation beds4198.783.550.1963.111.51.1649.76149.020010.7
2026-09-14isolation beds4164.322.640.1592.321.251.0175.6888.640010.7
2026-09-15isolation beds4115.222.140.111.910.850.6875.234000.510.43
2026-09-18isolation beds474.761.570.0781.450.550.4762.6712.0900.7510.27
2026-09-19isolation beds484.481.60.0891.460.620.5671.2613.20.020.7510.24
2026-09-20isolation beds478.161.40.0971.440.550.5759.1701911-0.32
2026-09-25isolation beds357.241.120.0661.090.430.434809.2411-0.19
2026-09-29isolation beds399.080.920.1070.930.640.5857.97041.120.671-0.4
2026-10-03isolation beds259.461.040.0650.980.460.4155.2504.2211-0.12
2026-10-13isolation beds1111.961.370.1191.410.730.6833078.9601-0.76
2026-07-26onset reports267.832.20.1191.9865.222.610110.13
2026-07-27onset reports268.121.010.1210.8156.661.4610.0111-0.15
2026-07-28onset reports2790.490.1290.3761.396.9410.6711-0.07
2026-07-31onset reports2117.290.710.2390.6175.7241.5800.510.4
2026-08-04onset reports3940.720.1370.6267.91.4524.6511-0.14
2026-08-08onset reports3341.250.760.4770.7171.380269.8800.33-0.88
2026-08-10onset reports3198.521.80.3091.9266.10132.4101-0.69
2026-08-15onset reports3263.972.840.2151.78135.96128.0100.3310.64
2026-08-17onset reports283.461.390.1781.6771.82011.6411-0.25
2026-08-18onset reports2114.861.270.241.5578.01036.850.51-0.33
2026-08-23onset reports164.410.990.2221.3158.2306.1811-0.2
2026-08-24onset reports170.40.930.3221.8462.2508.1511-0.21
2026-08-25onset reports1124.180.510.360.635.2888.90010.81
2026-09-09onset reports167.920.390.1240.1558.469.460110.23
2026-09-20onset reports156.910.560.1660.2614.2042.701-0.8
2026-10-03onset reports1306.691.920.6981.7315.69029100-1
2026-07-16recovered4139.341.10.4691.11135.503.8411-0.1
2026-07-18recovered4149.612.170.4552.13146.942.670110.1
2026-07-19recovered4181.472.230.4822.12179.352.120110.07
2026-07-20recovered4171.812.110.4171.91169.632.180110.06
2026-07-24recovered4179.372.260.4091.95175.364.010110.13
2026-07-25recovered4177.71.990.3791.66173.524.180110.12
2026-07-26recovered4139.991.510.3811.59138.990111-0.05
2026-07-27recovered4219.872.220.4831.86218.3801.4911-0.06
2026-07-28recovered4544.073.760.3690.85543.250.50.33110.02
2026-07-31recovered4249.162.460.4021.48242.916.250110.12
2026-08-04recovered4157.982.310.4572.65146.41011.5711-0.19
2026-08-08recovered4125.591.60.4172.09109.72015.8711-0.23
2026-08-10recovered4157.11.910.5372.47116.31040.7911-0.32
2026-08-15recovered4384.4513.150.4425.97335.6248.830110.29
2026-08-17recovered4548.5820.680.7799.23117.93430.640000.94
2026-08-18recovered4596.2725.050.83411.6299.02497.250000.95
2026-08-23recovered4553.2930.280.73915.62114.01439.28000.250.92
2026-08-24recovered4518.5831.720.72616.52112.37406.220000.94
2026-08-25recovered4500.2431.170.71616.7590.53409.710000.96
2026-08-30recovered4436.8822.010.63512.7497.38339.5000.250.92
2026-09-03recovered4505.8634.650.71419.3994.33411.53000.250.92
2026-09-05recovered4369.423.10.58213.2988.66280.73000.50.89
2026-09-06recovered4295.0118.230.4310.2995.6199.41000.50.79
2026-09-08recovered4563.2837.490.7519.76133.12430.16000.50.9
2026-09-09recovered4621.7934.250.83916.48139.18482.610000.97
2026-09-12recovered4633.3642.620.79420.81139.51493.85000.250.94
2026-09-14recovered4481.6734.950.67516.1275.86405.810000.94
2026-09-15recovered4487.533.470.68317.3887.32400.18000.50.91
2026-09-18recovered4458.8424.050.68114.9173.88384.96000.250.92
2026-09-19recovered4518.1430.740.72815.4584.46433.69000.250.93
2026-09-20recovered4406.2324.930.60914.8781.49324.740010.85
2026-09-25recovered3273.416.320.54310.4661.12212.280010.82
2026-09-29recovered3311.2817.710.63510.8959.96251.320010.88
2026-10-03recovered2223.8521.910.63614.0347.81176.04000.50.89
2026-10-13recovered1195.9810.280.7636.6636.6159.370010.83

Frozen skill by release ​

Frozen skill per release
julia
frozen_skill_by_vintage_fig = plot_forecast_skill_by_vintage(
    frozen_score_by_vintage_table
);

Skill at each cut-off that more than one release forecast, one point per release rather than pooled. Releases run in the order they were cut, evenly spaced rather than to calendar scale.
Frozen scores by release
streamreleaserelease_datencrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
confirmed casesresults-13592026-07-165242.12.920.4541.04121.4495.4625.20.610.07
confirmed casesresults-13912026-07-185242.12.990.4541.07121.4495.4625.20.610.07
confirmed casesresults-13942026-07-195242.12.950.4541.07121.4495.4625.20.610.07
confirmed casesresults-14412026-07-205245.992.970.4661.07113.62106.9225.450.20.80.16
confirmed casesresults-14462026-07-245245.992.940.4661.08113.62106.9225.450.20.80.16
confirmed casesresults-v1.12.02026-07-255245.992.960.4661.09113.62106.9225.450.20.80.16
confirmed casesresults-14622026-07-265247.612.980.4581.06122.2799.4625.890.60.80.13
confirmed casesresults-14692026-07-275247.613.170.4581.08122.2799.4625.890.60.80.13
confirmed casesresults-14792026-07-285247.612.980.4581.07122.2799.4625.890.60.80.13
confirmed casesresults-14892026-07-315247.612.950.4581.06122.2799.4625.890.60.80.13
confirmed casesresults-v1.13.22026-08-045247.612.920.4581.05122.2799.4625.890.60.80.13
confirmed casesresults-15052026-08-085247.613.070.4581.08122.2799.4625.890.60.80.13
confirmed casesresults-15272026-08-105247.613.130.4581.1122.2799.4625.890.60.80.13
confirmed casesresults-15732026-08-155256.513.170.4711.1131.5899.0425.890.40.80.13
confirmed casesresults-15912026-08-175304.063.840.5091.285.78190.6927.590.20.60.3
confirmed casesresults-v1.15.02026-08-185304.063.690.5091.1685.78190.6927.590.20.60.3
confirmed casesresults-v1.16.02026-08-235318.573.870.5131.1995.65195.7327.190.20.40.3
confirmed casesresults-16252026-08-245318.423.940.5131.2195.56195.7327.130.20.40.3
confirmed casesresults-v1.17.02026-08-255278.343.370.4711.0982.69170.1325.510.20.40.28
confirmed casesresults-v1.18.02026-08-305278.343.310.4711.0882.69170.1325.510.20.40.28
confirmed casesresults-17392026-09-035288.73.570.4751.1192.73169.1126.860.20.60.22
confirmed casesresults-18282026-09-055285.093.440.5051.17102.17151.5831.3500.60.15
confirmed casesresults-18582026-09-065290.993.450.5091.1988.02171.6231.3500.60.14
confirmed casesresults-19702026-09-085295.583.650.5181.2396.92166.931.7700.60.15
confirmed casesresults-v2.1.02026-09-095295.583.680.5181.2196.92166.931.7700.60.15
confirmed casesresults-21772026-09-125311.383.920.5011.18106.74175.6928.9500.60.19
confirmed casesresults-23602026-09-145174.792.130.4220.9866.8474.4433.5100.80.07
confirmed casesresults-25062026-09-155140.971.650.3620.8258.1853.1829.610.20.80.02
confirmed casesresults-25362026-09-185135.091.610.3660.8548.9255.0831.090.20.80.03
confirmed casesresults-27202026-09-195161.241.930.3990.9256.2273.1231.8900.80.08
confirmed casesresults-v3.0.02026-09-205162.591.980.3980.9256.9674.2331.400.80.1
confirmed casesresults-29682026-09-255151.781.790.3960.9155.6563.7232.410.20.80.03
confirmed casesresults-30002026-09-295142.691.710.3790.8756.1455.4231.1200.80.06
confirmed casesresults-30862026-10-035141.831.720.3660.8656.6654.5130.660.20.80.03
confirmed casesresults-31402026-10-135150.451.840.3710.8757.5363.6329.290.20.80.04
confirmed deathsresults-13592026-07-16536.610.690.5530.728.040.078.490.60.8-0.39
confirmed deathsresults-13912026-07-18536.610.70.5530.7128.040.078.490.60.8-0.39
confirmed deathsresults-13942026-07-19536.610.70.5530.6928.040.078.490.60.8-0.39
confirmed deathsresults-14412026-07-20537.950.740.5790.7428.760.448.760.60.8-0.35
confirmed deathsresults-14462026-07-24537.950.730.5790.7428.760.448.760.60.8-0.35
confirmed deathsresults-v1.12.02026-07-25537.950.730.5790.7428.760.448.760.60.8-0.35
confirmed deathsresults-14622026-07-26537.320.710.6350.827.4809.840.60.8-0.43
confirmed deathsresults-14692026-07-27537.320.720.6350.827.4809.840.60.8-0.43
confirmed deathsresults-14792026-07-28537.320.720.6350.8127.4809.840.60.8-0.43
confirmed deathsresults-14892026-07-31537.320.720.6350.827.4809.840.60.8-0.43
confirmed deathsresults-v1.13.22026-08-04537.320.710.6350.827.4809.840.60.8-0.43
confirmed deathsresults-15052026-08-08537.320.730.6350.8227.4809.840.60.8-0.43
confirmed deathsresults-15272026-08-10537.320.720.6350.8227.4809.840.60.8-0.43
confirmed deathsresults-15732026-08-15537.790.740.6310.8127.840.149.820.60.8-0.4
confirmed deathsresults-15912026-08-17536.940.70.2880.3620.912.143.90.610
confirmed deathsresults-v1.15.02026-08-18536.940.70.2880.3720.912.143.90.610
confirmed deathsresults-v1.16.02026-08-23538.990.740.270.3524.1711.593.220.610.04
confirmed deathsresults-16252026-08-24538.960.750.270.3424.1511.593.220.610.05
confirmed deathsresults-v1.17.02026-08-25535.050.670.2560.3321.9610.142.940.61-0.03
confirmed deathsresults-v1.18.02026-08-30535.050.660.2560.3221.9610.142.940.61-0.03
confirmed deathsresults-17392026-09-03534.710.680.2740.3522.918.353.450.61-0.02
confirmed deathsresults-18282026-09-05528.720.560.2950.3820.393.265.070.60.8-0.21
confirmed deathsresults-18582026-09-06535.330.670.3140.421.978.265.10.60.8-0.17
confirmed deathsresults-19702026-09-08531.570.590.2960.3722.64.244.730.60.8-0.19
confirmed deathsresults-v2.1.02026-09-09531.570.60.2960.3822.64.244.730.60.8-0.19
confirmed deathsresults-21772026-09-12539.440.730.2930.3725.679.913.860.61-0.05
confirmed deathsresults-23602026-09-14541.870.790.2940.3723.5414.653.680.410.01
confirmed deathsresults-25062026-09-15528.50.540.2520.3120.714.243.550.81-0.1
confirmed deathsresults-25362026-09-18531.720.60.2750.3520.547.353.830.81-0.08
confirmed deathsresults-27202026-09-19533.370.630.2730.3420.938.923.520.61-0.01
confirmed deathsresults-v3.0.02026-09-20531.40.60.2610.3320.437.553.410.81-0.08
confirmed deathsresults-29682026-09-25530.290.580.2660.3418.558.23.540.61-0.06
confirmed deathsresults-30002026-09-29526.130.490.2470.3118.554.143.430.81-0.1
confirmed deathsresults-30862026-10-03529.860.580.2630.3421.015.323.540.81-0.1
confirmed deathsresults-31402026-10-13534.780.670.2710.3423.417.933.450.61-0.04
isolation bedsresults-13592026-07-16477.620.460.1580.4141.551.5434.540.51-0.22
isolation bedsresults-13912026-07-18477.620.460.1580.4141.551.5434.540.51-0.22
isolation bedsresults-13942026-07-19477.620.460.1580.4141.551.5434.540.51-0.22
isolation bedsresults-14412026-07-20477.320.460.1580.438.491.0237.810.51-0.22
isolation bedsresults-14462026-07-24477.320.460.1580.438.491.0237.810.51-0.22
isolation bedsresults-v1.12.02026-07-25477.320.460.1580.4138.491.0237.810.51-0.22
isolation bedsresults-14622026-07-26471.120.430.1440.3838.043.5329.560.51-0.2
isolation bedsresults-14692026-07-27471.120.420.1440.3738.043.5329.560.51-0.2
isolation bedsresults-14792026-07-28471.120.420.1440.3738.043.5329.560.51-0.2
isolation bedsresults-14892026-07-31471.120.410.1440.3638.043.5329.560.51-0.2
isolation bedsresults-v1.13.22026-08-04471.120.430.1440.3838.043.5329.560.51-0.2
isolation bedsresults-15052026-08-08471.120.430.1440.3838.043.5329.560.51-0.2
isolation bedsresults-15272026-08-10471.120.420.1440.3738.043.5329.560.51-0.2
isolation bedsresults-15732026-08-15476.50.460.1540.435.782.1538.570.51-0.22
isolation bedsresults-15912026-08-17473.460.430.1470.3736.12.1335.230.51-0.2
isolation bedsresults-v1.15.02026-08-18473.460.440.1470.3936.12.1335.230.51-0.2
isolation bedsresults-v1.16.02026-08-23473.460.460.1470.435.252.0536.160.51-0.2
isolation bedsresults-16252026-08-24473.460.440.1470.3835.252.0536.160.51-0.2
isolation bedsresults-v1.17.02026-08-25474.840.450.1510.439.271.4434.130.51-0.23
isolation bedsresults-v1.18.02026-08-30474.840.460.1510.439.271.4434.130.51-0.23
isolation bedsresults-17392026-09-03475.350.450.1550.440.750.9833.630.51-0.24
isolation bedsresults-18282026-09-05473.290.420.1490.3735.511.8135.980.51-0.24
isolation bedsresults-18582026-09-06467.650.390.1360.3436.961.3129.390.751-0.2
isolation bedsresults-19702026-09-08476.590.460.1560.437.540.8838.170.51-0.28
isolation bedsresults-v2.1.02026-09-09476.590.470.1560.4237.540.8838.170.51-0.28
isolation bedsresults-21772026-09-12476.80.450.1560.3934.071.341.430.51-0.24
isolation bedsresults-23602026-09-144205.61.250.2410.64126.5279.0800.2510.58
isolation bedsresults-25062026-09-154131.190.760.1790.4585.3645.8300.7510.46
isolation bedsresults-25362026-09-184101.70.60.1470.3772.2929.4100.7510.4
isolation bedsresults-27202026-09-194122.90.710.1670.4277.5545.3400.510.45
isolation bedsresults-v3.0.02026-09-204150.810.90.1990.5190.816000.510.54
isolation bedsresults-29682026-09-254138.520.80.1860.4683.2455.2800.510.49
isolation bedsresults-30002026-09-294121.410.730.1680.4477.6743.7400.510.43
isolation bedsresults-30862026-10-034141.150.860.1870.588.9952.1600.510.51
isolation bedsresults-31402026-10-134167.90.990.2140.5597.2370.6700.510.56
Frozen-fit forecasts-versus-now overlay
julia
# The frozen joint and the persistence baseline only. A single-stream
# frozen fit exists at the one-week-back cut-off alone, so its series lands
# on one made date of a panel spanning every cut-off, overplotting the
# joint point it sits beside rather than reading as a second series. The
# single-stream frozen fits are compared against the joint in the skill
# figures and in the validation plot at that cut-off.
frozen_overlay_fig = plot_forecast_overlay(
    scored_overlay(
        vcat(
            select_fit_role(frozen_overlay_df, "joint"),
            select_fit_role(frozen_overlay_df, "baseline")
        )
    )
);

The frozen forecasts made at each cut-off against the value observed since (black), one panel per stream and horizon by the cut-off each was made from. Each panel carries the frozen forecast and the persistence baseline, coloured as in the cross-release overlay above.

The frozen re-fits below freeze the renewal data to an earlier cut-off and re-fit, so that a change driven by newer data can be distinguished from one driven by a change of method. The one-week-back re-fit uses the headline sampler settings, and the re-fits at earlier cut-offs use the single-population frozen settings given in the methods.

Freeze the renewal data to a cut-off and re-fit
julia
# Frozen re-fits and released_df are prepared in the setup block above.

Individual fits against the baseline ​

This section scores each stream's own individual fit across releases, as forecast scoring across releases does the joint, against the same persistence baseline.

Individual-fit rows of the cross-release scores
julia
# The relative skill against a stream's individual fit is only ever
# computed on the joint model's row, so on these rows it is missing by
# construction and the column is dropped rather than shown empty.
individual_score_overview_table = drop_individual_fit_columns(
    select_fit_role(forecast_score_overview_table, "individual")
)
individual_score_by_horizon_table = drop_individual_fit_columns(
    select_fit_role(forecast_score_by_horizon_table, "individual")
)
# See the comment above `joint_score_by_release_table`'s assignment for why
# this setup chunk's last statement needs a trailing `;`.
individual_score_by_release_table = drop_individual_fit_columns(
    select_fit_role(forecast_score_by_release_table, "individual")
);
streamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
confirmed casesconfirmed126437.383.290.2832.85224.21212.240.920.640.770.39
confirmed deathsconfirmed_deaths126204.322.340.3662.97152.4418.133.780.731-0.12
isolation bedstreatment126112.31.950.1562.17101.160.5610.570.981-0.14
onset reportsonsets27191.411.440.3061.3210031.2660.150.590.93-0.05
julia
individual_relative_skill_fig = plot_forecast_relative_skill(
    individual_score_by_horizon_table;
    empty_message = "Empty: no release old enough for its targets to " *
        "have been observed carries an individual-stream " *
        "forecast. Not a missing forecast."
);

Relative skill against the baseline by horizon for each stream's own individual fit, one panel per stream.
Scores by horizon
streamhorizonfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
confirmed cases7confirmed34154.723.670.2713.3369.3583.571.790.620.740.36
confirmed cases14confirmed33325.113.510.2743.01153.94169.781.390.610.730.39
confirmed cases21confirmed31533.513.470.2852.86271.63261.620.260.650.770.41
confirmed cases28confirmed28806.493.020.3042.34442.58363.860.050.710.860.41
confirmed deaths7confirmed_deaths3460.252.430.3273.4639.842.6117.80.651-0.17
confirmed deaths14confirmed_deaths33135.172.450.3483.3695.278.3731.530.671-0.16
confirmed deaths21confirmed_deaths31241.972.30.3782.89180.3519.0942.520.741-0.12
confirmed deaths28confirmed_deaths28419.082.310.4182.44325.6647.2846.140.891-0.02
isolation beds7treatment34107.142.610.1563.0295.350.2611.5211-0.14
isolation beds14treatment33110.542.040.1532.29101.980.118.4511-0.11
isolation beds21treatment31112.71.740.1531.89101.440.4410.830.971-0.14
isolation beds28treatment28120.21.630.1631.77106.951.6111.640.931-0.17
onset reports7onsets13100.920.850.351.0663.6614.4422.830.770.850.08
onset reports14onsets8231.662.220.2682.22116.2844.4170.960.51-0.08
onset reports21onsets6333.791.650.261.61157.0350.16126.590.331-0.28
Scores by release
made_datestreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
2026-07-23confirmed casesconfirmed4327.810.850.2060.81319.434.843.5511-0.03
2026-07-25confirmed casesconfirmed4346.241.490.2131.51343.882.30.06110.02
2026-07-26confirmed casesconfirmed4299.071.370.2111.6291.390.167.5111-0.12
2026-07-27confirmed casesconfirmed4267.621.310.1711.25262.035.380.21110.04
2026-07-31confirmed casesconfirmed4402.872.920.2192.5383.8219.050110.09
2026-08-01confirmed casesconfirmed4336.312.830.2092.92322.9813.330110.08
2026-08-02confirmed casesconfirmed4296.982.330.1712.12286.156.724.1111-0.03
2026-08-03confirmed casesconfirmed4291.81.530.1751.23281.4210.080.3110.05
2026-08-04confirmed casesconfirmed4350.843.680.1913.26323.4127.40.04110.11
2026-08-07confirmed casesconfirmed4308.43.60.1922.79294.779.514.1211-0.01
2026-08-11confirmed casesconfirmed4335.757.660.1915.28307.327.620.84110.1
2026-08-15confirmed casesconfirmed4312.225.30.2114.19308.061.822.3411-0.03
2026-08-17confirmed casesconfirmed4299.384.030.2283.47293.3306.0611-0.09
2026-08-22confirmed casesconfirmed4335.334.910.2064.06293.0242.310110.15
2026-08-24confirmed casesconfirmed4315.974.780.1853.93253.0562.920110.26
2026-08-25confirmed casesconfirmed4302.154.470.1843.98236.9365.230110.27
2026-08-30confirmed casesconfirmed4384.814.680.2033.48243.44141.370110.37
2026-08-31confirmed casesconfirmed4298.733.120.1772.81219.1279.610110.26
2026-09-01confirmed casesconfirmed4350.743.690.1932.88251.3499.40110.29
2026-09-06confirmed casesconfirmed4352.022.80.2152.76256.3295.710110.33
2026-09-10confirmed casesconfirmed4448.082.460.2642.02245.86202.2200.2510.48
2026-09-12confirmed casesconfirmed41307.035.170.7473.77117.751189.280001
2026-09-13confirmed casesconfirmed41202.655.780.6944.27121.721080.920001
2026-09-15confirmed casesconfirmed41039.064.560.6573.31126.86912.210001
2026-09-16confirmed casesconfirmed49484.450.6173.4127.78820.220001
2026-09-19confirmed casesconfirmed4650.673.350.4822.92126.04524.63000.250.95
2026-09-21confirmed casesconfirmed4480.054.970.3664.83100.45379.61000.250.94
2026-09-22confirmed casesconfirmed4356.784.660.2874.97112.6244.18000.50.9
2026-09-25confirmed casesconfirmed3308.386.720.2794.2574.77233.6000.330.91
2026-09-26confirmed casesconfirmed3380.8810.330.3788.2767.39313.490000.96
2026-09-27confirmed casesconfirmed3212.424.130.192.4472.52139.90010.81
2026-10-02confirmed casesconfirmed2123.672.150.1552.0342.6181.060010.82
2026-10-06confirmed casesconfirmed2135.844.190.1623.745.7190.130010.77
2026-10-10confirmed casesconfirmed196.012.460.1732.2929.2366.780010.79
2026-07-23confirmed deathsconfirmed_deaths4701.544.320.4482.27383.29318.2500.2510.56
2026-07-25confirmed deathsconfirmed_deaths4237.781.790.2341.44214.2823.50110.18
2026-07-26confirmed deathsconfirmed_deaths4256.462.270.2551.99231.6524.810110.13
2026-07-27confirmed deathsconfirmed_deaths4310.272.770.2521.82280.8429.430110.21
2026-07-31confirmed deathsconfirmed_deaths4312.362.620.2892.04292.2120.080.07110.08
2026-08-01confirmed deathsconfirmed_deaths4280.922.380.2531.67261.4619.460110.1
2026-08-02confirmed deathsconfirmed_deaths4256.052.850.2762.47238.2316.880.94110.03
2026-08-03confirmed deathsconfirmed_deaths4224.51.720.2451.29212.210.831.46110.02
2026-08-04confirmed deathsconfirmed_deaths4248.773.560.2833.43242.124.292.3511-0.02
2026-08-07confirmed deathsconfirmed_deaths4231.53.30.3253.17223.681.026.7911-0.11
2026-08-11confirmed deathsconfirmed_deaths4244.612.230.33711.68233.591.959.0611-0.13
2026-08-15confirmed deathsconfirmed_deaths4244.835.940.345.47239.910.044.8911-0.1
2026-08-17confirmed deathsconfirmed_deaths4251.324.340.2973.49246.093.931.311-0.02
2026-08-22confirmed deathsconfirmed_deaths42422.540.4823.76219.73022.2711-0.18
2026-08-24confirmed deathsconfirmed_deaths4220.022.990.5436.03144.48075.5411-0.4
2026-08-25confirmed deathsconfirmed_deaths4247.193.40.7969.42151.31095.880.751-0.45
2026-08-30confirmed deathsconfirmed_deaths4260.373.170.9079.9129.820130.550.51-0.51
2026-08-31confirmed deathsconfirmed_deaths4247.042.180.7825.82144.820102.2211-0.4
2026-09-01confirmed deathsconfirmed_deaths4246.662.550.9278.12116.860129.80.51-0.48
2026-09-06confirmed deathsconfirmed_deaths4281.883.011.17110.5179.510202.3701-0.64
2026-09-10confirmed deathsconfirmed_deaths4207.511.570.7463.78124.24083.270.751-0.45
2026-09-12confirmed deathsconfirmed_deaths4100.940.720.150.6660.0640.8700.2510.54
2026-09-13confirmed deathsconfirmed_deaths494.040.780.1450.7655.4938.5500.2510.57
2026-09-15confirmed deathsconfirmed_deaths462.490.520.1080.5352.899.60110.31
2026-09-16confirmed deathsconfirmed_deaths457.360.560.1020.5852.384.9800.7510.26
2026-09-19confirmed deathsconfirmed_deaths454.290.610.0950.6548.661.34.32110.01
2026-09-21confirmed deathsconfirmed_deaths444.170.770.080.8831.160.212.811-0.2
2026-09-22confirmed deathsconfirmed_deaths468.071.60.1211.8227.69040.380.51-0.51
2026-09-25confirmed deathsconfirmed_deaths391.9130.2282.4720.35071.5601-0.82
2026-09-26confirmed deathsconfirmed_deaths348.0120.1091.8820.640.2327.140.331-0.39
2026-09-27confirmed deathsconfirmed_deaths362.672.860.1492.4720.1042.5701-0.65
2026-10-02confirmed deathsconfirmed_deaths243.182.140.1422.4314.79028.3901-0.72
2026-10-06confirmed deathsconfirmed_deaths247.972.170.1482.3714.74033.2301-0.79
2026-10-10confirmed deathsconfirmed_deaths114.380.710.0690.8410.7903.5911-0.32
2026-07-23isolation bedstreatment455.731.890.0892.347.7907.9411-0.21
2026-07-25isolation bedstreatment451.681.320.0821.5847.140.014.5211-0.14
2026-07-26isolation bedstreatment461.671.550.11.8654.820.236.6311-0.12
2026-07-27isolation bedstreatment459.472.180.0982.6654.6704.811-0.18
2026-07-31isolation bedstreatment482.762.010.1322.568.17014.60.751-0.28
2026-08-01isolation bedstreatment488.621.380.1531.7873.94014.670.751-0.25
2026-08-02isolation bedstreatment484.371.590.1411.9669.7014.670.751-0.28
2026-08-03isolation bedstreatment479.931.960.1342.4271.9108.0111-0.21
2026-08-04isolation bedstreatment487.061.350.1411.5382.731.223.1111-0.03
2026-08-07isolation bedstreatment4104.981.560.1711.9485.76019.2111-0.29
2026-08-11isolation bedstreatment4102.610.590.1710.6890.93011.6811-0.21
2026-08-15isolation bedstreatment4127.511.460.2041.8598.57028.9411-0.33
2026-08-17isolation bedstreatment4103.761.390.1611.6793.32010.4411-0.19
2026-08-22isolation bedstreatment4144.982.730.2183.39120.25024.7311-0.22
2026-08-24isolation bedstreatment4127.242.620.1813.01115.131.0511.0611-0.11
2026-08-25isolation bedstreatment4131.921.820.1912.11111.86020.0611-0.2
2026-08-30isolation bedstreatment4128.622.920.1713.13117.425.735.4711-0.02
2026-08-31isolation bedstreatment4120.612.820.163.12111.751.797.0711-0.07
2026-09-01isolation bedstreatment4120.72.930.1543.24116.430.683.611-0.05
2026-09-06isolation bedstreatment4124.132.490.1562.58120.731.811.5811-0.01
2026-09-10isolation bedstreatment4129.272.550.1582.64119.720.249.3111-0.06
2026-09-12isolation bedstreatment4134.162.240.1812.69121.99012.1611-0.13
2026-09-13isolation bedstreatment4121.642.210.1572.53120.420.021.211-0.04
2026-09-15isolation bedstreatment4132.572.430.162.74125.50.316.7511-0.06
2026-09-16isolation bedstreatment4130.443.260.163.62123.530.26.711-0.1
2026-09-19isolation bedstreatment4126.152.410.1492.48124.251.850.05110.03
2026-09-21isolation bedstreatment4136.462.390.1552.35134.531.030.91110.01
2026-09-22isolation bedstreatment4182.972.620.2433.22143.66039.3211-0.26
2026-09-25isolation bedstreatment3131.322.610.1512.52127.472.091.7611-0.01
2026-09-26isolation bedstreatment3118.861.510.1421.49112.750.056.0611-0.09
2026-09-27isolation bedstreatment3139.6420.1792.13126.23013.411-0.17
2026-10-02isolation bedstreatment2125.252.270.152.37120.5204.7311-0.12
2026-10-06isolation bedstreatment2139.191.930.1662.31110.52028.6711-0.29
2026-10-10isolation bedstreatment1123.474.60.1465.14118.470511-0.12
2026-08-02onset reportsonsets3122.90.90.1881.1396.84026.0611-0.28
2026-08-03onset reportsonsets3105.391.590.1661.28100.321.153.9211-0.02
2026-08-04onset reportsonsets3141.841.110.2661.2288.340.0453.460.671-0.26
2026-08-07onset reportsonsets3378.481.140.6331.3982.320296.1601-0.8
2026-08-11onset reportsonsets3243.781.420.4291.6985.910157.870.331-0.64
2026-08-15onset reportsonsets3369.234.410.2772.54205.04164.1800.3310.57
2026-08-17onset reportsonsets2246.4940.2552.25153.6192.8800.510.47
2026-08-22onset reportsonsets2120.981.150.211.39104.1216.870110.22
2026-08-24onset reportsonsets185.161.240.3922.582.0903.0711-0.12
2026-08-25onset reportsonsets195.660.40.5110.8688.577.090110.18
2026-09-21onset reportsonsets172.061.120.2360.7610.9761.090000.98
2026-09-22onset reportsonsets172.991.240.2370.5612.8360.160000.98
2026-10-02onset reportsonsets122.280.340.0770.3413.7108.5711-0.43

Saving forecast outputs ​

The one-week-back validation forecast is written in the release forecast's archive format, so it is recorded as a release asset alongside the forecast it is scored against.

Write forecast outputs
julia
output_dir = get(
    ENV, "BVD_OUTPUT_DIR",
    joinpath(pkgdir(BVDOutbreakSize), "output")
)
mkpath(output_dir)
CSV.write(
    joinpath(output_dir, "forecast_validation.csv"),
    forecast_archive(
        [(7, validation_forecast)];
        made_date = frozen_lastweek.o.cutoff, thin = 5
    )
)
"/home/runner/work/BVDOutbreakSize/BVDOutbreakSize/output/forecast_validation.csv"
Write the summary bullets
julia
# The bullets under the summary heading at the top of the page. They read
# tables built further down, so they are written here and read back when
# the site is assembled.
evaluation_forecast_national_summary = let
    fmt(x) = ismissing(x) || !isfinite(x) ? "n/a" :
        string(round(x; digits = 2))
    scored(tbl) = filter(r -> !ismissing(r.rel_to_baseline), tbl)
    function beat(label, tbl)
        rows = scored(tbl)
        size(rows, 1) == 0 &&
            return string("- **", label, ":** no scored forecasts yet.")
        return string(
            "- **", label, ":** beat the baseline on ",
            count(<(1), rows.rel_to_baseline), " of ", size(rows, 1),
            " streams."
        )
    end
    function block(lead, tbl)
        rows = scored(tbl)
        size(rows, 1) == 0 && return nothing
        return join(
            vcat(
                [string("**", lead, "**"), ""],
                [
                    string(
                        "- ", r.stream, ": relative skill ",
                        fmt(r.rel_to_baseline), ", 90% coverage ",
                        fmt(r.coverage_90), ", bias ", fmt(r.bias), " over ",
                        r.n, " forecasts."
                    )
                        for r in eachrow(rows)
                ]
            ), "\n"
        )
    end
    frozen_joint = select_fit_role(frozen_score_overview_table, "joint")
    overall = [
        beat("Joint model across releases", joint_score_overview_table),
        beat("Frozen joint model", frozen_joint),
    ]
    blocks = filter(
        !isnothing,
        [
            block("Across releases", joint_score_overview_table),
            block("Frozen fits", frozen_joint),
        ]
    )
    join(vcat([join(overall, "\n")], blocks), "\n\n")
end
dashboard_dir = joinpath(
    pkgdir(BVDOutbreakSize), "docs", "src", "summary_assets"
)
mkpath(dashboard_dir)
write(
    joinpath(dashboard_dir, "evaluation_forecast_national.md"),
    evaluation_forecast_national_summary
);