Sensitivity and comparison analyses
This page continues the main analysis from the one-week-ahead forecast onward: how last week's forecast held up, how every release's forecasts scored against what has since been observed, the reproduction number by release, the outbreak size each data stream implies on its own, how the estimate has evolved across releases, comparisons with McCabe et al. and Chamla et al., and the delay and tree-prior sensitivity re-fits. The tree-prior sensitivity compares the Skygrid and Exponential growth TMRCA estimates (Mbala-Kingebeni and others, 2026) (~1.1E-3 subs/site/year, 139 BDBV genomes). It renders from the same fitted chains as the main analysis, loaded through the shared setup, so no model is re-fit here beyond the frozen and (gated) sensitivity re-fits.
Load packages, data and fitted chains
# Shared setup: packages, observations, the fit registry and every model fit
# (loaded from the content-addressed cache). See docs/examples/_setup.jl.
using BVDOutbreakSize
include(joinpath(pkgdir(BVDOutbreakSize), "docs", "examples", "_setup.jl"))45Forecast validation (last week versus now)
How last week's forecast held up against the data since observed, using the frozen re-fit and one-week projection defined in the methods forecast-versus-frozen evaluation. The frozen fit also conditions on the isolation beds, so the projected bed occupancy is scored against the beds held a week later. The bed validation 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, widening the projected bed interval. Unlike the scores further down, the confirmed new-count rows here keep any retrospective harmonisation step the week contained, so a week holding one reads high against a forecast that never predicted it.
Fit one week back and validate the one-week-ahead forecast
# frozen_lastweek and frozen_lastweek_streams are computed in the setup
# block above.
# `obs_recovered` is passed so the frozen fit's forecast carries a
# `recovered_new` column (materialised only when the recovered origin is
# given), letting the recovered stream be scored against the observed count
# below like the other streams.
validation_forecast = forecast_reported(frozen_lastweek.chn;
horizon = 7,
obs_cases = frozen_lastweek.o.reported_cases,
obs_deaths = frozen_lastweek.o.total_deaths,
obs_confirmed = frozen_lastweek.o.confirmed_cases,
obs_confirmed_deaths = frozen_lastweek.o.confirmed_deaths,
obs_recovered = frozen_lastweek.o.recovered_cases);
# 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.
function _validation_individual_new(sid, stream::Symbol, obs_value)
f = frozen_lastweek_streams[sid]
bp = f.o.n - f.o.who_first_sitrep_days
return Float64.(forecast_stream(f.chn, stream; horizon = 7,
obs_value = obs_value, n = f.o.n, breakpoint = bp,
rt_start = 1, rt_walk_start = 1))
end
validation_individual = (;
cases_new = _validation_individual_new(
"cases", :reported_cases,
frozen_lastweek_streams["cases"].o.reported_cases),
deaths_new = _validation_individual_new(
"deaths", :suspected_deaths,
frozen_lastweek_streams["deaths"].o.total_deaths),
confirmed_new = _validation_individual_new(
"confirmed", :confirmed_cases,
frozen_lastweek_streams["confirmed"].o.confirmed_cases),
confirmed_deaths_new = _validation_individual_new(
"confirmed_deaths", :confirmed_deaths,
frozen_lastweek_streams["confirmed_deaths"].o.confirmed_deaths))
# The frozen individual (treatment-only) fit's own bed-occupancy forecast,
# anchored on the beds occupied at ITS OWN cut-off (the frozen fit's own
# `o`, not the current `obs`), matching how the joint frozen forecast is
# itself anchored.
_validation_iso_obs = let f = frozen_lastweek_streams["treatment"]
isempty(f.o.isolation_history.counts) ? 0.0 :
Float64(f.o.isolation_history.counts[end])
end
validation_individual_isolation = _validation_individual_new(
"treatment", :isolation_beds, _validation_iso_obs)
# 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.
_obs_beds = isempty(obs.isolation_history.counts) ? missing :
obs.isolation_history.counts[end]
# 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.
validation_table = forecast_vs_truth(validation_forecast;
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),
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),
isolation = _obs_beds);Forecast-versus-observed validation table
| Row | Stream | Quantity | Observed | Lower 90% | Lower 60% | Lower 30% | Upper 30% | Upper 60% | Upper 90% | Within 90% PI |
|---|---|---|---|---|---|---|---|---|---|---|
| String | String | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | String | |
| 1 | DRC reported cases | cumulative by T+7 | 1077.0 | 1335.0 | 1995.0 | 2697.0 | 4470.0 | 6482.0 | 10981.0 | no |
| 2 | DRC reported cases | new this week | 0.0 | 258.0 | 918.0 | 1620.0 | 3393.0 | 5405.0 | 9904.0 | no |
| 3 | DRC suspected deaths | cumulative by T+7 | 246.0 | 308.0 | 456.0 | 644.0 | 1070.0 | 1544.0 | 2925.0 | no |
| 4 | DRC suspected deaths | new this week | 0.0 | 62.0 | 210.0 | 398.0 | 824.0 | 1298.0 | 2679.0 | no |
| 5 | DRC confirmed cases | cumulative by T+7 | 3748.0 | 3446.0 | 3653.0 | 3768.0 | 4072.0 | 4293.0 | 4817.0 | yes |
| 6 | DRC confirmed cases | new this week | 548.0 | 246.0 | 453.0 | 568.0 | 872.0 | 1093.0 | 1617.0 | yes |
| 7 | DRC confirmed deaths | cumulative by T+7 | 1657.0 | 308.0 | 456.0 | 644.0 | 1070.0 | 1490.0 | 1903.0 | yes |
| 8 | DRC confirmed deaths | new this week | 252.0 | 0.0 | 0.0 | 0.0 | 0.0 | 85.0 | 498.0 | yes |
| 9 | DRC recovered | cumulative by T+7 | 708.0 | 571.0 | 602.0 | 670.0 | 845.0 | 977.0 | 1469.0 | yes |
| 10 | DRC recovered | new this week | 137.0 | 0.0 | 31.0 | 99.0 | 274.0 | 406.0 | 898.0 | yes |
| 11 | DRC isolation beds | occupancy at T+7 | 690.0 | 735.0 | 862.0 | 879.0 | 897.0 | 907.0 | 925.0 | no |
The observation panels histogram the one-week-ahead forecast made from the frozen fit, a cumulative and a new-count panel for each fitted count stream the forecast carries (reported cases, suspected deaths, confirmed cases, confirmed deaths and recovered), with the 90% predictive interval shaded and the count actually observed by the current cut-off drawn as a dashed black rule. Each stream draws only when the forecast carries its column, so a fit observing fewer streams shows fewer panels. Where a stream has its own individual (single-stream) fit, that fit's own forecast, made from the same frozen cut-off, is overlaid as a dotted density alongside the joint's histogram, so both are visible rather than the joint alone; recovered has no individual fit and draws the joint alone.
Forecast-versus-observed plot
# 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 panel is scored against observed minus origin.
validation_fig = plot_forecast_vs_truth(validation_forecast;
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),
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),
individual = validation_individual);The bed panel scores last week's projected occupancy against the beds occupied now (the dashed rule), the individual (treatment-only) fit's own projection overlaid as a dotted density alongside the joint.
Bed forecast-versus-observed plot
validation_beds_fig = plot_forecast_beds_vs_truth(validation_forecast;
isolation = _obs_beds, individual = validation_individual_isolation);The latent quantities are not observed, so they are scored distribution against distribution: what the frozen fit forecast for the past week's new infections, onsets and deaths against what the current fit now estimates for the same window.
Forecast-versus-now latent plot
# 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);Forecast scoring across releases
How every release's saved one- to four-week-ahead forecast scored against the data observed since, against a persistence baseline and, where one exists, the stream's own individual fit as well as the joint (see forecast scoring against a persistence baseline for how the scores, the relative skill and the baseline are built). Recovered has no individual fit of its own, so its comparison is the baseline against the joint only. Reported cases and suspected deaths stopped being updated by the situation reports partway through the outbreak, and exports' confirmed-detection series is anchored to an earlier cut-off, so exports contributes no scored forecast at all and reported cases and suspected deaths each rest on exactly one matched forecast, a single window rather than a settled sample.
Only a minority of the daily releases examined across the outbreak contribute a row to the table below, and every one of them is a reconstruction of an earlier version of the model rather than the model as it stands today, so the table below is not a verdict on the current fit. One reconstruction is dropped from scoring entirely: its chain forecasts a near-zero median at every horizon and stream, with the upper predictive tail occasionally reaching five- and six-digit values, which is the signature of a chain that failed to sample properly rather than a genuine forecast (see scripts/score_releases.jl, _FAILED_RECONSTRUCTIONS). The releases that do carry the current model's own individual-stream forecasts are too recent for their targets to be observed yet, which is why the comparison against each stream's individual fit is still empty. It will populate once those targets resolve. Every row also rests on one to a handful of matched forecasts, shown as its own count rather than rounded away, so a ratio here should be read as an early signal rather than a settled result.
Load and summarise the cross-release forecast scores
# scripts/score_releases.jl writes these after the fits. The committed files
# are header-only until a release carries the asset, so the common path
# reads a real file to a zero-row frame; the typed `schema` is the fallback
# for a file that is absent entirely, since CSV.read throws on a missing
# path and would take the whole docs build with it.
function _release_data(name, schema::NamedTuple)
path = joinpath(pkgdir(BVDOutbreakSize), "data", name)
isfile(path) && return CSV.read(path, DataFrame)
return DataFrame([k => T[] for (k, T) in pairs(schema)])
end
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 headline table, 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).
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)| Row | made_date | stream | fit | n | crps | rel_to_baseline | log_crps | log_rel_to_baseline | rel_to_individual | log_rel_to_individual | dispersion | overprediction | underprediction | coverage_50 | coverage_90 | bias |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | String31 | String31 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64? | Float64? | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | 2026-06-07 | confirmed cases | joint | 4 | 826.77 | 5.62 | 0.823 | 3.74 | missing | missing | 765.65 | 61.12 | 0.0 | 1.0 | 1.0 | 0.2 |
| 2 | 2026-06-10 | confirmed cases | joint | 4 | 833.23 | 6.5 | 0.758 | 4.06 | missing | missing | 666.38 | 166.85 | 0.0 | 1.0 | 1.0 | 0.31 |
| 3 | 2026-07-01 | confirmed cases | joint | 4 | 15862.8 | 77.9 | 0.583 | 2.85 | missing | missing | 15377.3 | 485.47 | 0.0 | 0.0 | 1.0 | 0.66 |
| 4 | 2026-07-06 | confirmed cases | joint | 3 | 279.41 | 1.51 | 0.267 | 1.24 | missing | missing | 190.63 | 88.78 | 0.0 | 0.33 | 1.0 | 0.52 |
| 5 | 2026-07-08 | confirmed cases | joint | 3 | 266.99 | 1.82 | 0.248 | 1.45 | missing | missing | 163.25 | 103.73 | 0.0 | 0.33 | 1.0 | 0.51 |
| 6 | 2026-07-23 | confirmed cases | confirmed | 1 | 100.25 | 2.4 | 0.201 | 2.76 | missing | missing | 86.6 | 0.0 | 13.65 | 1.0 | 1.0 | -0.24 |
| 7 | 2026-07-23 | confirmed cases | joint | 1 | 82.58 | 1.98 | 0.142 | 1.95 | 0.82 | 0.71 | 80.09 | 0.0 | 2.49 | 1.0 | 1.0 | -0.11 |
| 8 | 2026-07-25 | confirmed cases | confirmed | 1 | 107.9 | 3.27 | 0.198 | 3.43 | missing | missing | 107.74 | 0.0 | 0.16 | 1.0 | 1.0 | -0.02 |
| 9 | 2026-07-25 | confirmed cases | joint | 1 | 72.17 | 2.19 | 0.131 | 2.27 | 0.67 | 0.66 | 72.06 | 0.11 | 0.0 | 1.0 | 1.0 | 0.02 |
| 10 | 2026-06-07 | confirmed deaths | joint | 4 | 120.57 | 0.99 | 0.725 | 1.07 | missing | missing | 117.8 | 0.0 | 2.76 | 1.0 | 1.0 | -0.11 |
| 11 | 2026-06-10 | confirmed deaths | joint | 4 | 144.92 | 1.1 | 0.598 | 1.06 | missing | missing | 140.54 | 3.76 | 0.62 | 1.0 | 1.0 | 0.02 |
| 12 | 2026-07-01 | confirmed deaths | joint | 4 | 17196.0 | 79.42 | 0.549 | 1.2 | missing | missing | 17103.1 | 92.89 | 0.0 | 1.0 | 1.0 | 0.28 |
| 13 | 2026-07-06 | confirmed deaths | joint | 3 | 150.06 | 1.11 | 0.96 | 3.37 | missing | missing | 71.58 | 0.0 | 78.49 | 0.0 | 1.0 | -0.53 |
| 14 | 2026-07-08 | confirmed deaths | joint | 3 | 138.4 | 1.08 | 0.816 | 2.86 | missing | missing | 57.07 | 0.0 | 81.33 | 0.0 | 1.0 | -0.64 |
| 15 | 2026-07-23 | confirmed deaths | confirmed_deaths | 1 | 110.13 | 5.38 | 0.281 | 4.11 | missing | missing | 82.57 | 27.56 | 0.0 | 1.0 | 1.0 | 0.33 |
| 16 | 2026-07-23 | confirmed deaths | joint | 1 | 84.6 | 4.13 | 0.472 | 6.91 | 0.77 | 1.68 | 40.54 | 0.0 | 44.06 | 1.0 | 1.0 | -0.49 |
| 17 | 2026-07-25 | confirmed deaths | confirmed_deaths | 1 | 56.36 | 2.26 | 0.201 | 2.26 | missing | missing | 53.83 | 2.53 | 0.0 | 1.0 | 1.0 | 0.11 |
| 18 | 2026-07-25 | confirmed deaths | joint | 1 | 79.84 | 3.21 | 0.491 | 5.53 | 1.42 | 2.45 | 33.04 | 0.0 | 46.8 | 0.0 | 1.0 | -0.52 |
| 19 | 2026-07-01 | isolation beds | joint | 4 | 90.19 | 1.5 | 0.138 | 1.54 | missing | missing | 13.48 | 0.0 | 76.71 | 0.0 | 0.0 | -0.99 |
| 20 | 2026-07-06 | isolation beds | joint | 3 | 42.99 | 0.59 | 0.063 | 0.58 | missing | missing | 18.38 | 0.0 | 24.61 | 0.0 | 1.0 | -0.69 |
| 21 | 2026-07-08 | isolation beds | joint | 3 | 49.21 | 1.51 | 0.065 | 1.49 | missing | missing | 10.96 | 38.24 | 0.0 | 0.0 | 1.0 | 0.81 |
| 22 | 2026-07-23 | isolation beds | joint | 1 | 65.2 | 3.33 | 0.081 | 3.16 | 0.82 | 0.6 | 22.47 | 42.73 | 0.0 | 0.0 | 1.0 | 0.64 |
| 23 | 2026-07-23 | isolation beds | treatment | 1 | 79.34 | 4.05 | 0.136 | 5.3 | missing | missing | 58.35 | 0.0 | 20.99 | 1.0 | 1.0 | -0.39 |
| 24 | 2026-07-25 | isolation beds | joint | 1 | 98.17 | 1.99 | 0.128 | 1.91 | 1.77 | 1.33 | 23.93 | 74.24 | 0.0 | 0.0 | 1.0 | 0.78 |
| 25 | 2026-07-25 | isolation beds | treatment | 1 | 55.52 | 1.12 | 0.096 | 1.43 | missing | missing | 55.49 | 0.03 | 0.0 | 1.0 | 1.0 | 0.02 |
| 26 | 2026-07-01 | recovered | joint | 4 | 10457.7 | 153.97 | 0.539 | 1.89 | missing | missing | 10409.1 | 48.57 | 0.0 | 1.0 | 1.0 | 0.29 |
| 27 | 2026-07-06 | recovered | joint | 3 | 56.79 | 0.97 | 0.4 | 1.1 | missing | missing | 39.82 | 0.0 | 16.96 | 1.0 | 1.0 | -0.37 |
| 28 | 2026-07-08 | recovered | joint | 3 | 44.34 | 1.18 | 0.288 | 1.48 | missing | missing | 29.46 | 0.0 | 14.87 | 1.0 | 1.0 | -0.4 |
| 29 | 2026-05-18 | reported cases | joint | 1 | 231.14 | 0.48 | 1.87 | 0.3 | missing | missing | 61.15 | 0.0 | 169.99 | 0.0 | 1.0 | -0.66 |
| 30 | 2026-05-18 | suspected deaths | joint | 1 | 41.59 | 0.39 | 0.577 | 0.12 | missing | missing | 41.12 | 0.0 | 0.47 | 1.0 | 1.0 | -0.08 |
The headline pools every horizon and release into one row per stream and fit: the mean CRPS and its decomposition, coverage and bias, and the relative skill against the persistence baseline for every fit and, on the joint row where the stream has one, against its own individual fit, on both the natural and the log scale (column definitions in forecast scoring against a persistence baseline).
| Row | stream | fit | n | crps | rel_to_baseline | log_crps | log_rel_to_baseline | rel_to_individual | log_rel_to_individual | dispersion | overprediction | underprediction | coverage_50 | coverage_90 | bias |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| String31 | String31 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64? | Float64? | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | confirmed cases | confirmed | 2 | 104.08 | 2.79 | 0.199 | 3.06 | missing | missing | 97.17 | 0.0 | 6.9 | 1.0 | 1.0 | -0.13 |
| 2 | confirmed cases | joint | 20 | 3594.25 | 24.07 | 0.524 | 2.8 | 0.74 | 0.68 | 3422.56 | 171.57 | 0.12 | 0.6 | 1.0 | 0.39 |
| 3 | confirmed deaths | confirmed_deaths | 2 | 83.24 | 3.67 | 0.241 | 3.06 | missing | missing | 68.2 | 15.05 | 0.0 | 1.0 | 1.0 | 0.22 |
| 4 | confirmed deaths | joint | 20 | 3543.79 | 26.11 | 0.689 | 1.59 | 0.99 | 2.0 | 3495.27 | 19.33 | 29.19 | 0.65 | 1.0 | -0.19 |
| 5 | isolation beds | joint | 12 | 66.73 | 1.28 | 0.095 | 1.26 | 1.21 | 0.9 | 15.7 | 19.31 | 31.72 | 0.0 | 0.67 | -0.18 |
| 6 | isolation beds | treatment | 2 | 67.43 | 1.96 | 0.116 | 2.5 | missing | missing | 56.92 | 0.02 | 10.5 | 1.0 | 1.0 | -0.18 |
| 7 | recovered | joint | 10 | 4213.4 | 75.26 | 0.422 | 1.5 | missing | missing | 4184.43 | 19.43 | 9.55 | 1.0 | 1.0 | -0.11 |
| 8 | reported cases | joint | 1 | 231.14 | 0.48 | 1.87 | 0.3 | missing | missing | 61.15 | 0.0 | 169.99 | 0.0 | 1.0 | -0.66 |
| 9 | suspected deaths | joint | 1 | 41.59 | 0.39 | 0.577 | 0.12 | missing | missing | 41.12 | 0.0 | 0.47 | 1.0 | 1.0 | -0.08 |
The same relative skill against the baseline, by horizon: one panel per stream, one series per fit role, on a log-scaled skill axis with the reference line at one. A fit that beats the baseline on average but not at every cut-off is visible as a series that crosses the line rather than sitting under it throughout.
forecast_relative_skill_fig = plot_forecast_relative_skill(
forecast_score_by_horizon_table);The same columns as a table, broken out by horizon, and again broken out by release and averaged across horizons, are behind the two dropdowns below.
Scores by horizon
| Row | stream | horizon | fit | n | crps | rel_to_baseline | log_crps | log_rel_to_baseline | rel_to_individual | log_rel_to_individual | dispersion | overprediction | underprediction | coverage_50 | coverage_90 | bias |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| String31 | Int64 | String31 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64? | Float64? | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | confirmed cases | 7 | confirmed | 2 | 104.08 | 2.79 | 0.199 | 3.06 | missing | missing | 97.17 | 0.0 | 6.9 | 1.0 | 1.0 | -0.13 |
| 2 | confirmed cases | 7 | joint | 7 | 161.14 | 4.8 | 0.463 | 4.78 | 0.74 | 0.68 | 122.28 | 38.5 | 0.36 | 0.57 | 1.0 | 0.31 |
| 3 | confirmed cases | 14 | joint | 5 | 413.68 | 5.56 | 0.513 | 4.27 | missing | missing | 292.87 | 120.81 | 0.0 | 0.6 | 1.0 | 0.42 |
| 4 | confirmed cases | 21 | joint | 5 | 987.69 | 4.13 | 0.504 | 2.02 | missing | missing | 756.12 | 231.57 | 0.0 | 0.6 | 1.0 | 0.42 |
| 5 | confirmed cases | 28 | joint | 3 | 21250.1 | 53.91 | 0.713 | 1.76 | missing | missing | 20783.4 | 466.65 | 0.0 | 0.67 | 1.0 | 0.45 |
| 6 | confirmed deaths | 7 | confirmed_deaths | 2 | 83.24 | 3.67 | 0.241 | 3.06 | missing | missing | 68.2 | 15.05 | 0.0 | 1.0 | 1.0 | 0.22 |
| 7 | confirmed deaths | 7 | joint | 7 | 70.44 | 3.81 | 0.853 | 6.02 | 0.99 | 2.0 | 39.99 | 0.8 | 29.65 | 0.57 | 1.0 | -0.35 |
| 8 | confirmed deaths | 14 | joint | 5 | 152.51 | 1.91 | 0.639 | 2.11 | missing | missing | 110.13 | 6.96 | 35.42 | 0.6 | 1.0 | -0.2 |
| 9 | confirmed deaths | 21 | joint | 5 | 432.87 | 1.9 | 0.558 | 0.9 | missing | missing | 371.55 | 21.83 | 39.49 | 0.6 | 1.0 | -0.14 |
| 10 | confirmed deaths | 28 | joint | 3 | 22485.3 | 64.54 | 0.607 | 0.6 | missing | missing | 22405.7 | 79.0 | 0.6 | 1.0 | 1.0 | 0.14 |
| 11 | isolation beds | 7 | joint | 5 | 74.11 | 1.42 | 0.099 | 1.36 | 1.21 | 0.9 | 15.91 | 31.4 | 26.8 | 0.0 | 0.8 | 0.1 |
| 12 | isolation beds | 7 | treatment | 2 | 67.43 | 1.96 | 0.116 | 2.5 | missing | missing | 56.92 | 0.02 | 10.5 | 1.0 | 1.0 | -0.18 |
| 13 | isolation beds | 14 | joint | 3 | 56.17 | 1.12 | 0.08 | 1.09 | missing | missing | 12.97 | 14.35 | 28.85 | 0.0 | 0.67 | -0.26 |
| 14 | isolation beds | 21 | joint | 3 | 54.83 | 1.03 | 0.081 | 1.03 | missing | missing | 16.37 | 10.55 | 27.91 | 0.0 | 0.67 | -0.29 |
| 15 | isolation beds | 28 | joint | 1 | 97.17 | 1.79 | 0.161 | 1.95 | missing | missing | 20.79 | 0.0 | 76.38 | 0.0 | 0.0 | -0.99 |
| 16 | recovered | 7 | joint | 3 | 26.96 | 2.06 | 0.319 | 1.68 | missing | missing | 20.93 | 1.28 | 4.76 | 1.0 | 1.0 | -0.18 |
| 17 | recovered | 14 | joint | 3 | 89.7 | 2.13 | 0.393 | 1.56 | missing | missing | 68.01 | 5.81 | 15.87 | 1.0 | 1.0 | -0.23 |
| 18 | recovered | 21 | joint | 3 | 297.16 | 3.5 | 0.422 | 1.24 | missing | missing | 269.9 | 16.05 | 11.2 | 1.0 | 1.0 | -0.09 |
| 19 | recovered | 28 | joint | 1 | 40892.6 | 293.62 | 0.818 | 1.75 | missing | missing | 40767.7 | 124.85 | 0.0 | 1.0 | 1.0 | 0.35 |
| 20 | reported cases | 7 | joint | 1 | 231.14 | 0.48 | 1.87 | 0.3 | missing | missing | 61.15 | 0.0 | 169.99 | 0.0 | 1.0 | -0.66 |
| 21 | suspected deaths | 7 | joint | 1 | 41.59 | 0.39 | 0.577 | 0.12 | missing | missing | 41.12 | 0.0 | 0.47 | 1.0 | 1.0 | -0.08 |
Scores by release
| Row | made_date | stream | fit | n | crps | rel_to_baseline | log_crps | log_rel_to_baseline | rel_to_individual | log_rel_to_individual | dispersion | overprediction | underprediction | coverage_50 | coverage_90 | bias |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | String31 | String31 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64? | Float64? | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | 2026-06-07 | confirmed cases | joint | 4 | 826.77 | 5.62 | 0.823 | 3.74 | missing | missing | 765.65 | 61.12 | 0.0 | 1.0 | 1.0 | 0.2 |
| 2 | 2026-06-10 | confirmed cases | joint | 4 | 833.23 | 6.5 | 0.758 | 4.06 | missing | missing | 666.38 | 166.85 | 0.0 | 1.0 | 1.0 | 0.31 |
| 3 | 2026-07-01 | confirmed cases | joint | 4 | 15862.8 | 77.9 | 0.583 | 2.85 | missing | missing | 15377.3 | 485.47 | 0.0 | 0.0 | 1.0 | 0.66 |
| 4 | 2026-07-06 | confirmed cases | joint | 3 | 279.41 | 1.51 | 0.267 | 1.24 | missing | missing | 190.63 | 88.78 | 0.0 | 0.33 | 1.0 | 0.52 |
| 5 | 2026-07-08 | confirmed cases | joint | 3 | 266.99 | 1.82 | 0.248 | 1.45 | missing | missing | 163.25 | 103.73 | 0.0 | 0.33 | 1.0 | 0.51 |
| 6 | 2026-07-23 | confirmed cases | confirmed | 1 | 100.25 | 2.4 | 0.201 | 2.76 | missing | missing | 86.6 | 0.0 | 13.65 | 1.0 | 1.0 | -0.24 |
| 7 | 2026-07-23 | confirmed cases | joint | 1 | 82.58 | 1.98 | 0.142 | 1.95 | 0.82 | 0.71 | 80.09 | 0.0 | 2.49 | 1.0 | 1.0 | -0.11 |
| 8 | 2026-07-25 | confirmed cases | confirmed | 1 | 107.9 | 3.27 | 0.198 | 3.43 | missing | missing | 107.74 | 0.0 | 0.16 | 1.0 | 1.0 | -0.02 |
| 9 | 2026-07-25 | confirmed cases | joint | 1 | 72.17 | 2.19 | 0.131 | 2.27 | 0.67 | 0.66 | 72.06 | 0.11 | 0.0 | 1.0 | 1.0 | 0.02 |
| 10 | 2026-06-07 | confirmed deaths | joint | 4 | 120.57 | 0.99 | 0.725 | 1.07 | missing | missing | 117.8 | 0.0 | 2.76 | 1.0 | 1.0 | -0.11 |
| 11 | 2026-06-10 | confirmed deaths | joint | 4 | 144.92 | 1.1 | 0.598 | 1.06 | missing | missing | 140.54 | 3.76 | 0.62 | 1.0 | 1.0 | 0.02 |
| 12 | 2026-07-01 | confirmed deaths | joint | 4 | 17196.0 | 79.42 | 0.549 | 1.2 | missing | missing | 17103.1 | 92.89 | 0.0 | 1.0 | 1.0 | 0.28 |
| 13 | 2026-07-06 | confirmed deaths | joint | 3 | 150.06 | 1.11 | 0.96 | 3.37 | missing | missing | 71.58 | 0.0 | 78.49 | 0.0 | 1.0 | -0.53 |
| 14 | 2026-07-08 | confirmed deaths | joint | 3 | 138.4 | 1.08 | 0.816 | 2.86 | missing | missing | 57.07 | 0.0 | 81.33 | 0.0 | 1.0 | -0.64 |
| 15 | 2026-07-23 | confirmed deaths | confirmed_deaths | 1 | 110.13 | 5.38 | 0.281 | 4.11 | missing | missing | 82.57 | 27.56 | 0.0 | 1.0 | 1.0 | 0.33 |
| 16 | 2026-07-23 | confirmed deaths | joint | 1 | 84.6 | 4.13 | 0.472 | 6.91 | 0.77 | 1.68 | 40.54 | 0.0 | 44.06 | 1.0 | 1.0 | -0.49 |
| 17 | 2026-07-25 | confirmed deaths | confirmed_deaths | 1 | 56.36 | 2.26 | 0.201 | 2.26 | missing | missing | 53.83 | 2.53 | 0.0 | 1.0 | 1.0 | 0.11 |
| 18 | 2026-07-25 | confirmed deaths | joint | 1 | 79.84 | 3.21 | 0.491 | 5.53 | 1.42 | 2.45 | 33.04 | 0.0 | 46.8 | 0.0 | 1.0 | -0.52 |
| 19 | 2026-07-01 | isolation beds | joint | 4 | 90.19 | 1.5 | 0.138 | 1.54 | missing | missing | 13.48 | 0.0 | 76.71 | 0.0 | 0.0 | -0.99 |
| 20 | 2026-07-06 | isolation beds | joint | 3 | 42.99 | 0.59 | 0.063 | 0.58 | missing | missing | 18.38 | 0.0 | 24.61 | 0.0 | 1.0 | -0.69 |
| 21 | 2026-07-08 | isolation beds | joint | 3 | 49.21 | 1.51 | 0.065 | 1.49 | missing | missing | 10.96 | 38.24 | 0.0 | 0.0 | 1.0 | 0.81 |
| 22 | 2026-07-23 | isolation beds | joint | 1 | 65.2 | 3.33 | 0.081 | 3.16 | 0.82 | 0.6 | 22.47 | 42.73 | 0.0 | 0.0 | 1.0 | 0.64 |
| 23 | 2026-07-23 | isolation beds | treatment | 1 | 79.34 | 4.05 | 0.136 | 5.3 | missing | missing | 58.35 | 0.0 | 20.99 | 1.0 | 1.0 | -0.39 |
| 24 | 2026-07-25 | isolation beds | joint | 1 | 98.17 | 1.99 | 0.128 | 1.91 | 1.77 | 1.33 | 23.93 | 74.24 | 0.0 | 0.0 | 1.0 | 0.78 |
| 25 | 2026-07-25 | isolation beds | treatment | 1 | 55.52 | 1.12 | 0.096 | 1.43 | missing | missing | 55.49 | 0.03 | 0.0 | 1.0 | 1.0 | 0.02 |
| 26 | 2026-07-01 | recovered | joint | 4 | 10457.7 | 153.97 | 0.539 | 1.89 | missing | missing | 10409.1 | 48.57 | 0.0 | 1.0 | 1.0 | 0.29 |
| 27 | 2026-07-06 | recovered | joint | 3 | 56.79 | 0.97 | 0.4 | 1.1 | missing | missing | 39.82 | 0.0 | 16.96 | 1.0 | 1.0 | -0.37 |
| 28 | 2026-07-08 | recovered | joint | 3 | 44.34 | 1.18 | 0.288 | 1.48 | missing | missing | 29.46 | 0.0 | 14.87 | 1.0 | 1.0 | -0.4 |
| 29 | 2026-05-18 | reported cases | joint | 1 | 231.14 | 0.48 | 1.87 | 0.3 | missing | missing | 61.15 | 0.0 | 169.99 | 0.0 | 1.0 | -0.66 |
| 30 | 2026-05-18 | suspected deaths | joint | 1 | 41.59 | 0.39 | 0.577 | 0.12 | missing | missing | 41.12 | 0.0 | 0.47 | 1.0 | 1.0 | -0.08 |
Forecasts made at each release against the value observed since, one panel per stream and horizon, the observed value in black. The median and 90% interval are coloured by fit role: the persistence baseline, the stream's individual fit and the joint. The x-axis is the date each forecast was made, so an incident stream's observed window pairs unambiguously with the forecast that made it. Each panel's axis is cropped to a small multiple of what that stream actually reached, so one very wide interval cannot squash every other series flat; an interval or median too wide for the panel is clamped at the top and marked with an open triangle rather than silently cut off.
Forecasts-versus-now overlay
forecast_overlay_fig = plot_forecast_overlay(forecast_overlay_df);Frozen-fit forecast evaluation
The current model, frozen at earlier data cut-offs (see Forecast-versus-frozen evaluation), scored the same way as the cross-release forecasts above and against the same persistence baseline. Only the joint model is scored here, so no individual single-stream fit appears in the tables and figures below. The May cut-offs predate the first reported bed occupancy and the first reported recoveries, so those windows are left unscored rather than scored against a series that had not started. The baseline carries a weaker data-vintage guarantee than the cross-release one, since its snapshot was taken weeks after the frozen cut-off and can hold later revisions to earlier days (see forecast scoring against a persistence baseline).
Load and summarise the frozen-fit forecast scores
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))
# The frozen evaluation never carries an individual single-stream fit
# (it scores only the joint model at past cut-offs), so the individual-fit
# comparison columns are dropped rather than shown as a column of missing.
frozen_score_overview_table = drop_individual_fit_columns(
forecast_score_overview(frozen_scores_df))
frozen_score_by_horizon_table = drop_individual_fit_columns(
forecast_score_by_horizon(frozen_scores_df))
frozen_score_by_release_table = drop_individual_fit_columns(
forecast_score_by_release(frozen_scores_df))
# `fit` is single-valued (`FROZEN_FIT`) by construction in every one of
# these tables, not just for the releases scored so far, so it is dropped
# from the display tables below as a degenerate column. The `..._table`
# frames above keep it and still feed the relative-skill plots, which read
# it to colour each series in the joint role.
frozen_score_overview_display = drop_degenerate_fit_column(
frozen_score_overview_table)
frozen_score_by_horizon_display = drop_degenerate_fit_column(
frozen_score_by_horizon_table)
frozen_score_by_release_display = drop_degenerate_fit_column(
frozen_score_by_release_table)| Row | made_date | stream | n | crps | rel_to_baseline | log_crps | log_rel_to_baseline | dispersion | overprediction | underprediction | coverage_50 | coverage_90 | bias |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | String31 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | 2026-05-20 | confirmed cases | 24 | 267.3 | 0.74 | 0.374 | 0.25 | 207.91 | 59.39 | 0.0 | 0.75 | 1.0 | 0.44 |
| 2 | 2026-05-23 | confirmed cases | 24 | 181.87 | 0.44 | 0.223 | 0.17 | 158.24 | 22.65 | 0.98 | 1.0 | 1.0 | 0.06 |
| 3 | 2026-05-27 | confirmed cases | 24 | 288.77 | 0.56 | 0.839 | 0.55 | 54.19 | 0.0 | 234.58 | 0.0 | 0.75 | -0.82 |
| 4 | 2026-06-08 | confirmed cases | 24 | 660.92 | 4.69 | 0.445 | 2.23 | 357.78 | 303.14 | 0.0 | 0.25 | 1.0 | 0.61 |
| 5 | 2026-07-16 | confirmed cases | 2 | 141.26 | 0.4 | 0.159 | 0.3 | 139.35 | 1.42 | 0.49 | 1.0 | 1.0 | -0.0 |
| 6 | 2026-07-18 | confirmed cases | 2 | 143.3 | 0.81 | 0.148 | 0.63 | 135.98 | 7.32 | 0.0 | 1.0 | 1.0 | 0.11 |
| 7 | 2026-07-19 | confirmed cases | 1 | 108.67 | 1.54 | 0.161 | 1.13 | 89.38 | 19.29 | 0.0 | 1.0 | 1.0 | 0.26 |
| 8 | 2026-07-20 | confirmed cases | 1 | 74.09 | 0.75 | 0.119 | 0.63 | 74.09 | 0.0 | 0.0 | 1.0 | 1.0 | -0.0 |
| 9 | 2026-07-24 | confirmed cases | 1 | 101.34 | 2.52 | 0.146 | 1.99 | 89.35 | 11.99 | 0.0 | 1.0 | 1.0 | 0.26 |
| 10 | 2026-07-25 | confirmed cases | 1 | 110.54 | 3.36 | 0.162 | 2.83 | 89.15 | 21.39 | 0.0 | 1.0 | 1.0 | 0.29 |
| 11 | 2026-05-20 | confirmed deaths | 24 | 34.23 | 0.35 | 0.401 | 0.19 | 20.18 | 0.0 | 14.05 | 0.88 | 1.0 | -0.34 |
| 12 | 2026-05-23 | confirmed deaths | 24 | 42.06 | 0.35 | 0.488 | 0.23 | 19.73 | 0.0 | 22.34 | 0.25 | 1.0 | -0.56 |
| 13 | 2026-05-27 | confirmed deaths | 24 | 122.12 | 0.83 | 1.738 | 0.89 | 5.91 | 0.0 | 116.22 | 0.0 | 0.0 | -0.96 |
| 14 | 2026-06-08 | confirmed deaths | 24 | 81.76 | 0.61 | 0.23 | 0.35 | 79.2 | 1.94 | 0.62 | 1.0 | 1.0 | -0.02 |
| 15 | 2026-07-16 | confirmed deaths | 2 | 242.53 | 1.67 | 2.165 | 5.3 | 38.11 | 0.0 | 204.42 | 0.0 | 1.0 | -0.84 |
| 16 | 2026-07-18 | confirmed deaths | 2 | 217.24 | 3.32 | 2.14 | 12.39 | 43.29 | 0.0 | 173.95 | 0.0 | 1.0 | -0.79 |
| 17 | 2026-07-19 | confirmed deaths | 1 | 187.2 | 6.37 | 2.813 | 24.96 | 15.7 | 0.0 | 171.5 | 0.0 | 1.0 | -0.83 |
| 18 | 2026-07-20 | confirmed deaths | 1 | 216.88 | 4.64 | 3.349 | 19.57 | 11.19 | 0.0 | 205.69 | 0.0 | 1.0 | -0.83 |
| 19 | 2026-07-24 | confirmed deaths | 1 | 198.5 | 11.19 | 3.767 | 60.61 | 6.33 | 0.0 | 192.17 | 0.0 | 1.0 | -0.87 |
| 20 | 2026-07-25 | confirmed deaths | 1 | 182.4 | 6.26 | 3.595 | 35.05 | 7.61 | 0.0 | 174.79 | 0.0 | 1.0 | -0.81 |
| 21 | 2026-06-08 | isolation beds | 24 | 77.47 | 0.46 | 0.158 | 0.41 | 40.02 | 1.28 | 36.18 | 0.5 | 1.0 | -0.22 |
| 22 | 2026-07-16 | isolation beds | 2 | 61.65 | 2.47 | 0.077 | 2.27 | 15.29 | 46.36 | 0.0 | 0.0 | 1.0 | 0.62 |
| 23 | 2026-07-18 | isolation beds | 2 | 75.58 | 2.44 | 0.097 | 2.29 | 13.0 | 62.58 | 0.0 | 0.0 | 1.0 | 0.68 |
| 24 | 2026-07-19 | isolation beds | 1 | 96.27 | 4.77 | 0.122 | 4.44 | 14.16 | 82.11 | 0.0 | 0.0 | 1.0 | 0.81 |
| 25 | 2026-07-20 | isolation beds | 1 | 86.12 | 4.94 | 0.108 | 4.58 | 11.53 | 74.59 | 0.0 | 0.0 | 1.0 | 0.75 |
| 26 | 2026-07-24 | isolation beds | 1 | 82.11 | 4.43 | 0.101 | 4.1 | 6.18 | 75.93 | 0.0 | 0.0 | 1.0 | 0.89 |
| 27 | 2026-07-25 | isolation beds | 1 | 152.28 | 2.8 | 0.196 | 2.66 | 8.35 | 143.93 | 0.0 | 0.0 | 0.0 | 0.92 |
| 28 | 2026-06-08 | recovered | 24 | 114.08 | 1.02 | 0.452 | 0.32 | 89.33 | 24.75 | 0.0 | 1.0 | 1.0 | 0.42 |
| 29 | 2026-07-16 | recovered | 2 | 65.28 | 1.11 | 0.49 | 1.37 | 59.42 | 0.0 | 5.86 | 1.0 | 1.0 | -0.16 |
| 30 | 2026-07-18 | recovered | 2 | 73.57 | 3.21 | 0.431 | 2.9 | 71.78 | 1.8 | 0.0 | 1.0 | 1.0 | 0.11 |
| 31 | 2026-07-19 | recovered | 1 | 49.32 | 4.03 | 0.452 | 4.74 | 48.63 | 0.69 | 0.0 | 1.0 | 1.0 | 0.09 |
| 32 | 2026-07-20 | recovered | 1 | 45.56 | 5.44 | 0.399 | 5.47 | 45.09 | 0.47 | 0.0 | 1.0 | 1.0 | 0.07 |
| 33 | 2026-07-24 | recovered | 1 | 42.29 | 2.09 | 0.357 | 2.29 | 40.63 | 1.66 | 0.0 | 1.0 | 1.0 | 0.17 |
| 34 | 2026-07-25 | recovered | 1 | 46.94 | 2.61 | 0.326 | 2.06 | 46.01 | 0.93 | 0.0 | 1.0 | 1.0 | 0.09 |
The headline frozen table, one row per stream pooled across cut-offs and horizons, against the persistence baseline on both scales, with the same CRPS decomposition, coverage and bias columns described above. There is no model column, since only one model is scored.
| Row | stream | n | crps | rel_to_baseline | log_crps | log_rel_to_baseline | dispersion | overprediction | underprediction | coverage_50 | coverage_90 | bias |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| String31 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | confirmed cases | 104 | 616.36 | 1.8 | 0.383 | 0.36 | 345.92 | 270.4 | 0.05 | 0.42 | 1.0 | 0.47 |
| 2 | confirmed deaths | 104 | 151.21 | 1.25 | 1.209 | 0.76 | 77.22 | 1.79 | 72.2 | 0.62 | 1.0 | -0.27 |
| 3 | isolation beds | 32 | 92.89 | 0.7 | 0.152 | 0.51 | 28.17 | 37.58 | 27.13 | 0.12 | 0.94 | 0.17 |
| 4 | recovered | 32 | 105.87 | 1.16 | 0.434 | 0.39 | 88.14 | 16.99 | 0.73 | 1.0 | 1.0 | 0.23 |
The same relative skill against the baseline, by horizon, for the frozen cut-offs.
frozen_relative_skill_fig = plot_forecast_relative_skill(
frozen_score_by_horizon_table);The same columns as a table, broken out by horizon, and again broken out by frozen cut-off, are behind the two dropdowns below.
Scores by horizon
| Row | stream | horizon | n | crps | rel_to_baseline | log_crps | log_rel_to_baseline | dispersion | overprediction | underprediction | coverage_50 | coverage_90 | bias |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| String31 | Int64 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | confirmed cases | 7 | 30 | 93.51 | 1.28 | 0.146 | 0.34 | 84.51 | 8.83 | 0.16 | 1.0 | 1.0 | 0.13 |
| 2 | confirmed cases | 14 | 26 | 234.54 | 0.96 | 0.269 | 0.28 | 170.38 | 64.16 | 0.0 | 0.54 | 1.0 | 0.39 |
| 3 | confirmed cases | 21 | 24 | 708.98 | 1.61 | 0.544 | 0.4 | 324.41 | 384.57 | 0.0 | 0.0 | 1.0 | 0.75 |
| 4 | confirmed cases | 28 | 24 | 1590.95 | 2.31 | 0.641 | 0.38 | 884.35 | 706.6 | 0.0 | 0.0 | 1.0 | 0.71 |
| 5 | confirmed deaths | 7 | 30 | 199.33 | 8.23 | 3.21 | 4.75 | 11.29 | 0.0 | 188.04 | 0.0 | 1.0 | -0.85 |
| 6 | confirmed deaths | 14 | 26 | 117.9 | 1.68 | 0.669 | 0.49 | 46.08 | 0.0 | 71.83 | 0.62 | 1.0 | -0.32 |
| 7 | confirmed deaths | 21 | 24 | 91.96 | 0.59 | 0.254 | 0.12 | 90.69 | 1.27 | 0.0 | 1.0 | 1.0 | 0.05 |
| 8 | confirmed deaths | 28 | 24 | 186.4 | 0.72 | 0.248 | 0.1 | 179.92 | 6.48 | 0.0 | 1.0 | 1.0 | 0.18 |
| 9 | isolation beds | 7 | 12 | 86.5 | 2.25 | 0.109 | 1.17 | 10.7 | 75.81 | 0.0 | 0.0 | 0.83 | 0.78 |
| 10 | isolation beds | 14 | 8 | 64.44 | 1.37 | 0.106 | 0.81 | 27.82 | 36.62 | 0.0 | 0.5 | 1.0 | 0.43 |
| 11 | isolation beds | 21 | 6 | 96.71 | 0.38 | 0.194 | 0.34 | 44.84 | 0.0 | 51.86 | 0.0 | 1.0 | -0.56 |
| 12 | isolation beds | 28 | 6 | 139.77 | 0.44 | 0.26 | 0.39 | 46.93 | 0.0 | 92.84 | 0.0 | 1.0 | -0.66 |
| 13 | recovered | 7 | 12 | 45.66 | 3.75 | 0.41 | 2.71 | 43.29 | 0.92 | 1.46 | 1.0 | 1.0 | 0.05 |
| 14 | recovered | 14 | 8 | 75.24 | 1.2 | 0.419 | 0.41 | 68.82 | 5.66 | 0.75 | 1.0 | 1.0 | 0.2 |
| 15 | recovered | 21 | 6 | 120.11 | 0.84 | 0.445 | 0.23 | 90.25 | 29.86 | 0.0 | 1.0 | 1.0 | 0.43 |
| 16 | recovered | 28 | 6 | 252.87 | 1.08 | 0.49 | 0.21 | 201.49 | 51.38 | 0.0 | 1.0 | 1.0 | 0.42 |
Scores by frozen cut-off
| Row | made_date | stream | n | crps | rel_to_baseline | log_crps | log_rel_to_baseline | dispersion | overprediction | underprediction | coverage_50 | coverage_90 | bias |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | String31 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | 2026-05-20 | confirmed cases | 24 | 267.3 | 0.74 | 0.374 | 0.25 | 207.91 | 59.39 | 0.0 | 0.75 | 1.0 | 0.44 |
| 2 | 2026-05-23 | confirmed cases | 24 | 181.87 | 0.44 | 0.223 | 0.17 | 158.24 | 22.65 | 0.98 | 1.0 | 1.0 | 0.06 |
| 3 | 2026-05-27 | confirmed cases | 24 | 288.77 | 0.56 | 0.839 | 0.55 | 54.19 | 0.0 | 234.58 | 0.0 | 0.75 | -0.82 |
| 4 | 2026-06-08 | confirmed cases | 24 | 660.92 | 4.69 | 0.445 | 2.23 | 357.78 | 303.14 | 0.0 | 0.25 | 1.0 | 0.61 |
| 5 | 2026-07-16 | confirmed cases | 2 | 141.26 | 0.4 | 0.159 | 0.3 | 139.35 | 1.42 | 0.49 | 1.0 | 1.0 | -0.0 |
| 6 | 2026-07-18 | confirmed cases | 2 | 143.3 | 0.81 | 0.148 | 0.63 | 135.98 | 7.32 | 0.0 | 1.0 | 1.0 | 0.11 |
| 7 | 2026-07-19 | confirmed cases | 1 | 108.67 | 1.54 | 0.161 | 1.13 | 89.38 | 19.29 | 0.0 | 1.0 | 1.0 | 0.26 |
| 8 | 2026-07-20 | confirmed cases | 1 | 74.09 | 0.75 | 0.119 | 0.63 | 74.09 | 0.0 | 0.0 | 1.0 | 1.0 | -0.0 |
| 9 | 2026-07-24 | confirmed cases | 1 | 101.34 | 2.52 | 0.146 | 1.99 | 89.35 | 11.99 | 0.0 | 1.0 | 1.0 | 0.26 |
| 10 | 2026-07-25 | confirmed cases | 1 | 110.54 | 3.36 | 0.162 | 2.83 | 89.15 | 21.39 | 0.0 | 1.0 | 1.0 | 0.29 |
| 11 | 2026-05-20 | confirmed deaths | 24 | 34.23 | 0.35 | 0.401 | 0.19 | 20.18 | 0.0 | 14.05 | 0.88 | 1.0 | -0.34 |
| 12 | 2026-05-23 | confirmed deaths | 24 | 42.06 | 0.35 | 0.488 | 0.23 | 19.73 | 0.0 | 22.34 | 0.25 | 1.0 | -0.56 |
| 13 | 2026-05-27 | confirmed deaths | 24 | 122.12 | 0.83 | 1.738 | 0.89 | 5.91 | 0.0 | 116.22 | 0.0 | 0.0 | -0.96 |
| 14 | 2026-06-08 | confirmed deaths | 24 | 81.76 | 0.61 | 0.23 | 0.35 | 79.2 | 1.94 | 0.62 | 1.0 | 1.0 | -0.02 |
| 15 | 2026-07-16 | confirmed deaths | 2 | 242.53 | 1.67 | 2.165 | 5.3 | 38.11 | 0.0 | 204.42 | 0.0 | 1.0 | -0.84 |
| 16 | 2026-07-18 | confirmed deaths | 2 | 217.24 | 3.32 | 2.14 | 12.39 | 43.29 | 0.0 | 173.95 | 0.0 | 1.0 | -0.79 |
| 17 | 2026-07-19 | confirmed deaths | 1 | 187.2 | 6.37 | 2.813 | 24.96 | 15.7 | 0.0 | 171.5 | 0.0 | 1.0 | -0.83 |
| 18 | 2026-07-20 | confirmed deaths | 1 | 216.88 | 4.64 | 3.349 | 19.57 | 11.19 | 0.0 | 205.69 | 0.0 | 1.0 | -0.83 |
| 19 | 2026-07-24 | confirmed deaths | 1 | 198.5 | 11.19 | 3.767 | 60.61 | 6.33 | 0.0 | 192.17 | 0.0 | 1.0 | -0.87 |
| 20 | 2026-07-25 | confirmed deaths | 1 | 182.4 | 6.26 | 3.595 | 35.05 | 7.61 | 0.0 | 174.79 | 0.0 | 1.0 | -0.81 |
| 21 | 2026-06-08 | isolation beds | 24 | 77.47 | 0.46 | 0.158 | 0.41 | 40.02 | 1.28 | 36.18 | 0.5 | 1.0 | -0.22 |
| 22 | 2026-07-16 | isolation beds | 2 | 61.65 | 2.47 | 0.077 | 2.27 | 15.29 | 46.36 | 0.0 | 0.0 | 1.0 | 0.62 |
| 23 | 2026-07-18 | isolation beds | 2 | 75.58 | 2.44 | 0.097 | 2.29 | 13.0 | 62.58 | 0.0 | 0.0 | 1.0 | 0.68 |
| 24 | 2026-07-19 | isolation beds | 1 | 96.27 | 4.77 | 0.122 | 4.44 | 14.16 | 82.11 | 0.0 | 0.0 | 1.0 | 0.81 |
| 25 | 2026-07-20 | isolation beds | 1 | 86.12 | 4.94 | 0.108 | 4.58 | 11.53 | 74.59 | 0.0 | 0.0 | 1.0 | 0.75 |
| 26 | 2026-07-24 | isolation beds | 1 | 82.11 | 4.43 | 0.101 | 4.1 | 6.18 | 75.93 | 0.0 | 0.0 | 1.0 | 0.89 |
| 27 | 2026-07-25 | isolation beds | 1 | 152.28 | 2.8 | 0.196 | 2.66 | 8.35 | 143.93 | 0.0 | 0.0 | 0.0 | 0.92 |
| 28 | 2026-06-08 | recovered | 24 | 114.08 | 1.02 | 0.452 | 0.32 | 89.33 | 24.75 | 0.0 | 1.0 | 1.0 | 0.42 |
| 29 | 2026-07-16 | recovered | 2 | 65.28 | 1.11 | 0.49 | 1.37 | 59.42 | 0.0 | 5.86 | 1.0 | 1.0 | -0.16 |
| 30 | 2026-07-18 | recovered | 2 | 73.57 | 3.21 | 0.431 | 2.9 | 71.78 | 1.8 | 0.0 | 1.0 | 1.0 | 0.11 |
| 31 | 2026-07-19 | recovered | 1 | 49.32 | 4.03 | 0.452 | 4.74 | 48.63 | 0.69 | 0.0 | 1.0 | 1.0 | 0.09 |
| 32 | 2026-07-20 | recovered | 1 | 45.56 | 5.44 | 0.399 | 5.47 | 45.09 | 0.47 | 0.0 | 1.0 | 1.0 | 0.07 |
| 33 | 2026-07-24 | recovered | 1 | 42.29 | 2.09 | 0.357 | 2.29 | 40.63 | 1.66 | 0.0 | 1.0 | 1.0 | 0.17 |
| 34 | 2026-07-25 | recovered | 1 | 46.94 | 2.61 | 0.326 | 2.06 | 46.01 | 0.93 | 0.0 | 1.0 | 1.0 | 0.09 |
The frozen forecasts made at each cut-off against the value observed since, one panel per stream and horizon, the observed value in black. Each panel carries the frozen forecast and the persistence baseline, coloured as in the cross-release overlay above, and the x-axis is the cut-off each forecast was made from. The same per-panel axis crop and overflow marker applies here.
Frozen-fit forecasts-versus-now overlay
frozen_overlay_fig = plot_forecast_overlay(frozen_overlay_df);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. Each uses the full headline settings (1000 draws across two chains), reusing the frozen-fit helper defined above.
Freeze the renewal data to a cut-off and re-fit
# Frozen re-fits and released_df are prepared in the setup block above.Individual fits against the baseline
The same cross-release forecast scoring as Forecast scoring across releases above, filtered to each stream's own individual single-stream fit rather than the joint, against the same persistence baseline. Recovered has no individual fit, so it does not appear here. forecast_score_overview_table and its by-horizon and by-release counterparts already carry one row per (stream, fit) for every fit scored, joint and individual alike, so this section reuses those same tables filtered to the individual-fit rows rather than building its own. As noted in Forecast scoring across releases above, the releases that carry the current model's own individual-stream forecasts are too recent for their targets to be observed yet, so every table and figure in this section is currently empty; it will populate once those targets resolve.
Individual-fit rows of the cross-release scores
individual_score_overview_table = forecast_score_overview_table[
forecast_score_overview_table.fit .!= "joint", :]
individual_score_by_horizon_table = forecast_score_by_horizon_table[
forecast_score_by_horizon_table.fit .!= "joint", :]
individual_score_by_release_table = forecast_score_by_release_table[
forecast_score_by_release_table.fit .!= "joint", :]| Row | made_date | stream | fit | n | crps | rel_to_baseline | log_crps | log_rel_to_baseline | rel_to_individual | log_rel_to_individual | dispersion | overprediction | underprediction | coverage_50 | coverage_90 | bias |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | String31 | String31 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64? | Float64? | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | 2026-07-23 | confirmed cases | confirmed | 1 | 100.25 | 2.4 | 0.201 | 2.76 | missing | missing | 86.6 | 0.0 | 13.65 | 1.0 | 1.0 | -0.24 |
| 2 | 2026-07-25 | confirmed cases | confirmed | 1 | 107.9 | 3.27 | 0.198 | 3.43 | missing | missing | 107.74 | 0.0 | 0.16 | 1.0 | 1.0 | -0.02 |
| 3 | 2026-07-23 | confirmed deaths | confirmed_deaths | 1 | 110.13 | 5.38 | 0.281 | 4.11 | missing | missing | 82.57 | 27.56 | 0.0 | 1.0 | 1.0 | 0.33 |
| 4 | 2026-07-25 | confirmed deaths | confirmed_deaths | 1 | 56.36 | 2.26 | 0.201 | 2.26 | missing | missing | 53.83 | 2.53 | 0.0 | 1.0 | 1.0 | 0.11 |
| 5 | 2026-07-23 | isolation beds | treatment | 1 | 79.34 | 4.05 | 0.136 | 5.3 | missing | missing | 58.35 | 0.0 | 20.99 | 1.0 | 1.0 | -0.39 |
| 6 | 2026-07-25 | isolation beds | treatment | 1 | 55.52 | 1.12 | 0.096 | 1.43 | missing | missing | 55.49 | 0.03 | 0.0 | 1.0 | 1.0 | 0.02 |
| Row | stream | fit | n | crps | rel_to_baseline | log_crps | log_rel_to_baseline | rel_to_individual | log_rel_to_individual | dispersion | overprediction | underprediction | coverage_50 | coverage_90 | bias |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| String31 | String31 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64? | Float64? | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | confirmed cases | confirmed | 2 | 104.08 | 2.79 | 0.199 | 3.06 | missing | missing | 97.17 | 0.0 | 6.9 | 1.0 | 1.0 | -0.13 |
| 2 | confirmed deaths | confirmed_deaths | 2 | 83.24 | 3.67 | 0.241 | 3.06 | missing | missing | 68.2 | 15.05 | 0.0 | 1.0 | 1.0 | 0.22 |
| 3 | isolation beds | treatment | 2 | 67.43 | 1.96 | 0.116 | 2.5 | missing | missing | 56.92 | 0.02 | 10.5 | 1.0 | 1.0 | -0.18 |
The same relative skill against the baseline, by horizon, one panel per stream (dataset), for each stream's own individual fit.
individual_relative_skill_fig = plot_forecast_relative_skill(
individual_score_by_horizon_table;
empty_message = "Empty: the releases that carry the current model's " *
"own individual-stream forecasts are too recent for their " *
"targets to be observed yet. Not a missing forecast.");The same columns as a table, broken out by horizon, and again broken out by release, are behind the two dropdowns below.
Scores by horizon
| Row | stream | horizon | fit | n | crps | rel_to_baseline | log_crps | log_rel_to_baseline | rel_to_individual | log_rel_to_individual | dispersion | overprediction | underprediction | coverage_50 | coverage_90 | bias |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| String31 | Int64 | String31 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64? | Float64? | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | confirmed cases | 7 | confirmed | 2 | 104.08 | 2.79 | 0.199 | 3.06 | missing | missing | 97.17 | 0.0 | 6.9 | 1.0 | 1.0 | -0.13 |
| 2 | confirmed deaths | 7 | confirmed_deaths | 2 | 83.24 | 3.67 | 0.241 | 3.06 | missing | missing | 68.2 | 15.05 | 0.0 | 1.0 | 1.0 | 0.22 |
| 3 | isolation beds | 7 | treatment | 2 | 67.43 | 1.96 | 0.116 | 2.5 | missing | missing | 56.92 | 0.02 | 10.5 | 1.0 | 1.0 | -0.18 |
Scores by release
| Row | made_date | stream | fit | n | crps | rel_to_baseline | log_crps | log_rel_to_baseline | rel_to_individual | log_rel_to_individual | dispersion | overprediction | underprediction | coverage_50 | coverage_90 | bias |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | String31 | String31 | Int64 | Float64 | Float64 | Float64 | Float64 | Float64? | Float64? | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | 2026-07-23 | confirmed cases | confirmed | 1 | 100.25 | 2.4 | 0.201 | 2.76 | missing | missing | 86.6 | 0.0 | 13.65 | 1.0 | 1.0 | -0.24 |
| 2 | 2026-07-25 | confirmed cases | confirmed | 1 | 107.9 | 3.27 | 0.198 | 3.43 | missing | missing | 107.74 | 0.0 | 0.16 | 1.0 | 1.0 | -0.02 |
| 3 | 2026-07-23 | confirmed deaths | confirmed_deaths | 1 | 110.13 | 5.38 | 0.281 | 4.11 | missing | missing | 82.57 | 27.56 | 0.0 | 1.0 | 1.0 | 0.33 |
| 4 | 2026-07-25 | confirmed deaths | confirmed_deaths | 1 | 56.36 | 2.26 | 0.201 | 2.26 | missing | missing | 53.83 | 2.53 | 0.0 | 1.0 | 1.0 | 0.11 |
| 5 | 2026-07-23 | isolation beds | treatment | 1 | 79.34 | 4.05 | 0.136 | 5.3 | missing | missing | 58.35 | 0.0 | 20.99 | 1.0 | 1.0 | -0.39 |
| 6 | 2026-07-25 | isolation beds | treatment | 1 | 55.52 | 1.12 | 0.096 | 1.43 | missing | missing | 55.49 | 0.03 | 0.0 | 1.0 | 1.0 | 0.02 |
Outbreak size estimated by each data stream
Each data stream constrains the latent outbreak size differently. The table below puts the posteriors over the infection count side by side, the single-stream fits and the joint, to show what each stream implies on its own and what the joint adds.
Per-stream infection-count table
streams_C_table = streams_table(
"exports" => posterior_C_exports,
"deaths (DRC)" => posterior_C_deaths,
"cases (DRC)" => posterior_C_cases,
"confirmed (DRC)" => posterior_C_confirmed,
"isolation (DRC)" => posterior_C_treatment,
"joint" => posterior_C_joint);| Row | Stream | Lower 90% | Lower 60% | Lower 30% | Upper 30% | Upper 60% | Upper 90% |
|---|---|---|---|---|---|---|---|
| String | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | exports | 3274.0 | 11016.0 | 20297.0 | 82547.0 | 199032.0 | 1.2919e6 |
| 2 | deaths (DRC) | 16944.0 | 22392.0 | 26597.0 | 36563.0 | 44756.0 | 65293.0 |
| 3 | cases (DRC) | 20511.0 | 22871.0 | 25237.0 | 32286.0 | 41152.0 | 70518.0 |
| 4 | confirmed (DRC) | 28279.0 | 34848.0 | 40770.0 | 52675.0 | 66424.0 | 101928.0 |
| 5 | isolation (DRC) | 6910.0 | 8113.0 | 9080.0 | 11573.0 | 14505.0 | 25317.0 |
| 6 | joint | 5702.0 | 6375.0 | 6907.0 | 8028.0 | 8993.0 | 11219.0 |
The first figure shows each single-stream fit's cumulative-infection trajectory projected to the cut-off, with a dotted rule in each stream's colour marking where its data stops and the ribbon beyond it becomes a forward projection.
Per-stream projected-trajectory plot
# Per-draw cumulative-infection trajectory carried by each single-stream
# fit out to the cut-off on day `n`, so streams whose data ends earlier are
# still projected to today.
function _cuminf(chn)
mat = chn[:cumulative_infections]
return [collect(v) for v in vec(collect(mat))]
end
# Grid day a stream's data last reports, used for the dotted rule. The
# suspected case and death histories freeze at 26 May; exports and confirmed
# run to the cut-off.
_last_day(days) = isempty(days) ? nothing : maximum(days)
stream_traj_fig = plot_stream_trajectories(
[
(; label = "exports", trajs = _cuminf(chn_exports),
last_day = _last_day(vcat(obs.export_case_days,
obs.export_death_days)), colour = :seagreen),
(; label = "deaths (DRC)", trajs = _cuminf(chn_deaths),
last_day = _last_day(obs.deaths_history.days),
colour = :firebrick),
(; label = "cases (DRC)", trajs = _cuminf(chn_cases),
last_day = _last_day(obs.reported_history.days),
colour = :steelblue),
(; label = "confirmed (DRC)", trajs = _cuminf(chn_confirmed),
last_day = _last_day(obs.confirmed_history.days),
colour = :goldenrod),
(; label = "isolation (DRC)", trajs = _cuminf(chn_treatment),
last_day = _last_day(obs.isolation_history.days),
colour = :darkorange)];
n = obs.n, seeding = obs.seeding);The second figure is the posterior density of each fit's cumulative infection count at the cut-off; the x-axis is scaled to a multiple of the joint-fit 90% upper bound so the bulk of the streams stays visible rather than being flattened by the wide, ill-defined confirmed-only tail.
Cut-off infection-count density plot
# Scale the x-axis to twice the joint-fit 90% upper bound, so the joint and
# the streams that track it read clearly while the confirmed-only tail runs
# off the axis rather than dominating it.
density_xmax = 2.0 * quantile(posterior_C_joint, 0.95)
cumulative_density_fig = plot_cumulative_cases(
"exports" => posterior_C_exports,
"deaths (DRC)" => posterior_C_deaths,
"cases (DRC)" => posterior_C_cases,
"confirmed (DRC)" => posterior_C_confirmed,
"isolation (DRC)" => posterior_C_treatment,
"joint" => posterior_C_joint;
scenarios = [], xmax = density_xmax);Estimate evolution across releases
How the outbreak-size estimate has moved as situation reports accrued, three series on one calendar axis. The estimate published at each release is in blue, drawn as a median with nested 30/60/90% interval bars because each release is its own fit rather than one continuous model. The current model frozen at earlier cut-offs is in red, reusing fits already made for the McCabe and Chamla comparisons and the forecast validation. The current model on current data is the green band, drawn day by day so the latest estimate reads against the earlier points, and dotted vertical rules mark the release dates. The published series switches from a closed-form integral model to a renewal model on 7 June, so a step there can reflect the change of method rather than of data.
Released estimates and the current-model frozen re-fits
# Released median and 30/60/90% intervals per release, from
# `data/released_estimates.csv`. Each tuple is
# `(date, median, lo30, hi30, lo60, hi60, lo90, hi90)`.
release_evolution = [(string(r.date), r.median, r.lo30, r.hi30, r.lo60, r.hi60,
r.lo90, r.hi90) for r in eachrow(released_df)]
# The current model frozen at earlier cut-offs, each its own discrete
# estimate: the matched-McCabe cut-offs (20, 23, 27 May) already computed
# for the matched-in-time comparison below, the 8 June Chamla
# confirmed-case anchor computed for the Chamla comparison, and the
# one-week-back validation fit (`frozen_lastweek`, at `validation_cutoff`)
# already computed for the forecast validation above. All are reused here so
# the current-model estimate at those earlier cut-offs reads against the
# released overlay, including a recent point one week before the cut-off.
# No extra fits are run. Each tuple carries the median and 30/60/90%
# credible bounds from the frozen draws; `round_fn` rounds to a whole count
# for outbreak size, and is passed through unrounded for a continuous
# quantity such as R0.
function _ci369(xs; round_fn = x -> round(Int, x))
q(p) = round_fn(quantile(xs, p))
(q(0.5), q(0.35), q(0.65), q(0.20), q(0.80), q(0.05), q(0.95))
end
frozen_by_cutoff[validation_cutoff] = frozen_lastweek
# The cut-offs every frozen fit above was made at, shared by the
# outbreak-size and R0 by-release overlays below.
_frozen_matched_cutoffs = sort(union(frozen_cutoffs,
[validation_cutoff, default_chamla_cutoff()]))
frozen_matched = [(c, _ci369(frozen_C(c))...) for c in _frozen_matched_cutoffs]
# The current-data, current-model estimate as the cumulative-infection
# trajectory over the day grid (one calendar date per grid day, day 1 is
# the seeding date), summarised by per-day 30/60/90% credible bounds. This
# is the same latent quantity the cumulative-trajectory figure shows, so
# the current estimate rises over time on the release-date axis instead of
# sitting flat. Drawn against calendar dates, it lines up with the
# release and frozen points.
infection_trajectory = let
mat = chn_joint[:cumulative_infections]
trajs = [collect(v) for v in vec(collect(mat))]
# Only over the comparison window — from the earliest release date to the
# cut-off — not back to the seeding date.
start_day = obs.n - value(obs.cutoff - Date(release_evolution[1][1]))
days = max(start_day, 1):obs.n
dates = [obs.seeding + Day(d - 1) for d in days]
q(d, p) = quantile(Float64[t[d] for t in trajs], p)
(dates,
[q(d, 0.35) for d in days], [q(d, 0.65) for d in days],
[q(d, 0.20) for d in days], [q(d, 0.80) for d in days],
[q(d, 0.05) for d in days], [q(d, 0.95) for d in days])
end
evolution_fig = plot_estimate_evolution(release_evolution;
renewal = frozen_matched,
renewal_label = "Current model frozen at earlier cut-offs",
trajectory = infection_trajectory,
title = "Outbreak-size estimate as data accrued");Reproduction number estimated by each data stream
The reproduction number each stream implies on its own, one panel per stream with the joint fit overlaid in grey as the reference.
Per-stream implied-Rt plot
# The per-stream fits walk Rt from day 1 (the default `rt_start`), while the
# joint walks from `RT_WALK_LEAD` days before the first situation report; the
# shared `display_start` is the joint renewal start so every stream reads over
# the same established window. `ramp` matches the joint Rt figure.
_rt_walk_start_joint = clamp(_BREAKPOINT - RT_WALK_LEAD, _rt_start_plot, obs.n);
stream_rt_fig = plot_rt_streams(
[
(; label = "exports", chn = chn_exports, rt_start = 1,
rt_walk_start = 1, colour = :seagreen),
(; label = "deaths (DRC)", chn = chn_deaths, rt_start = 1,
rt_walk_start = 1, colour = :firebrick),
(; label = "cases (DRC)", chn = chn_cases, rt_start = 1,
rt_walk_start = 1, colour = :steelblue),
(; label = "confirmed (DRC)", chn = chn_confirmed, rt_start = 1,
rt_walk_start = 1, colour = :goldenrod),
(; label = "isolation (DRC)", chn = chn_treatment, rt_start = 1,
rt_walk_start = 1, colour = :darkorange)];
joint = (; label = "joint", chn = chn_joint, rt_start = _rt_start_plot,
rt_walk_start = _rt_walk_start_joint),
n = obs.n, breakpoint = _BREAKPOINT,
as_of_date = string(obs.cutoff), seeding = obs.seeding,
display_start = _rt_start_plot, ramp = RT_INTERVENTION_RAMP);Reproduction number by release
The reproduction number estimated at each release, the same kind of release-by-release picture as the outbreak-size evolution above. Each release's cut-off reproduction number
Reproduction number per release with the current-fit band
rt_release_df = CSV.read(
joinpath(pkgdir(BVDOutbreakSize), "data", "rt_by_release.csv"), DataFrame)
rt_release = [(string(r.date), r.median, r.lo30, r.hi30, r.lo60, r.hi60,
r.lo90, r.hi90) for r in eachrow(rt_release_df)]
# The current fit's daily Rt over its established window, summarised per day
# into a 30/60/90% band, reusing the same walk reconstruction the Rt figure
# uses so the band lines up with the per-release points on the calendar axis.
# The band is drawn only from the first release date onward, so it spans the
# same window as the per-release estimates rather than extending back to the
# renewal start. The first release day is the earliest date in
# `rt_by_release.csv` as a grid day; the walk is still reconstructed from the
# renewal start `_rt_start_plot` (the model knot grid) and the window is
# clamped into the reconstructed range so the quantiles never hit masked days.
rt_release_trajectory = let
rt_walk_start = clamp(_BREAKPOINT - RT_WALK_LEAD, _rt_start_plot, obs.n)
mat = reconstruct_rt(chn_joint; n = obs.n, breakpoint = _BREAKPOINT,
rt_start = _rt_start_plot, rt_walk_start = rt_walk_start,
ramp = RT_INTERVENTION_RAMP)
first_release_day = clamp(
value(minimum(rt_release_df.date) - obs.seeding) + 1,
_rt_start_plot, obs.n)
days = first_release_day:obs.n
dates = [obs.seeding + Day(d - 1) for d in days]
q(d, p) = quantile(collect(skipmissing(@view mat[:, d])), p)
(dates,
[q(d, 0.35) for d in days], [q(d, 0.65) for d in days],
[q(d, 0.20) for d in days], [q(d, 0.80) for d in days],
[q(d, 0.05) for d in days], [q(d, 0.95) for d in days])
end
rt_evolution_fig = plot_estimate_evolution(rt_release;
trajectory = rt_release_trajectory,
ylabel = "Reproduction number",
title = "Reproduction number as data accrued",
released_label = "Released estimate (per project release)",
trajectory_label = "Current model, current data",
refline = 1.0);Reproduction number by release and dataset
The same release-by-release reproduction number split into one panel per dataset, so each dataset's history reads against the others and against the joint. Panels share a calendar axis and a y range,
Reproduction number per release by fit
# Schema of the per-release, per-fit estimate tables written by
# scripts/score_releases.jl from each release's stream_estimates.csv.
_by_stream_schema = (; release = String, date = Date, fit = String,
median = Float64, lo30 = Float64, hi30 = Float64, lo60 = Float64,
hi60 = Float64, lo90 = Float64, hi90 = Float64)
# Fits in a fixed order, the joint first, so the panels do not reshuffle
# between builds. Labels match the per-stream table above (in "Outbreak
# size estimated by each data stream"). Recovered is absent because it
# has no individual fit.
_fit_order = ["joint", "cases", "deaths", "confirmed", "confirmed_deaths",
"treatment", "exports"]
_fit_labels = Dict("joint" => "joint", "cases" => "cases (DRC)",
"deaths" => "deaths (DRC)", "confirmed" => "confirmed (DRC)",
"confirmed_deaths" => "confirmed deaths (DRC)",
"treatment" => "isolation (DRC)", "exports" => "exports")
# Group a per-fit estimate table into the label => tuples pairs the faceted
# plot takes, keyed on the date so the mixed release tag shapes
# (`results-v1.9.0` and `results-1243`) never reach the axis.
function _fit_groups(df)
return [get(_fit_labels, f, f) =>
[(string(r.date), r.median, r.lo30, r.hi30, r.lo60, r.hi60,
r.lo90, r.hi90) for r in eachrow(df) if r.fit == f]
for f in _fit_order]
end
# Per-fit reproduction-number trajectory, reconstructing the walk exactly as
# `plot_rt_streams` does per stream.
function _stream_rt_trajectory(chn, dates; rt_start, rt_walk_start)
mat = reconstruct_rt(chn; n = obs.n, breakpoint = _BREAKPOINT,
rt_start = rt_start, rt_walk_start = rt_walk_start,
ramp = RT_INTERVENTION_RAMP)
first_date = isempty(dates) ? obs.seeding : minimum(dates)
first_day = clamp(value(first_date - obs.seeding) + 1, rt_start, obs.n)
days = first_day:obs.n
ds = [obs.seeding + Day(d - 1) for d in days]
q(d, p) = quantile(collect(skipmissing(@view mat[:, d])), p)
(ds,
[q(d, 0.35) for d in days], [q(d, 0.65) for d in days],
[q(d, 0.20) for d in days], [q(d, 0.80) for d in days],
[q(d, 0.05) for d in days], [q(d, 0.95) for d in days])
end
# The single-stream chains and their renewal-walk starts, keyed on the fit
# id the per-release tables use. Both the joint walk start and the day-1
# per-stream starts are the ones the per-stream implied-Rt figure above
# uses, so the bands here match it. Confirmed deaths has no trajectory
# here: its panel still draws its release points alone.
_stream_chains = (
"joint" => (; chn = chn_joint, rt_start = _rt_start_plot,
rt_walk_start = _rt_walk_start_joint),
"cases" => (; chn = chn_cases, rt_start = 1, rt_walk_start = 1),
"deaths" => (; chn = chn_deaths, rt_start = 1, rt_walk_start = 1),
"confirmed" => (; chn = chn_confirmed, rt_start = 1, rt_walk_start = 1),
"treatment" => (; chn = chn_treatment, rt_start = 1, rt_walk_start = 1),
"exports" => (; chn = chn_exports, rt_start = 1, rt_walk_start = 1))
# Build a fit label => trajectory dictionary from a per-release table,
# restricted to the fits `_stream_chains` names. A fit with no row in `df`
# gets no trajectory, so its panel still draws its release points alone.
function _rt_trajectories(df)
trajs = Dict{String, Any}()
for (fid, cfg) in _stream_chains
fdates = df.date[df.fit .== fid]
isempty(fdates) && continue
trajs[get(_fit_labels, fid, fid)] = _stream_rt_trajectory(
cfg.chn, fdates; rt_start = cfg.rt_start,
rt_walk_start = cfg.rt_walk_start)
end
return trajs
end
rt_stream_df = _release_data("rt_by_release_by_stream.csv",
_by_stream_schema)
rt_stream_fig = plot_evolution_by_group(_fit_groups(rt_stream_df);
trajectories = _rt_trajectories(rt_stream_df),
ylabel = "Reproduction number",
title = "Reproduction number as data accrued, by dataset",
released_label = "Released estimate (per release)",
refline = 1.0,
empty_note = "No per-dataset reproduction numbers saved yet.");Basic reproduction number by release
The basic reproduction number
Basic reproduction number per release with frozen re-fits and the current-fit band
# Per-release R0 points from r0_by_release.csv, read through the typed
# fallback so a missing or header-only file (until a release carries
# `rt_state.log_R0` in its posterior draws) does not break the build. The
# schema mirrors rt_by_release.csv.
_r0_schema = (; release = String, date = Date, median = Float64,
lo30 = Float64, hi30 = Float64, lo60 = Float64, hi60 = Float64,
lo90 = Float64, hi90 = Float64)
r0_release_df = _release_data("r0_by_release.csv", _r0_schema)
r0_release = [(string(r.date), r.median, r.lo30, r.hi30, r.lo60, r.hi60,
r.lo90, r.hi90) for r in eachrow(r0_release_df)]
# The current model frozen at earlier cut-offs, one discrete estimate per
# cut-off, reusing the same frozen fits `frozen_matched` above already
# computed. No extra fits are run. Each tuple carries the median and
# 30/60/90% credible bounds of that frozen fit's own R0 draws, unrounded
# since R0 is continuous.
frozen_r0_matched = [(c, _ci369(frozen_R0(c); round_fn = identity)...)
for c in _frozen_matched_cutoffs]
# The current fit's R0 posterior is a single distribution rather than a
# daily series, so it summarises into a flat 30/60/90% reference band. The
# window runs from the earliest mark on the axis, the first frozen cut-off
# or release point, to the current cut-off, so the band reads behind both
# series rather than only their recent end.
r0_reference = let
draws = r0_walk_draws(chn_joint)
q(p) = quantile(draws, p)
first_date = min(minimum(Date.(_frozen_matched_cutoffs)),
isempty(r0_release_df.date) ? obs.cutoff :
minimum(r0_release_df.date))
dates = [first_date, obs.cutoff]
(dates, fill(q(0.35), 2), fill(q(0.65), 2), fill(q(0.20), 2),
fill(q(0.80), 2), fill(q(0.05), 2), fill(q(0.95), 2))
end
r0_evolution_fig = plot_estimate_evolution(r0_release;
renewal = frozen_r0_matched,
renewal_label = "Current model frozen at earlier cut-offs",
trajectory = r0_reference,
ylabel = "Basic reproduction number",
title = "Basic reproduction number as data accrued",
released_label = "Released estimate (per project release)",
trajectory_label = "Current model, current data",
refline = 1.0);Basic reproduction number by release and dataset
The basic reproduction number estimated at each release, one panel per fit, the by-dataset counterpart of the figure above. Panels share a calendar axis and a y range, each release a median with nested 30/60/90% interval bars, and
Basic reproduction number per release by fit
# Per-fit R0 flat reference band, the by-dataset counterpart of
# `r0_reference` above, a single distribution rather than a daily walk, so
# each fit's band is flat across its own release window. `r0_walk_draws`
# probes for the walk base, so a single-stream model built without its own
# renewal walk drops its band instead of erroring.
function _r0_stream_trajectory(chn, dates)
draws = r0_walk_draws(chn)
isnothing(draws) && return nothing
q(p) = quantile(draws, p)
first_date = isempty(dates) ? obs.seeding : minimum(dates)
ds = [first_date, obs.cutoff]
(ds, fill(q(0.35), 2), fill(q(0.65), 2), fill(q(0.20), 2),
fill(q(0.80), 2), fill(q(0.05), 2), fill(q(0.95), 2))
end
# Build a fit label => trajectory dictionary from a per-release R0 table,
# restricted to the fits `_stream_chains` names, the same restriction the
# reproduction-number-by-dataset trajectories use. A fit with no row in
# `df`, or whose chain carries no walk base, gets no trajectory, so its
# panel still draws its release points alone.
function _r0_trajectories(df)
trajs = Dict{String, Any}()
for (fid, cfg) in _stream_chains
fdates = df.date[df.fit .== fid]
isempty(fdates) && continue
traj = _r0_stream_trajectory(cfg.chn, fdates)
isnothing(traj) || (trajs[get(_fit_labels, fid, fid)] = traj)
end
return trajs
end
r0_stream_df = _release_data("r0_by_release_by_stream.csv",
_by_stream_schema)
r0_stream_fig = plot_evolution_by_group(_fit_groups(r0_stream_df);
trajectories = _r0_trajectories(r0_stream_df),
ylabel = "Basic reproduction number",
title = "Basic reproduction number as data accrued, by dataset",
released_label = "Released estimate (per release)",
refline = 1.0,
empty_note = "No per-dataset basic reproduction numbers saved yet.");Comparison with McCabe et al.
Our model is a discrete-time renewal model with a time-varying reproduction number and every data stream fitted jointly. McCabe et al. published their estimates as scenarios at fixed situation-report cut-offs, each scenario carrying a 95% confidence interval. We show all three, the 18 May report, the 20 May update and the 27 May Lancet publication, as one panel each, with their intervals kept. Within a panel each method and scenario family is a single line, carrying its sweep over the nuisance assumptions: the case-fatality ratio, the geographic window and the doubling time. The geographic-spread scenarios come from exported cases and travel volume. Their back-calculation-from-deaths scenarios differ between the reports, since the 18 May report used 88 reported deaths and the 20 May update 131, with a corrected set of case-fatality ratios. McCabe's scenarios estimate cumulative cases at their report dates, though their report is not fully explicit about whether this is symptomatic cases or all infections. We take the like-for-like quantity to be our cumulative symptom onsets, the symptomatic cases, on the same dates, rather than the latent infections (which include the not-yet-symptomatic) or our current cut-off total. We read our value off the joint fit's cumulative-onset trajectory at the grid day for each report date and show it with its credible interval, the 18 May report against our 18 May value, the 20 May update against our 20 May value, and the 27 May Lancet publication against our 27 May value, so each scenario sits beside our estimate for the date it was made.
McCabe scenarios with uncertainty against our estimates
function _ci90row(xs)
(round(Int, quantile(xs, 0.5)),
round(Int, quantile(xs, 0.05)),
round(Int, quantile(xs, 0.95)))
end
# Our modelled cumulative symptom onsets on a McCabe report date, read off
# the joint fit's per-draw `cumulative_onsets` trajectory. The grid runs to
# the cut-off on day `n`, so the day-index for a date is `n` minus the days
# from that date back to the cut-off (`grid_day("2026-06-07") = n`,
# `"2026-05-20") = n - 18`, `"2026-05-18") = n - 20`).
_onset_trajs = let mat = chn_joint[:cumulative_onsets]
[collect(v) for v in vec(collect(mat))]
end
# Inverse of `grid_date(day) = obs.cutoff - Day(obs.n - day)`: the day-index
# whose calendar date is `date`, using `value` (imported above) for the
# day count rather than the non-exported `Dates.date2epochdays`.
_grid_day(date) = obs.n - value(obs.cutoff - Date(date))
function _ours_on(date)
d = _grid_day(date)
_ci90row(Float64[t[d] for t in _onset_trajs])
end
# Our matched cumulative-onset estimate for each report date, keyed by date so
# it lands beside that vintage's scenarios in its own panel.
mccabe_ours = Dict(
"2026-05-18" => _ours_on("2026-05-18"),
"2026-05-20" => _ours_on("2026-05-20"),
"2026-05-27" => _ours_on("2026-05-27"))
# One panel per report date; within a panel each method-and-family is one row,
# with the case-fatality / window / doubling-time sweep dodged onto that single
# line, so the ~40 scenarios keep their intervals without becoming ~40 rows.
matched_comparison_fig = plot_scenario_comparison(REPORT_SCENARIOS_CI;
ours = mccabe_ours,
date_titles = ["2026-05-18" => "18 May report",
"2026-05-20" => "20 May update",
"2026-05-27" => "27 May (Lancet)"],
xlabel = "Cumulative cases");The McCabe scenarios are outbreak-size estimates, the same quantity our renewal model and the released integral model report. Their 95% confidence intervals come from exact negative-binomial counts for the geographic-spread method and a Poisson likelihood profile for the back-calculation from deaths.
Frozen-fit C_T intervals (kept for the CSV export, not shown)
# The estimate-evolution figure above already shows how the size estimate
# shifts as data accrues, so the side-by-side frozen-fit table is no longer
# rendered in the report; it is kept only to populate the published
# `frozen_matched_cutoffs.csv` export.
frozen_streams_table = streams_table(
"frozen 20 May" => frozen_C("2026-05-20"),
"frozen 23 May" => frozen_C("2026-05-23"),
"frozen 27 May" => frozen_C("2026-05-27"),
"frozen 8 June" => frozen_C(default_chamla_cutoff()),
"current data" => posterior_C_joint);Comparison with Chamla et al.
A second group, Chamla et al. (Chamla et al., 2026) at the World Health Organization Regional Office for Africa, published a stochastic compartmental model of the same outbreak on 25 June 2026. Their model is a discrete-time susceptible-exposed-infectious-recovered-dead ensemble, recalibrated by simulation filtering to the laboratory-confirmed case series, anchored on the 598 confirmed cases reported by 8 June, then run forward to project the confirmed-case trajectory under a low, central and high transmissibility scenario.
Their published quantity is the cumulative confirmed-case count, with the reporting fraction held at one, so it does not adjust for the cases that are infected but never laboratory-confirmed. This is a different quantity from the cumulative cases this analysis and McCabe et al. estimate, which include the unconfirmed and unascertained, and it sits below them: it is a floor on the true size rather than an estimate of it. The like-for-like comparison is therefore against our own confirmed-case projection, not against our cumulative infection count.
We compare forward projections rather than refitting to their assumptions. We take our fit frozen at 8 June, the exact date of their confirmed-case calibration anchor, and roll its confirmed-case stream forward to the dates Chamla report with the same machinery as the one-week-ahead forecast. Setting our projection, their projection and the confirmed cases observed since on one timeline shows how each projection has held up against the data.
Project the 8 June fit forward and assemble the Chamla comparison
# The 8 June frozen joint fit matches Chamla's confirmed-case calibration
# anchor exactly and carries the confirmed-case testing history through then,
# so we roll its confirmed-case stream forward with the one-week-ahead forecast
# machinery to the dates Chamla report.
chamla_anchor = frozen_by_cutoff["2026-06-08"]
# Our projected cumulative confirmed cases at a horizon of `h` days past the
# 8 June cut-off: a forward `forecast_reported` run (its reproduction number
# left to keep evolving), summarised as (median, 5%, 95%).
function _our_confirmed_h(h)
fc = forecast_reported(chamla_anchor.chn;
horizon = h,
obs_cases = chamla_anchor.o.reported_cases,
obs_deaths = chamla_anchor.o.total_deaths,
obs_confirmed = chamla_anchor.o.confirmed_cases,
obs_confirmed_deaths = chamla_anchor.o.confirmed_deaths)
return _ci90row(float.(fc.confirmed_cum))
end
# Our projection at Chamla's forward report dates (10 and 24 June, week 12):
# the anchor day is the fitted confirmed total at 8 June, each later date a
# forward forecast. Reused for the matched-date table and the week-12 figure.
chamla_fan = map(["2026-06-08", "2026-06-10", "2026-06-24"]) do d
h = value(Date(d) - chamla_anchor.cutoff)
row = h == 0 ?
(chamla_anchor.o.confirmed_cases, chamla_anchor.o.confirmed_cases,
chamla_anchor.o.confirmed_cases) : _our_confirmed_h(h)
(d, row...)
end
_fan_at(date) =
let r = first(x for x in chamla_fan if x[1] == date)
(r[2], r[3], r[4])
end
ours_10jun = _fan_at("2026-06-10")
ours_24jun = _fan_at("2026-06-24")
# Observed confirmed cases over the comparison window: the daily cumulative
# series read off the chain's grid from 18 May (Chamla's first projected point)
# to the cut-off.
chamla_obs_series = let
ds = [grid_date(d) for d in obs.confirmed_history.days]
cs = obs.confirmed_history.counts
[(string(ds[i]), cs[i]) for i in eachindex(ds) if ds[i] >= Date("2026-05-18")]
end
# Chamla's central confirmed-case projection over the comparison window; their
# later, far-larger horizons are noted in the text rather than plotted so the
# window stays legible.
chamla_central_window = CHAMLA_CONFIRMED_CENTRAL[1:4]
chamla_projection_fig = plot_projection_comparison(;
external = chamla_central_window,
ours = chamla_fan,
observed = chamla_obs_series,
external_label = "Chamla et al. central (R₀=1.71)",
ours_label = "Our projection (from 8 June)",
observed_label = "Observed confirmed",
title = "Confirmed-case projections versus observed, from mid-May");By 24 June their central scenario projected just under a thousand confirmed cases, and their low and high scenarios ranged from roughly 870 to 1360. The figure below sets that week-12 scenario spread beside our 8 June projection for the same date and the confirmed count observed by the cut-off, so each reads against their three scenarios at a glance.
Week-12 (24 June) scenario spread against ours and observed
chamla_w12_rows = vcat(
[(label, m, lo, hi) for (label, m, lo, hi) in CHAMLA_CONFIRMED_W12],
[("Our projection (from 8 June)", ours_24jun...)],
[("Observed by 23 June cut-off", obs.confirmed_cases,
obs.confirmed_cases, obs.confirmed_cases)])
chamla_w12_groups = vcat(fill("Chamla et al. scenarios", 3),
["Our projection"], ["Observed"])
chamla_w12_fig = plot_estimate_comparison(chamla_w12_rows;
xlabel = "Cumulative confirmed cases by 24 June",
groups = chamla_w12_groups,
group_colours = ["Chamla et al. scenarios" => :steelblue,
"Our projection" => :firebrick,
"Observed" => :black]);The matched-date numbers behind these figures are in the dropdown below, with the observed column taken to the 23 June cut-off.
Matched-date projection numbers (10 and 24 June)
chamla_comparison_table = let
fmt(t) = string(t[1], " (", t[2], "–", t[3], ")")
central(date) =
let r = first(x for x in CHAMLA_CONFIRMED_CENTRAL
if x[1] == date)
fmt((r[2], r[3], r[4]))
end
DataFrame(
"Date" => ["10 June", "24 June"],
"Chamla central (90% PI)" => [central("2026-06-10"),
central("2026-06-24")],
"Our projection (90% CrI)" => [fmt(ours_10jun), fmt(ours_24jun)],
"Observed confirmed" => [
string(freeze_observations("2026-06-10").confirmed_cases),
string(obs.confirmed_cases) * " (23 June)"])
end;| Row | Date | Chamla central (90% PI) | Our projection (90% CrI) | Observed confirmed |
|---|---|---|---|---|
| String | String | String | String | |
| 1 | 10 June | 648 (470–812) | 679 (621–815) | 676 |
| 2 | 24 June | 990 (709–1293) | 1690 (872–4098) | 3748 (23 June) |
Beyond the comparison window their central scenario continues to roughly 8200 confirmed cases by mid-September, with the high scenario far higher; those longer projections are not set against data here. Their confirmed-case projections are a floor on the outbreak size, so they sit below our cumulative infection count, which adds the unconfirmed and unascertained cases on top.
Reproduction number behind the projection
The forward projection above is carried by the reproduction-number trajectory our 8 June fit estimated, a quantity we report in its own right rather than as a comparison. The figure shows that trajectory, the time-varying reproduction number from the renewal walk with its credible intervals, as the fit saw it at 8 June. It declines over the weeks leading to the cut-off, and that decline is what bends the projected trajectory away from sustained early growth.
Reproduction number as estimated by the 8 June fit
# Reconstruct the reproduction-number trajectory the 8 June fit estimated,
# mirroring the current-data R_t figure but with the frozen vintage's own grid,
# breakpoint and renewal start.
chamla_rt_obs = chamla_anchor.o
chamla_rt_breakpoint = chamla_rt_obs.n - chamla_rt_obs.who_first_sitrep_days
chamla_rt_start = clamp(
chamla_rt_obs.n - round(Int, chamla_rt_obs.tmrca_days) + RENEWAL_START_LEAD,
1, chamla_rt_obs.n)
chamla_rt_fig = plot_rt(chamla_anchor.chn;
n = chamla_rt_obs.n, breakpoint = chamla_rt_breakpoint,
rt_start = chamla_rt_start,
rt_walk_start = clamp(chamla_rt_breakpoint - RT_WALK_LEAD,
chamla_rt_start, chamla_rt_obs.n),
as_of_date = string(chamla_rt_obs.cutoff),
seeding = chamla_rt_obs.seeding, ramp = RT_INTERVENTION_RAMP);Delay sensitivity
The death stream dates the outbreak from how far deaths lag symptom onset, so the assumed onset-to-death delay sets the implied infection count. The baseline uses the hospital-pathway delay from the Isiro 2012 line-list reanalysis (onset to admission then admission to death, implied mean about 12 d). We re-fit the joint model under the community-pathway delay from the same reanalysis, the delay for deaths that occur in the community without a recorded admission, which is shorter (implied mean about 8 d). Both pathways come from the line list, so this varies the actual delay assumption rather than an arbitrary scenario. The re-fit uses the full headline settings (1000 draws across two chains).
The infection count to date shifts with the assumed delay, and the table and overlaid densities below show how far.
Re-fit the joint under the community-pathway onset-to-death delay
# The sensitivity re-fits (community-delay variant) are
# defined in the fit registry (`docs/fits/registry.jl`) and loaded through the cache
# (when enabled) in the setup block above.
posterior_C_community_delay = RUN_SENSITIVITY ?
vec(Array(chn_joint_community_delay[:C_T])) : nothing1000-element Vector{Float64}:
5085.764277041447
7673.963621376773
8902.557707649876
8034.562198542198
6894.031846318285
6620.920339964601
5887.7625698318625
7001.645667734614
8187.990681132155
7978.19368957122
9223.693788992541
4782.239104326803
8254.833232628765
5021.972417547306
5602.6938471782105
13050.088348597446
12124.55348529482
6100.155308429465
6716.31504967074
6852.74012541257
9188.37505805805
10556.853268231955
5711.8965647055165
6014.8884256357605
8759.969865105373
10171.207760807578
6461.524549669196
9306.189528622504
6929.902019442229
5865.342926984478
9663.500139571623
6459.584951348632
7666.2943892909125
9535.726963909396
6706.733520840806
6787.841592651661
6376.799974707088
6246.127824486882
5525.084725135105
7946.786995907042
6694.148299894555
6099.092989556827
6893.726221970348
5396.117900905476
5281.948122703548
11553.682838288505
11268.085226334768
7397.48969074769
6023.879439362502
4984.62132054618
11466.904248915262
5424.619007865526
6935.3906152804775
8434.806822798228
5840.972559770786
10807.579464090477
7372.311149420273
7982.954361592162
5478.1495752002875
6497.729088562753
7110.456640827987
6365.26730809987
6311.891606460384
6829.477988853286
8160.311652768606
6819.390821457161
7939.76492466428
7402.854666790536
4898.776570885539
5903.848144555923
6450.875285094536
5472.636881051579
8921.87414321292
9165.8427524154
8698.509451640619
6554.302739041052
9450.24678695783
7231.007144822909
9615.541580380248
7019.601138411541
7462.2578264819
6799.621509689932
5545.5588192245505
7706.636233253211
7804.3592947637735
6727.090689005259
6218.980442304035
8264.328652849454
9095.953179977343
13190.492333658514
7699.378063101561
8227.838718108436
9247.757592163056
6810.9013802645395
8413.76181761812
10187.938898860652
9259.476508033533
9326.165402372453
9230.751379449031
7352.389136962112
9227.377683262048
6864.629065223225
8005.038289683583
5939.678940396823
8281.741045993418
7996.724860595969
7910.667635626672
8351.407371552978
5477.386227933329
8849.459875069806
8316.490814525883
8041.427944932847
6899.968390766624
6260.932353725043
5620.655859622982
6645.664427332685
6585.648907077271
5697.623185662401
10198.802281313516
7865.38906220158
10272.313024169303
9064.975080426633
8850.33724449608
8411.218140494131
7156.234289593954
6886.713121446636
9852.017308501301
7749.419286294567
7477.261115294918
9345.068431625832
8724.228201260903
6671.023075568173
6806.548982576114
6013.748462652946
6090.324871677533
7241.374348470296
7291.784819839767
7071.239055349244
10021.876455038786
7815.658155527944
6433.603787612
6157.31915198217
8216.464575566235
6371.272973081318
6298.1850984246175
5679.096359671527
7923.379643064593
6486.617122871047
7776.364002837831
6076.572009400463
6540.462722629988
9620.15096133327
8784.386200952174
6756.249605516079
6216.537541519782
6083.1439395538255
7034.433361661072
5371.7109759089435
15223.096817157524
12224.50302841146
8911.835940751605
8293.181329231811
7635.331579051027
8530.500020519237
7533.550505910127
7331.795014004651
7835.203937032433
8581.900892107278
7739.089527737374
7737.732112826173
6904.004573808474
6548.536149106926
5893.839220506387
6181.687404063634
6520.920860867318
6310.561452207391
5257.095119762319
5760.575052353627
5654.402716331439
5608.821981149695
5842.453642361094
6538.114795615964
4857.023565009406
7817.790983614928
6270.340045909065
7370.7836004634
6371.249166240385
5888.809559475787
6671.234026844699
7171.976170959075
10273.957057767073
10057.79443067402
7401.9019312211385
7654.64784420306
7772.383610945956
6724.817050574804
6709.65324935328
7602.4645636546975
5091.805474280677
6868.527361765733
5119.1199526618
7151.136937283445
7489.916208232246
7086.965905736227
8339.759588976664
12704.627898274563
10623.887200741734
12662.687586665052
9305.603005094183
10327.955877898714
6100.48764595897
6683.614621087247
7755.7174138693435
8259.05041514847
6684.947900778152
7252.622932835522
7426.021430069558
5747.065985193861
7910.1736072803715
14557.324295144968
7103.9533241432655
7588.266865316251
7066.073904995324
6221.309790745404
7577.188557309794
7261.08620876743
9346.328613128358
6652.0176800739155
8056.437198080332
5661.463771698581
9296.317043311457
5956.671733513147
6211.493477008057
4963.454778388979
11263.385308155233
10200.570899422846
13908.95116546956
6771.98656286568
6036.26389079505
6514.777448036506
7389.25487971704
4735.939889296867
5286.158515582968
7156.4836892286
7077.739204375678
8891.462870894125
6735.675095811435
6464.16845973751
6361.733736124339
8297.501005792838
7620.005611492871
6351.682369629641
6099.370554041053
5101.065452294655
7355.8920473394255
7839.177033973527
7306.9866090735095
5496.961425856974
7463.46273742186
6736.922672059665
5390.230305292231
8987.433123282035
6006.219451185602
6302.746536497704
5995.878995970792
5785.8867390325595
6153.563804051773
9174.72552564542
8656.510322811691
8166.254429994441
6044.11001534716
5708.858003729163
7432.896069047052
12363.845340388318
7966.205640576813
7978.247359710813
10243.539529021531
10919.434454564793
13940.510137893885
9904.850441269075
8274.061357147752
6796.222349049831
7312.275995613671
6846.9807470358355
6050.19192167438
7636.204994970376
9464.366317637625
8743.72860552336
5926.202518218319
7530.136988924911
6385.297992113514
7712.398046934231
9309.47242486598
10685.294364862444
11922.332787751673
5746.816711952252
12658.101224604332
8870.069084081739
7558.186550685755
8003.231441673092
9619.573672516086
7149.906249395242
5225.150496071548
6640.2023975749335
5696.656731606194
4916.553143778209
9642.816741727058
8389.408240618592
5564.265363592901
5212.329162330244
5067.472351396241
11437.794572547817
5422.057890536032
6206.614121994366
7309.424861752951
8130.562502250385
5662.120632604549
7396.054068416679
5980.639970575235
6274.501324957871
5723.657997341566
7370.236939200058
7260.621464033194
10314.228470302876
8545.670616330523
5974.129283122742
8779.557819382531
6629.165776232128
9457.56406560442
7420.622165065229
5834.827392789524
6307.853495823186
7913.006877585776
7441.081669658308
5199.354453821305
7340.531583654367
5668.718481292821
11746.091023751735
9273.33467141922
7281.1507843266745
7947.760813611295
6734.315480211152
7212.420247728269
6991.5042797860315
7424.574326093896
8623.350653886017
6705.3088304276425
6546.8082825392
6665.693652241852
6281.360528436258
9311.862932439597
11075.240582693592
9275.045584756352
5359.990723839583
7685.73392979772
6852.3122544917505
8011.158098620021
7180.42818535842
7188.789041694924
7148.9942463172765
7606.917105688427
8644.659110261071
7150.65875617231
7669.19068541522
7622.999059257495
11212.29846380043
6499.682637998273
7288.648181906534
6854.401072585236
6208.914165961109
7295.384289137575
6851.326562290553
7388.900492168128
12512.718828117026
5581.6837364206995
11282.918102283316
4866.946539898971
6022.534954748
5724.4795107505615
5723.40823470828
5972.258242164248
4929.815922441612
8861.854261780823
10113.34183340483
6263.347842180958
6999.280274624024
7392.1121959726715
6476.0838576288
5942.254591426381
6453.033136988629
8890.55471006002
10890.62487582867
8661.947502452998
8789.172793806458
8487.681273068267
10866.07150513631
5874.556508956971
5980.752317605461
7977.875001754171
6340.882540232122
8133.774088571156
5727.962888789454
8368.621527599309
8853.123987665384
7331.164565968214
6105.489597864812
5156.444065418417
6023.4636462017925
7921.573529270572
6819.543069439206
7615.234906557683
6792.166128306419
5806.415222927657
6825.4068488421135
6554.1801449835475
8357.950077908466
8036.946102728122
6191.331505872691
6647.264370578989
7892.652419870402
7888.900421126032
9295.775059214498
8418.857497074914
7408.550541870847
7190.874298772002
7350.057853870685
5604.655285079767
7934.877829753083
6281.746343971563
7004.544554128428
7093.5846944553905
7675.733534352055
8530.878867175552
8795.178209014444
7884.237566548873
10729.417419523757
8069.371633533232
9905.581716210138
6228.390686281078
6582.328201522896
9519.427166644835
6383.187979978567
7647.206039767817
9858.682775430161
6981.899140998751
7963.700006459147
9086.952931721173
9360.27048821041
7762.347279228739
7664.68442819268
6072.399223182741
9433.099375930995
10143.225341545423
8163.752957974717
10361.55840813807
6738.095256048003
5945.332239650147
6649.298324731436
7850.88353540655
6429.710383827799
8878.260364957714
10070.112863134222
8219.6278779625
6578.94016884463
10124.179624419297
8911.41712975959
8999.757684757606
5925.233436122522
6382.042740289164
8673.520231943297
7222.038426589173
7226.653483813401
7226.653483813401
7596.914768019955
9283.301726279811
5305.142277315525
5975.271104987193
6695.2943885627865
7935.526947792152
8394.015002424336
6463.385746763585
6636.805103859085
6609.378298900837
7850.047835367273
5994.534059203679
9768.396712341952
7132.744651148125
5494.772232934494
6007.353624213436
10133.970662766014
8565.966748921452
9944.225084561618
8739.942795284464
7004.25953225594
8007.844194284009
7062.519941055328
6368.109246134409
6079.869637949579
6391.536863874076
6391.589780582106
6304.366591986304
8984.334021795725
5508.911961649347
9739.641919807333
7633.096438275698
6508.741227366063
6563.573137809664
5436.473463588229
6927.097370137597
8866.660702982912
6438.071477522516
6988.573775964308
6805.2727836579825
7385.799730667772
5243.006319600218
7236.068734855731
6752.809007332811
5781.022530567722
10195.535858018
8209.061480969325
8928.170935283242
8111.010688279879
5945.392533902628
7990.963421585615
5405.657139457888
7399.958022544895
8027.88653193869
13622.256903265008
5462.619004388565
8449.229808404296
5129.620370193607
6669.911998915473
7747.3022052117485
8117.531393440335
12032.461501632573
5735.360000446959
9216.905139474471
6244.402258832264
9257.457006449567
6310.343613342844
9458.778295048121
7318.705610185246
5993.233874063001
5861.031900888526
13663.78528649396
5391.185582198811
5429.641930678428
7461.442474067464
6393.978492685509
8941.596920524276
8026.48972043595
6071.969688481912
5308.730779633757
8870.520545481515
8305.30190358521
6938.326566481419
8680.060592062848
7763.652854946681
7757.490071488903
7998.252418207945
7761.597430122187
5818.227011089184
6266.553956947976
6818.473518785098
7348.02097049035
6379.874346151311
7350.843805447296
7700.869097178031
4330.035788226813
8648.645464737592
9226.172182553231
5459.554754872294
6320.3061743274575
7409.209706817611
6483.336416647301
5840.065170428008
7195.908700700571
7467.486378093038
10628.929106309843
8722.439984251547
8757.239007307755
6809.296412871691
6740.49841479949
7026.751033304067
6109.901844630129
7514.537065426407
7974.3935198028885
7405.25587319379
7313.102943439394
6205.858285198621
8037.406852543895
7057.517436126824
6964.217648633064
5600.7978205335085
6285.300095233112
9019.335380510096
7892.606141555845
8026.526809629915
7681.908517470814
8401.081650811018
5759.735289121191
6342.369205520035
6373.788779811576
6483.350217737376
6612.794618899878
7695.11707831906
8694.183240117038
5435.367125380937
9266.353418953933
7536.259804419798
7593.582423400249
7611.454752557852
5898.335090901296
6140.663858422136
6696.412238593277
7717.29252526889
6538.254404394207
6900.300345806605
7114.3580903843995
7180.862742191815
7771.536249965809
5761.130448414906
9167.437254720247
5942.7930079389635
10613.353073086684
5558.256179424453
6216.607562567499
11062.93673958564
6623.344887613885
8755.504642862801
6098.077564793534
7184.794256828907
6340.254269132117
6537.859353471435
8036.149650216374
7664.683606692115
8252.311951695961
7883.745646182046
6182.152459100931
5709.6035942390845
6340.59187220484
11662.568577422355
7653.801591615198
7872.744349562595
8322.150374818515
9111.97184923511
6566.393190033129
6360.015937035304
9674.575766697431
7898.705734962825
7156.670562760317
9343.150171598743
5735.180734986813
11801.131714977082
7235.956320101653
8382.832344279352
8104.196891533559
5922.059910610099
6614.997918194423
7997.387703770603
6503.933773414385
7901.2702540272685
7687.580825640972
7460.060092529913
8341.815834740119
8340.576443467913
8104.099201099187
6400.00570726317
7357.960434243624
4949.010335520774
6493.4001579047135
7367.807158547165
5970.606571330246
8389.248870841453
5561.149711637061
7482.669735251703
8270.506787497963
10278.957914930692
8554.20412190683
6939.979565730487
8778.799713540875
6710.570519487232
8792.99665608067
7468.126267854611
6953.633091013442
5626.726818332049
6273.354902308454
7698.504593490946
6458.856582339585
6902.147986256449
5350.473991148977
5656.284374069644
8430.509667117767
7493.565864519335
5675.2695519298195
6464.255039238453
7328.884984456688
6719.805150321427
10571.54815359989
6163.461369803919
8886.058779597432
6264.111509614985
8449.950112005992
7102.774379205737
5665.756431333502
7746.802311807591
7593.232962555895
5509.521247910688
12311.421687410317
8255.995978209261
9119.92082035031
4859.525514932333
5507.565590476053
6379.445174320514
9438.517814683952
9236.780971296299
6795.531781395189
7334.264500060185
9274.38381751156
5667.240106786474
6095.863808994612
8905.75525542341
7179.106865165063
6555.307647750417
6349.530370255143
6958.802558427163
12196.714741083257
5989.321092525023
8982.607060663
6597.59362791893
7493.6110308564275
9691.30365829319
8086.839134154294
5782.238321351221
7265.151009477697
7816.615999421929
5378.38931329166
5812.8714639089285
6399.345846346883
9533.374995309245
5985.155475756013
6768.589534692804
7822.928331065243
7776.066874763652
7906.697493436891
7897.129771515444
12442.021119433015
8782.098012017765
6818.047278006512
6528.960203070528
8239.251043061056
7430.95449294713
8649.327146324016
6647.2833904762
6757.738329984554
6952.4797393025165
7017.245659126435
7289.336956659633
7068.223005549205
6666.856922290748
6924.353029206856
6842.294443196298
6180.8349795979575
7930.576026961762
6129.095745606904
8586.633801209586
5833.2047191714755
6446.68049995113
5868.083174525071
7751.9050278025925
5936.886011236182
8502.003412325399
5324.643086056082
11667.842245877495
5977.718100544273
6576.349930933788
6237.291722505983
10978.321077614897
5432.751280688994
7966.8946337988655
7469.178873531358
8245.227954668966
6406.051470761342
9395.09380133198
7545.667460284665
6232.315595669647
8559.5371571972
6075.05034387856
7180.704598910583
6577.934530507426
5454.951251722502
5733.3659463633
7301.940451174974
7525.620264063137
8174.023391120492
7908.843854478403
7038.87039885012
6255.279745076499
8744.079069640502
6848.884440388302
6252.702510824235
6552.034938175545
5811.172410155017
14017.3730800716
7179.381731085018
7045.106593852267
6228.687732647556
8103.183180284863
8130.876353630824
7603.039542458257
7689.082983725562
8171.503167465774
9255.877688901368
7571.99010558569
6415.165956769735
6386.885358867847
6729.150061152102
8562.130323386924
6863.902852710513
7417.880504003892
10018.329381961852
6582.412715701524
7815.585921485077
8204.000426982173
8808.855162060076
5958.644540091534
8151.684724315546
5044.118700753443
7432.079046505756
5412.22495673037
7803.031627827975
5897.182599770347
5368.350922716104
8228.698606870814
6394.786761122341
6902.538159008034
9592.357789832997
16308.210641646869
5559.687961278926
7240.7926944356095
6628.799636599984
5873.194175422088
7816.019446173237
8131.720834045002
8223.89768085475
6780.6422012231
8210.308114881982
6021.595792314834
6452.886259669066
6686.363367280727
6339.539518289189
8357.900514547295
6895.36131240174
7451.95232165978
6780.135546670269
6826.556623158148
10178.670696576646
8333.836120303455
5591.2399741754225
6822.319627602674
6460.875847002663
7496.474099413723
8950.733952614457
8964.461572767568
7511.668567124795
6495.466730563741
6171.530183227284
9929.414216382642
7113.887801839509
6507.378908502863
8230.530142227233
9491.959169227384
8214.401563249465
7326.1065963380115
8189.145427135971
4890.441529715784
4895.372396523806
6626.987989528923
6645.39046309611
7504.1400470211465
7063.266724512207
6440.287179893076
4751.699899155006
5165.50779947095
6099.899104815303
6765.266257780052
4807.1751994764545
7847.102441475443
7944.02882154952
5686.478484794335
9308.092394395993
8214.251384392544
9044.770721532963
8349.459342428108
6299.640118137821
11404.823603469571
8269.014070399273
5426.450835296715
5730.977233093415
6527.0938727145285
5978.500033419869
8226.24359414678
6784.47454867366
5092.112053451978
10188.806623600658
5452.888997887396
5937.560149471831
6186.652121602099
8989.852020563194
8583.42305891524
5769.99501571036
6936.929183235013
7705.777230157384
7198.5476544660905
5843.235642312514
7369.115852049974
6621.999683437474
7276.9621661209285
6803.699243221332
8723.98527191929
9689.169569667067
8120.48641418952
7992.549813409023
7711.125436852071
10697.349801146313
6232.022392778512
9016.532730389365
9935.665261924134
7595.795192362808
6841.303047535638
7896.14999446235
7626.271070300891
5868.836114148345
8402.271507257201
6332.747758215398
6879.756235939616
6250.393104066803
10394.13621547864
10051.23151377191
8803.319180065442
6583.270840142833
6085.919895955987
7775.9437406933475
8624.632516389695
5574.496964716487
6930.704838177386
8705.521243582743
4810.997414593621
9182.058843398128
6866.916303870327
7116.261335368631
7113.303444903821
7648.250160630081
6920.4336095101835
7227.376911650629
6165.94177782371
7789.738411950441
6117.6836534777085
6500.793802570523
8780.451323448233
6062.536942308842
6331.547217062481
7711.014008356504
7617.421280027841
7856.017523068377
6417.070004672004
8591.832606740994
6336.783868868786
9611.781586843368
9295.369169174475
7387.1616928649555
9094.268019943689
8401.292504540485
8822.374129664477
8026.347597332491
6455.433500872523
6220.7159135080465
12299.766439756364
7445.737908588587
6927.677860376865
8313.531302763216
6362.4357352125935
8605.212662930115
7335.91904403441
7645.222294871501
5471.883438115985
6333.869927643756
6020.874577847515
6368.166658386857
5167.722241976639
5505.469303036447
6334.966382052718
6357.160693140214
6546.193623893993
8118.039028070018
6048.705223667392
6003.665996582916
8496.833375289021
6590.859182119258
6056.505108013587Delay-sensitivity infection-count table
delay_sensitivity_table = RUN_SENSITIVITY ?
streams_table("baseline (hospital pathway)" => posterior_C_joint,
"community pathway" => posterior_C_community_delay) :
Markdown.md"_Delay sensitivity analysis not shown in this build._"| Row | Stream | Lower 90% | Lower 60% | Lower 30% | Upper 30% | Upper 60% | Upper 90% |
|---|---|---|---|---|---|---|---|
| String | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | baseline (hospital pathway) | 5702.0 | 6375.0 | 6907.0 | 8028.0 | 8993.0 | 11219.0 |
| 2 | community pathway | 5424.0 | 6151.0 | 6639.0 | 7820.0 | 8645.0 | 10402.0 |
| Row | Stream | Lower 90% | Lower 60% | Lower 30% | Upper 30% | Upper 60% | Upper 90% |
|---|---|---|---|---|---|---|---|
| String | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | baseline (hospital pathway) | 5702.0 | 6375.0 | 6907.0 | 8028.0 | 8993.0 | 11219.0 |
| 2 | community pathway | 5424.0 | 6151.0 | 6639.0 | 7820.0 | 8645.0 | 10402.0 |
Delay-sensitivity infection-count density plot
delay_sensitivity_fig = RUN_SENSITIVITY ?
plot_cumulative_cases(
"baseline (hospital pathway)" => posterior_C_joint,
"community pathway" => posterior_C_community_delay; scenarios = []) :
Markdown.md"_Delay sensitivity analysis not shown in this build._"Tree-prior sensitivity
The outbreak-age estimate depends on the coalescent tree prior assumed in the BEAST X analysis. The baseline uses the more flexible Skygrid non-parametric model, which dates the common ancestor to 15 March 2026 (
Re-fit the joint under the Exponential growth tree prior
# The Exponential-growth re-fit (and its `tmrca_days` offset) is defined in the fit
# registry (`docs/fits/registry.jl`) and loaded through the cache (when enabled) in the
# setup block above.
posterior_C_exp_growth = RUN_SENSITIVITY ?
vec(Array(chn_joint_exp_growth_clock[:C_T])) : nothing
T_skygrid = vec(Array(chn_joint[:T]))
T_exp_growth = RUN_SENSITIVITY ? vec(Array(chn_joint_exp_growth_clock[:T])) : nothing1000-element Vector{Float64}:
138.18298857851585
136.91583188738045
148.38952835897027
137.57015849156588
157.1499192908903
136.55597452498114
137.3870881064551
144.21945479336793
141.25350368882206
139.96031453503943
140.15345044458599
152.68565726565458
132.5933019617249
140.26683399866167
138.55662617675372
156.7051909667075
138.30649992522672
148.1763080229829
134.34667667145894
155.79581590117394
134.95929475926653
135.4784138251859
152.51011393919492
141.67975742120242
137.5357582835957
134.2031220264321
146.653670811794
139.78251376410245
135.55813466414205
140.2167903450625
162.06418717654324
145.48591825560777
143.48298148613065
145.53527209490483
152.58270370328793
137.74783214183407
140.81302830476207
146.5589039751056
133.97939038547548
136.04053727066986
154.18446140261034
139.44850427211742
140.52853337722365
145.67530434964482
143.02963352931368
143.03350164883398
133.94720881873323
136.91610746809877
150.25040934515116
140.88947611192833
135.9653911620049
148.15635821287412
142.98747572776972
135.03652369306081
145.9942333332643
141.4699449504244
149.68340146835786
138.3156904064394
154.49454861316352
145.27076513144448
139.63326352562015
146.188293733055
151.0166612273706
141.63956934685515
141.95602093317714
145.8358444299448
137.32342357062433
140.39316125397735
139.27615476319207
138.7553276086246
147.5835865880325
141.7116031909064
148.23263237549253
139.43651479703453
138.56406179577633
143.60254737139323
140.18553807133407
157.37368544146727
135.7400314222732
134.2280914628301
134.00040033718426
133.8845692224694
153.95285302238855
138.2978113216176
144.57458439989514
139.3464513291748
143.57261261366344
133.0002540153141
150.92083636536876
134.68713617604513
154.38999494658097
133.24023169862159
151.46317074444892
139.30288000990518
146.2167600924638
134.97267406802735
135.32358417081363
142.49566508455567
135.0283574158233
136.08403266220503
160.83575965143075
132.39367001394416
133.78021842047116
155.14057870660773
132.63952889850023
133.39521649511798
167.1456978689996
150.40142910082136
134.98969020020849
159.1767366746774
139.0040087868306
150.3226207758186
142.61405127551075
157.33347680139732
135.2012145217049
151.56113488516897
139.58794153654864
142.8045772428114
147.49021647641598
139.65029331263383
137.79273156024365
140.89903446752564
144.65996567699696
141.3428437466923
147.1697011321074
135.89843945010819
144.58197114193183
146.01728791382436
140.09857046084423
137.8991596908404
163.1891656365512
153.69990176486624
145.05822082960552
142.52606416631028
142.54135618522997
141.43134839306038
139.0788420066903
140.58050461299587
140.18829090223488
145.31782601283723
136.17920762282895
169.59327946310918
150.74327012163377
140.59549944438677
150.4554648671708
135.9012997649346
146.6431250764788
134.53081802513415
153.45376955713866
140.00580261251264
146.63773602862327
143.14309068267502
147.84759470449865
135.91880620546283
140.46417610998844
147.1824612505841
135.9648237824857
146.0302784385136
138.71077872485066
142.44776190271546
156.1595685853921
142.71333220438694
136.54196394365553
149.86831607049055
142.20199373641833
140.18506973543634
144.4717911527728
138.35261423274378
142.8027635672488
138.90876762309134
140.62769303598918
139.40904285960903
147.36417206899088
149.2782598235517
140.38085756366388
137.01764687790597
138.69434520849927
142.213930698276
134.01484860924157
135.3870916802979
141.75470595151546
141.9274888920499
140.4620840901672
138.038405989229
143.11592925047648
136.3570265101921
136.73143226520565
137.90905649846079
138.45544283194724
144.54458615195517
149.6717807720237
134.97406832669768
133.32430227083458
156.38339550104112
142.4201834947038
138.20486799478468
141.29859160592596
146.02264640428123
142.34936867905446
159.50236574972223
133.28727864481056
146.21127390541812
142.37636734651406
136.47290313670442
156.74431900441965
148.52130962450548
146.9649976199919
139.70316796981086
148.98706430118162
141.10739776589446
145.62426151349038
136.57188806022927
150.74202310897047
134.96420671765486
142.3652067081267
139.0495285790527
149.7256386349195
145.4836858122757
144.7300442385785
143.45958857486576
138.00168254448002
142.3185867590743
134.5169322632746
156.12859827918206
147.44173652058657
134.38066059737284
162.91743616497678
143.294522071532
141.49230639700517
159.46374368946343
132.65225629550196
132.00728718080566
132.36462395443542
142.18740408496674
145.92451550835318
153.26601958948908
136.52120048379038
135.7005993906605
146.855585984224
139.40193005779565
155.1952535706822
138.15617608029075
141.49901544984448
135.7664459616034
136.7056241946108
138.92592647620683
134.84514764047438
146.76475532523662
136.01662845002136
166.949853149045
157.6099151107233
137.14723389273462
148.31027986334863
136.45463690440093
141.37847526603707
138.7258533836099
144.13493272530974
139.60154041987678
135.66095767227426
139.13966911130632
147.37526796986356
136.12481106452984
142.19682499928388
147.77537215263692
134.03615178464187
154.55289117953916
149.3970158886978
132.92016371900223
133.0877450128496
132.66896000267786
166.31616304407575
135.469025496874
138.89048753068158
155.51067337046675
142.32839013921136
140.1883830978836
142.6983495569676
133.39338032411194
148.92794324334665
137.5125583449603
149.45299168296654
133.4057387569731
136.15522330757676
150.3351125289596
155.01309744853023
138.66533871583238
147.04333941207813
141.1661445653613
139.83715887202382
135.02156555288434
145.17171292231586
137.07808484133508
138.62280503110202
143.46342511200223
141.6201514884516
140.39616321708644
139.93843621393464
137.68136613786214
138.94910891566184
149.5702443159178
133.6145600938776
151.7655259669491
144.4754034813016
133.91390706679223
147.35084879329935
143.23710463057301
137.31293539096038
137.07005916582355
148.08747255595156
143.80941411891982
140.63922766211255
146.05437330713355
133.83598177441453
132.52990684223334
138.361031298108
134.88881630239987
153.57231899652177
142.4919867110937
139.55829373236253
147.8488858777627
135.7438365145498
139.66618475330694
142.78314141751667
137.68439899668533
132.48077447992068
132.4150833180665
132.80930596185846
153.20200707105366
133.36544680257424
136.38985362834637
142.29542562749543
135.4938098066727
146.6128340733686
150.48690743522386
142.31728692061196
139.70090542991193
155.45038185574694
151.55284906957456
163.6294685347226
157.1869529726531
135.28558242312445
142.87575250991534
136.95210874481765
135.47357886292974
136.54084522303728
133.5336730758493
140.4559590736441
144.91060658004423
146.4891094436213
133.87071356840983
133.00682294319304
134.4546938867706
132.29096937683278
162.26546340251798
137.10232050317487
135.73432430392714
149.50455194180853
137.87574088916566
142.51253021585418
140.8522532958385
134.23056264479504
154.24202835404867
133.8368439516969
145.06939441243134
153.08777966620582
144.9226676781911
139.07223948614364
153.85223869149917
137.16470084064403
150.97281263243855
149.64398415157635
139.11853731916227
149.45129923199113
140.48901072693064
133.97885721433119
135.81005899037876
145.65587461125145
136.56288461683195
140.44285388894522
132.7057049723675
153.30309845604597
134.20094045519042
157.82230000360192
132.47612115513047
137.53175875931944
141.92462541087875
141.76462647112447
143.2394590993895
135.7025118760957
144.30274143259606
138.1244626831092
139.55640138541537
138.12053876181363
144.7145994601008
152.56274094324854
139.84439468783836
133.8138211742227
132.234780289268
140.57049781744726
139.14149621662662
147.56978007734958
134.66937959494825
137.10656637565992
159.05108048593007
142.83051473786966
149.73931023823732
140.12086034637858
139.01771283882232
137.17720603147708
153.9678075496257
137.1701456625488
149.26329775120163
145.00331716008216
137.78754742924767
135.88983695565025
137.7173639104562
152.26259169003168
136.8408745305863
132.95865726673
142.41209124089613
149.30026524008213
132.9848132140363
147.8619524821042
137.8947308055257
152.71771875128894
152.0007270174347
150.51238465319406
153.39320943743678
136.48944373581028
134.79341989351914
145.4034555629047
140.6943898805958
142.5870723292799
150.67491460635517
160.78762500681745
133.8625919360066
144.68885956762205
132.60007942222734
179.65634226657892
135.84376919307394
147.5597486320484
142.50669095041837
152.81596959595902
138.41064901949989
142.89377260567952
141.4717451131566
146.90169987426953
142.99708069514887
136.78938700748927
151.95305471224276
138.9760876238238
139.50076922072034
136.02616348151355
146.31245627079107
136.96841093399365
145.4398910971489
138.73590001135923
145.21774017928715
143.89450580529333
147.5397178178845
135.57351254948662
143.78450692988554
141.18307160118906
139.11670529685668
136.78255736691793
147.28916421515066
144.15442553189703
135.80346889239274
152.3830104870061
158.01892371665127
143.98560908348432
143.1128062807586
141.44927440073326
138.01117644480829
147.22010366314737
139.5567226034613
140.06526823938455
142.2321944302569
136.9469495252663
152.32721765931305
135.95671457271894
145.6497433721614
133.85939009970272
134.79456613661714
140.49363112145318
136.52227666468258
154.17912770701778
145.46789133328176
149.01607562035724
136.39773812011484
137.53968085348285
135.03453412406932
138.14698417682698
137.09526317432093
142.40931513704064
137.98482680740608
141.37718480325734
134.60560087766817
156.63105547767782
143.93319226859035
139.98875575562874
136.56081784697085
145.1483690166902
145.3239592848243
133.17398309215028
157.82010441348157
137.66560202346562
143.3924453476687
139.78823269713808
147.86082627108487
137.63487939518006
140.0003616652159
143.52150360272677
143.27004554727267
148.37128927799452
141.771891768558
132.36170241475716
132.71447279658625
132.13289446954755
132.34872220957263
134.8293499728772
142.93661367763818
147.4595358261525
134.38285928529493
133.21038026257472
147.9880456085761
135.92101642227388
135.97043731003626
142.88158837722196
142.89735411528886
137.26199459496718
139.85908141203674
136.43770241074313
160.68176651340238
140.38781257081882
137.56702454569816
139.53586911443057
162.31154017737134
142.5503437827182
137.82606322091394
143.39351654085178
139.14961451128318
147.53041293709413
150.02843266788673
140.4342079719341
153.8060810393266
144.8064889058613
142.14908070717357
146.61003435806037
142.7966716181378
136.8935623627523
140.08886439020898
137.01663142035127
137.44660749256983
143.54328668185875
140.56271235445703
136.0770894785861
158.65668091943752
133.8732840441083
135.0656583228362
148.10050551568196
155.80692141475367
139.8442040018234
138.34292639138968
138.34958756686734
148.27157449336673
151.1389441459342
133.87529480481479
154.85775662163093
137.9610171944539
146.48678970327316
139.18451216951365
140.27412815475003
141.5056303868302
138.36613476251068
148.53554176858555
136.32999882595178
133.63641376168468
132.6516497985457
132.83104686766137
145.70323125084158
139.099678254994
136.5699000662754
139.3913386435759
143.56091309256544
139.13127202779015
139.1865969044055
136.5478968627637
142.78570686277655
147.68161891963948
150.82432536606387
133.763266101075
147.00279106986474
135.7876056849119
134.47620169192143
136.0322996983361
155.0136424897016
132.78878482664152
163.75965348950058
137.62608140128197
145.84098948999184
148.38720919022086
135.00086486996477
132.65245290265446
134.35064497466266
143.63244737443895
137.38407126158603
146.1974661450396
139.92438729054817
142.10072574211173
139.35126837789656
140.56691299601098
146.00260266669707
132.13883384150873
145.18433242390063
165.0082002429389
171.98412155822174
136.05919200247703
150.43271686014043
132.90075308665612
132.60594486106987
147.3000878820682
138.67820845151587
134.22820421441773
148.43200854038017
137.4829039328529
132.04743460804028
136.93179611712873
137.36253275037774
149.35555948175184
143.3231155290504
134.90518774325702
141.50443043146188
142.77078461315122
135.98329495224607
134.10717784035845
132.73759561118402
147.34552360204336
139.72178062399706
149.89013581144206
154.25424853196893
139.6877014794308
145.99723768818396
155.318691811165
134.914994526005
142.6611309661181
137.74430291082382
143.5727143230035
135.5288709028415
159.31111919493992
148.30879858828828
140.16777080181924
140.86946737302995
146.02383146457612
138.1982737760411
132.95500136176508
144.47265971605205
138.56713037784093
141.94201915610316
134.11128436193675
148.03743458575346
133.87368152744378
157.3544352717064
145.95761421133201
136.06435706265157
137.58910306446285
138.21047556937447
138.920724265325
153.37624170705706
146.7684397359796
133.7748831815374
152.12943611332733
135.63254591041292
154.38502040375107
134.85388128138635
143.192341265985
143.30904676737833
134.40565033074364
150.12227735325908
135.5152226379494
133.38355070116083
145.25090287195454
139.47005933321168
139.11805138722332
144.9317891430088
152.7199314096984
139.16618995884485
155.43910282968272
137.13432749363713
140.70455303201223
134.49025050676167
138.28058893152377
132.91898890734137
137.78961854074845
135.92882462167907
132.20920764617836
132.29030531139804
132.74910737888666
138.43431389830377
135.1294402983072
154.3599398009245
150.97592101863546
147.06360572779752
143.87751298527024
143.21128393573773
145.97865234154963
146.53477844415866
147.46353493150596
149.33362521730703
142.67235357129297
160.76547243767766
135.87416785135892
138.71778072147495
142.0573248285483
149.65020352592455
133.2151386121966
133.02823802403458
137.45127852541287
133.95089716351623
139.02164010295684
133.4087827803794
135.25759989754806
133.53671527366248
141.03985134748314
132.35759178388204
144.5009514033462
144.0480996549489
133.50659381479267
161.30530373385085
137.6298526499011
139.08087110514614
146.14681839887547
139.7020133867588
141.52448825359352
136.427410698648
134.87862074617706
141.48139605854004
140.47456463002374
135.36867012503964
135.68664254408546
152.12227718721326
142.62220489010724
140.58705531406108
147.81498605940834
145.3869723266837
138.9754884455423
137.3620461452437
152.60548992336578
144.81334201137037
140.78257349698046
148.77963404434857
144.4634482987918
135.60490263034754
138.78121564909426
141.92829795936652
136.0750505351589
138.34521402823464
150.33386036848873
139.65066432545734
141.05652869540646
143.83337177248615
144.98443413174357
144.16459324392483
136.8745955348415
139.29996478317167
147.24333247804546
148.0857373689608
150.11280097103565
134.95311364028245
135.2311496541091
143.65143452202284
142.15748792070926
141.7124011736904
141.0366055746455
146.6338626571871
135.9415817419579
138.14866935303908
133.14628250176497
132.68233363299797
135.5498038359873
149.66070261367813
143.9147985288551
143.84704035173388
141.2922848573009
147.40780808954224
140.38349040418905
150.9235535701223
136.56346121671876
140.90406629239567
133.97413309516492
132.68130849724514
134.86898049408325
146.57908810866368
133.8531653698285
141.06752146420868
142.84910137633423
143.22148392645906
144.81482298098894
140.1956271019424
137.20666562376647
145.32419581272234
150.3359453673898
139.992658281229
168.69200172883333
138.69697482060292
151.59756677156273
136.88219764939024
165.0550424136085
155.31435052294188
135.26858075619677
150.4587111825205
138.39854924691173
150.17191885835888
146.76774087176216
144.56576976989632
144.79770546120395
142.51911676419144
147.93428788898967
140.80266232130361
139.90207433726454
143.96726087623014
139.19192734440745
143.6543472960615
144.35846348003076
136.75349585133586
137.7953850504461
143.87178535221952
139.97490809015466
137.38606802780362
147.84627987899583
152.74790685613164
137.91327332595841
158.72136382770307
154.10149404521292
142.916207388808
141.6593822170666
136.40071446187142
144.96420273281893
139.49432237307525
158.40973501487065
135.04698965884714
132.34244581497617
132.38160545846662
132.1429829993406
134.67402875809282
135.3095745965835
140.00868887227566
147.15644214103264
146.68037248877314
141.68252310269827
142.52699755931394
146.733095481625
132.1768272662142
153.91977035276182
138.7527110454845
148.789533660354
138.74938062812058
140.05302622497786
138.45514839812103
132.9249784733295
146.69065204920574
146.32740416500707
144.6577774025126
144.25772243167194
137.52267326261122
139.99967446725978
132.76570953710043
132.83397700392223
137.88692421737156
139.9135367420826
141.56758406499176
140.3898049515956
153.46523866084777
140.23845435770974
147.48629132158368
145.33171304593128
147.3358355572955
139.39565305721428
157.5412840409359
133.4908677016854
142.36365701060848
142.8865240786101
138.86095527345938
134.30155321263354
156.85004496726376
142.04608166260647
158.43244629364554
136.65652126867298
134.21809602999818
137.6647276994313
143.88747291804196
141.7120809609772
138.17118150489011
160.32702566171403
132.91471250759213
147.24527954131358
138.03613008818607
146.78696605599544
140.28252372257958
137.80661238653707
139.59251770914852
141.43507706359694
135.78896997200417
172.90927938356828
139.9705445512497
144.5547824978202
143.33798109634301
143.47534540160308
136.7178650041088
137.18514135517
134.8439205330536
146.24566088886533
144.77007531047414
145.31574683453266
139.49025930596324
139.96742613261748
133.72648322286085
132.52243249626127
150.7047888922998
132.54128852413038
160.167844406659
135.87100000209884
150.9916755386713
144.33925701647155
142.24247211388277
146.2239152625379
147.12302474705143
149.42128639973424
149.13179399731183
140.07186634676842
137.8896986423577
136.3039539983141
141.5256258411044
154.35668756745793
141.8445535593225
137.90557642727308
137.86252132091974
149.70123737826344
152.2822817176749
147.61088110319457
139.0774069017167
137.3223623710762
141.9793382585639
135.41640261320967
136.3240483172738
135.76870214604375
141.85174543547913
141.11981291946577
139.60527088130328
138.49785764645407
145.14735787824614
143.7682135302426
144.03003413661287
145.03612952399507
150.76117953856158
147.37887596519016
137.1337543212457
147.62174300295277
135.44364172975182
168.4359436902153
138.49490334800964
137.0467013327258
147.76384747564217
136.13637914489422
136.61679056356508
134.47119625021375
135.21232643131196
147.76702725618645
138.50140738902155
150.16854504417222
159.52976863567696
146.6177596699052
142.04421535678986
144.31354722602225
143.27279379895504
142.2262068205196
149.13153833878533
134.87173390178395
134.56625731807523
143.4872026179759
138.97372247188235
160.5483317416155
141.00312494219764
138.7329257065384
135.5143276271026
138.45588720892758
140.39016427510862
136.04950362201123
135.67961794609835
139.49279596913073
138.43520230341986
144.18402748912084
135.94224254525963
136.46343493581395
134.05150829835955
132.66354173767192
135.18801683639884
133.10309170375186
132.89573643674166
132.3878462777336The infection count to date under the two tree priors, side by side. A slightly earlier common ancestor (Exponential growth) permits a marginally older outbreak, though the difference is small because the evolutionary rates are nearly identical.
Tree-prior infection-count table
clock_sensitivity_C_table = RUN_SENSITIVITY ?
streams_table("Skygrid (baseline)" => posterior_C_joint,
"Exponential growth" => posterior_C_exp_growth) :
Markdown.md"_Tree-prior sensitivity analysis not shown in this build._"| Row | Stream | Lower 90% | Lower 60% | Lower 30% | Upper 30% | Upper 60% | Upper 90% |
|---|---|---|---|---|---|---|---|
| String | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | Skygrid (baseline) | 5702.0 | 6375.0 | 6907.0 | 8028.0 | 8993.0 | 11219.0 |
| 2 | Exponential growth | 5562.0 | 6364.0 | 6859.0 | 8063.0 | 9019.0 | 11080.0 |
| Row | Stream | Lower 90% | Lower 60% | Lower 30% | Upper 30% | Upper 60% | Upper 90% |
|---|---|---|---|---|---|---|---|
| String | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | Skygrid (baseline) | 5702.0 | 6375.0 | 6907.0 | 8028.0 | 8993.0 | 11219.0 |
| 2 | Exponential growth | 5562.0 | 6364.0 | 6859.0 | 8063.0 | 9019.0 | 11080.0 |
Tree-prior infection-count density plot
clock_sensitivity_C_fig = RUN_SENSITIVITY ?
plot_cumulative_cases("Skygrid (baseline)" => posterior_C_joint,
"Exponential growth" => posterior_C_exp_growth; scenarios = []) :
Markdown.md"_Tree-prior sensitivity analysis not shown in this build._"The outbreak age, the number of days from seeding to the cut-off, under the two tree priors.
Tree-prior outbreak-age table
clock_sensitivity_T_table = RUN_SENSITIVITY ?
streams_table("Skygrid (baseline)" => T_skygrid,
"Exponential growth" => T_exp_growth; digits = 0) :
Markdown.md"_Tree-prior sensitivity analysis not shown in this build._"| Row | Stream | Lower 90% | Lower 60% | Lower 30% | Upper 30% | Upper 60% | Upper 90% |
|---|---|---|---|---|---|---|---|
| String | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | Skygrid (baseline) | 127.0 | 129.0 | 132.0 | 139.0 | 143.0 | 153.0 |
| 2 | Exponential growth | 133.0 | 136.0 | 138.0 | 144.0 | 148.0 | 156.0 |
| Row | Stream | Lower 90% | Lower 60% | Lower 30% | Upper 30% | Upper 60% | Upper 90% |
|---|---|---|---|---|---|---|---|
| String | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | Skygrid (baseline) | 127.0 | 129.0 | 132.0 | 139.0 | 143.0 | 153.0 |
| 2 | Exponential growth | 133.0 | 136.0 | 138.0 | 144.0 | 148.0 | 156.0 |
Tree-prior outbreak-age density plot
clock_sensitivity_T_fig = RUN_SENSITIVITY ?
plot_density_overlay("Skygrid (baseline)" => T_skygrid,
"Exponential growth" => T_exp_growth;
xlabel = "Outbreak age (days before cut-off)",
title = "Posterior outbreak age by tree prior") :
Markdown.md"_Tree-prior sensitivity analysis not shown in this build._"Saving sensitivity results
The stream-comparison and frozen-fit tables and the per-stream reproduction number figure are written to the shared output/ directory (the main analysis writes the rest), so the combined release and summary dashboard pick up both pages' outputs.
Write sensitivity outputs
output_dir = get(ENV, "BVD_OUTPUT_DIR",
joinpath(pkgdir(BVDOutbreakSize), "output"))
mkpath(output_dir)
CSV.write(joinpath(output_dir, "cumulative_cases_by_stream.csv"),
streams_C_table)
CSV.write(joinpath(output_dir, "frozen_matched_cutoffs.csv"),
frozen_streams_table)
# The one-week-back validation forecast, in the same archive format as the
# release forecast, so the frozen "last week versus now" forecast is recorded
# as a release asset alongside the forecast it is scored against.
CSV.write(joinpath(output_dir, "forecast_validation.csv"),
forecast_archive([(7, validation_forecast)];
made_date = frozen_lastweek.o.cutoff, thin = 5))
# The per-stream reproduction-number figure for the summary dashboard; the
# main analysis writes the other three dashboard figures.
dashboard_dir = joinpath(
pkgdir(BVDOutbreakSize), "docs", "src", "summary_assets")
mkpath(dashboard_dir)
CairoMakie.save(joinpath(dashboard_dir, "rt_streams.png"), stream_rt_fig)