Skip to content

Sensitivity and comparison analyses

This page continues the main analysis from the one-week-ahead forecast onward: forecast validation, forecast scoring across releases, the reproduction number by release, the outbreak size each data stream implies alone, 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. 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 re-fits and the optional sensitivity re-fits below.

Load packages, data and fitted chains
julia
# 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"))
45

Forecast validation

How last week's forecast held up against the data since observed, using the frozen re-fit and one-week projection defined in 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. This widens the projected bed interval. Unlike the scores further down, the confirmed new-count rows here keep any retrospective harmonisation step the week contained. This means a week holding one reads high against a forecast that never predicted it.

Fit one week back and validate the one-week-ahead forecast
julia
# frozen_lastweek and frozen_lastweek_streams are computed in the setup
# block above.
# `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.
# The onset grid is the one the FROZEN fit saw, not the live one, so the
# validation forecast carries an `onset reports` row scored on the triangle
# the frozen fit was actually fitted to.
_val_onset_days = frozen_lastweek.o.onset_curve_history.onset_days
_val_grid_start = isempty(_val_onset_days) ? nothing :
                  minimum(_val_onset_days)
_val_grid_end = isnothing(_val_grid_start) ? nothing :
                max(maximum(frozen_lastweek.o.onset_curve_history.report_days),
    _val_grid_start)
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,
    grid_n = frozen_lastweek.o.n,
    onset_grid_start = _val_grid_start, onset_grid_end = _val_grid_end);

# 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
11×10 DataFrame
RowStreamQuantityObservedLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%Within 90% PI
StringStringFloat64Float64Float64Float64Float64Float64Float64String
1DRC reported casescumulative by T+71077.01184.01466.01843.02954.04136.07627.0no
2DRC reported casesnew this week0.0107.0389.0766.01877.03059.06550.0no
3DRC suspected deathscumulative by T+7246.0270.0349.0464.0795.01135.02136.0no
4DRC suspected deathsnew this week0.024.0103.0218.0549.0889.01890.0no
5DRC confirmed casescumulative by T+74945.04484.04652.04772.05007.05209.05633.0yes
6DRC confirmed casesnew this week651.0190.0358.0478.0713.0915.01339.0yes
7DRC confirmed deathscumulative by T+72325.0270.0349.0464.0795.01135.02077.0no
8DRC confirmed deathsnew this week365.00.00.00.00.00.0117.0no
9DRC recoveredcumulative by T+71040.0849.0869.0908.01031.01142.01546.0yes
10DRC recoverednew this week191.00.020.059.0182.0293.0697.0yes
11DRC isolation bedsoccupancy at T+7730.0627.0709.0765.0862.0918.0963.0yes

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). The 90% predictive interval is shaded, and the count observed by the current cut-off is 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 forecast from the same frozen cut-off is overlaid as a dotted density alongside the joint's histogram. Recovered has no individual fit and draws the joint alone.

Forecast-versus-observed plot
julia
# 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), with the individual (treatment-only) fit's own projection overlaid as a dotted density alongside the joint.

Bed forecast-versus-observed plot
julia
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
julia
# Current fit's draws of the new latent counts over the past week, the last
# seven days of each cumulative-trajectory deterministic.
function _now_new(chn, key)
    mat = chn[key]
    trajs = [collect(v) for v in vec(collect(mat))]
    return Float64[t[end] - t[max(1, length(t) - 7)] for t in trajs]
end
now_latent = (;
    infections_new = _now_new(chn_joint, :cumulative_infections),
    onsets_new = _now_new(chn_joint, :cumulative_onsets),
    deaths_latent_new = _now_new(chn_joint, :cumulative_expected_deaths))

validation_latent_fig = plot_forecast_vs_truth_latent(
    validation_forecast; now = now_latent);

Forecast scoring across releases

Every release's saved one- to four-week-ahead forecast is scored against the data observed since, against a persistence baseline and, where one exists, the stream's own individual fit 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. Exports therefore contributes no scored forecast, 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 contribute a row to the table below, each a reconstruction of an earlier model version rather than the current fit. The table below is therefore 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. This is the signature of a chain that failed to sample properly rather than a genuine forecast, so the scoring script flags and excludes it. The releases that carry the current model's own individual-stream forecasts are too recent for their targets to be observed yet. This 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. A ratio here should therefore be read as an early signal rather than a settled result.

The symptom-onset stream is scored on the new reported count each vintage adds rather than on its level, because every vintage rereads the whole figure. Its printed total therefore moves with the scan error as well as with late reporting. It appears only from the release that first carried it. Its intervals are dominated by that scan error rather than by epidemic uncertainty, so read its skill against the baseline rather than its coverage.

Load and summarise the cross-release forecast scores
julia
# 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)
86×16 DataFrame
Rowmade_datestreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
12026-06-07confirmed casesjoint4826.775.560.8233.65missingmissing765.6561.120.01.01.00.2
22026-06-10confirmed casesjoint4833.236.470.7583.93missingmissing666.38166.850.01.01.00.31
32026-07-01confirmed casesjoint415862.878.70.5832.91missingmissing15377.3485.470.00.01.00.66
42026-07-06confirmed casesjoint4421.281.40.2691.01missingmissing311.85109.430.00.51.00.49
52026-07-08confirmed casesjoint4390.981.30.2531.0missingmissing239.5151.470.00.251.00.52
62026-07-23confirmed casesconfirmed3219.981.00.1931.07missingmissing214.980.274.731.01.0-0.08
72026-07-23confirmed casesjoint3220.521.00.1570.871.00.81201.5318.160.831.01.00.09
82026-07-25confirmed casesconfirmed3252.732.110.212.13missingmissing251.361.30.071.01.00.02
92026-07-25confirmed casesjoint3145.561.210.1251.270.580.6144.481.080.01.01.00.04
102026-07-26confirmed casesconfirmed2146.773.30.2063.61missingmissing131.930.014.841.01.0-0.22
112026-07-26confirmed casesjoint2108.222.430.1272.230.740.62106.171.340.711.01.00.05
122026-07-27confirmed casesconfirmed2135.072.580.1612.23missingmissing134.260.740.081.01.00.02
132026-07-27confirmed casesjoint2114.162.180.1271.760.850.79109.854.320.01.01.00.12
142026-07-31confirmed casesconfirmed2189.023.980.2063.37missingmissing186.092.930.01.01.00.05
152026-07-31confirmed casesjoint2116.192.450.1372.250.610.67110.615.290.31.01.00.06
162026-08-01confirmed casesconfirmed2159.753.420.193.31missingmissing159.30.450.01.01.00.03
172026-08-01confirmed casesjoint2110.012.360.1232.140.690.65107.911.890.221.01.00.05
182026-08-02confirmed casesconfirmed191.772.590.1632.48missingmissing89.220.02.551.01.0-0.11
192026-08-02confirmed casesjoint178.022.20.1241.890.850.7670.887.140.01.01.00.23
202026-08-03confirmed casesconfirmed188.11.170.1641.06missingmissing87.140.00.961.01.0-0.06
212026-08-03confirmed casesjoint170.420.930.120.780.80.7369.361.060.01.01.00.08
222026-08-04confirmed casesconfirmed194.852.930.1673.08missingmissing94.70.00.151.01.0-0.02
232026-08-04confirmed casesjoint169.682.150.1162.150.730.768.481.20.01.01.00.07
242026-08-07confirmed casesconfirmed1103.831.560.1931.6missingmissing91.730.012.11.01.0-0.21
252026-08-07confirmed casesjoint173.821.110.1311.090.710.6867.80.06.021.01.0-0.18
262026-06-07confirmed deathsjoint4120.570.990.7251.09missingmissing117.80.02.761.01.0-0.11
272026-06-10confirmed deathsjoint4144.921.120.5981.09missingmissing140.543.760.621.01.00.02
282026-07-01confirmed deathsjoint417196.079.910.5491.21missingmissing17103.192.890.01.01.00.28
292026-07-06confirmed deathsjoint4197.20.890.8712.3missingmissing110.350.086.850.251.0-0.49
302026-07-08confirmed deathsjoint4177.590.790.7131.86missingmissing84.290.093.30.251.0-0.61
312026-07-23confirmed deathsconfirmed_deaths3427.714.690.3912.79missingmissing249.45178.260.00.331.00.53
322026-07-23confirmed deathsjoint3197.532.170.4933.520.461.26107.620.089.911.01.0-0.48
332026-07-25confirmed deathsconfirmed_deaths3150.092.450.2212.17missingmissing145.494.610.01.01.00.12
342026-07-25confirmed deathsjoint3190.393.110.5135.031.272.3271.50.0118.90.01.0-0.58
352026-07-26confirmed deathsconfirmed_deaths2105.514.90.2224.14missingmissing104.481.040.01.01.00.06
362026-07-26confirmed deathsjoint2144.076.690.539.871.372.3843.580.0100.490.01.0-0.68
372026-07-27confirmed deathsconfirmed_deaths2123.884.820.2263.32missingmissing110.5313.350.01.01.00.19
382026-07-27confirmed deathsjoint2133.975.210.5237.681.082.3151.310.082.660.01.0-0.62
392026-07-31confirmed deathsconfirmed_deaths2117.922.90.252.76missingmissing117.210.570.141.01.00.01
402026-07-31confirmed deathsjoint2162.864.010.5626.211.382.2553.220.0109.640.01.0-0.65
412026-08-01confirmed deathsconfirmed_deaths2103.761.910.2111.67missingmissing103.760.00.01.01.00.0
422026-08-01confirmed deathsjoint2195.823.60.6965.491.893.2943.730.0152.10.01.0-0.74
432026-08-02confirmed deathsconfirmed_deaths170.732.750.2522.62missingmissing69.50.01.231.01.0-0.08
442026-08-02confirmed deathsjoint196.073.740.4965.151.361.9733.230.062.840.01.0-0.66
452026-08-03confirmed deathsconfirmed_deaths160.781.130.2311.1missingmissing54.930.05.851.01.0-0.2
462026-08-03confirmed deathsjoint1109.992.040.6132.921.812.6534.890.075.10.01.0-0.59
472026-08-04confirmed deathsconfirmed_deaths168.664.090.275.07missingmissing60.320.08.341.01.0-0.24
482026-08-04confirmed deathsjoint1132.467.890.68712.891.932.5428.470.0103.990.01.0-0.74
492026-08-07confirmed deathsconfirmed_deaths184.532.390.3563.14missingmissing66.140.018.391.01.0-0.34
502026-08-07confirmed deathsjoint1192.365.431.0018.82.282.8123.40.0168.960.00.0-0.92
512026-07-01isolation bedsjoint490.191.490.1381.52missingmissing13.480.076.710.00.0-0.99
522026-07-06isolation bedsjoint442.870.630.0660.64missingmissing21.070.021.80.01.0-0.64
532026-07-08isolation bedsjoint454.751.440.0731.44missingmissing11.6743.080.00.01.00.8
542026-07-23isolation bedsjoint377.112.80.0972.661.250.9627.2149.90.00.01.00.6
552026-07-23isolation bedstreatment361.652.240.1012.76missingmissing51.160.010.491.01.0-0.27
562026-07-25isolation bedsjoint388.42.080.1132.011.811.4128.759.690.00.331.00.61
572026-07-25isolation bedstreatment348.781.150.081.42missingmissing48.520.010.251.01.0-0.02
582026-07-26isolation bedsjoint285.223.780.113.521.461.0729.6755.550.00.01.00.62
592026-07-26isolation bedstreatment258.452.590.1023.29missingmissing57.70.460.291.01.00.01
602026-07-27isolation bedsjoint276.073.790.0973.491.220.8833.4442.620.00.01.00.55
612026-07-27isolation bedstreatment262.43.110.113.95missingmissing61.760.00.641.01.0-0.07
622026-07-31isolation bedsjoint268.162.810.0842.580.890.6341.7726.40.00.51.00.46
632026-07-31isolation bedstreatment276.853.170.1334.08missingmissing72.50.04.351.01.0-0.16
642026-08-01isolation bedsjoint259.182.170.0751.890.770.5245.0214.160.01.01.00.33
652026-08-01isolation bedstreatment276.792.820.1433.62missingmissing76.690.00.11.01.0-0.02
662026-08-02isolation bedsjoint174.443.750.0953.430.990.738.3536.090.00.01.00.6
672026-08-02isolation bedstreatment175.183.780.1364.91missingmissing74.350.00.831.01.0-0.05
682026-08-03isolation bedsjoint154.952.770.072.570.730.5336.9218.030.01.01.00.41
692026-08-03isolation bedstreatment174.863.770.1334.87missingmissing74.860.00.01.01.00.0
702026-08-04isolation bedsjoint1142.952.230.2132.061.51.2533.51109.440.00.01.00.88
712026-08-04isolation bedstreatment195.261.490.171.65missingmissing90.624.640.01.01.00.12
722026-08-07isolation bedsjoint141.491.30.0561.30.480.3835.70.05.791.01.0-0.25
732026-08-07isolation bedstreatment187.192.720.1463.38missingmissing77.660.09.531.01.0-0.26
742026-08-02onset reportsjoint152.820.110.140.020.650.4943.980.08.841.01.0-0.29
752026-08-02onset reportsonsets181.880.180.2830.05missingmissing63.320.018.561.01.0-0.28
762026-08-03onset reportsjoint1175.691.430.7680.161.211.1547.9127.790.00.01.00.85
772026-08-03onset reportsonsets1145.591.180.6670.14missingmissing64.4281.170.00.01.00.64
782026-08-04onset reportsjoint1167.422.040.940.211.411.2652.38115.040.00.01.00.86
792026-08-04onset reportsonsets1118.931.450.7470.17missingmissing70.0248.910.00.01.00.51
802026-08-07onset reportsjoint1114.510.230.3380.050.750.4845.520.068.990.01.0-0.65
812026-08-07onset reportsonsets1153.640.310.7020.11missingmissing61.660.091.980.01.0-0.61
822026-07-01recoveredjoint410457.7153.440.5391.87missingmissing10409.148.570.01.01.00.29
832026-07-06recoveredjoint485.760.850.4070.92missingmissing68.390.017.371.01.0-0.33
842026-07-08recoveredjoint463.080.80.2921.07missingmissing44.510.018.571.01.0-0.38
852026-05-18reported casesjoint1231.140.481.870.3missingmissing61.150.0169.990.01.0-0.66
862026-05-18suspected deathsjoint141.590.390.5770.12missingmissing41.120.00.471.01.0-0.08

The headline pools every horizon and release into one row per stream and fit: the mean CRPS and its decomposition, coverage, bias, and the relative skill against the persistence baseline for every fit, on both the natural and the log scale. The joint row also carries relative skill against the stream's own individual fit where one exists. Column definitions are in forecast scoring against a persistence baseline.

11×15 DataFrame
Rowstreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
1confirmed casesconfirmed18169.881.90.191.98missingmissing165.830.723.331.01.0-0.05
2confirmed casesjoint381990.212.740.3452.090.750.691884.9105.00.290.761.00.26
3confirmed deathsconfirmed_deaths18162.243.340.2652.54missingmissing128.232.141.890.891.00.09
4confirmed deathsjoint381955.6116.40.6392.091.022.191875.410.1770.030.450.97-0.4
5isolation bedsjoint3071.31.780.0971.691.140.8526.5631.413.330.20.870.19
6isolation bedstreatment1867.372.270.1172.84missingmissing64.10.312.961.01.0-0.09
7onset reportsjoint4127.610.440.5460.11.020.9147.4460.7119.460.251.00.19
8onset reportsonsets4125.010.430.5990.11missingmissing64.8532.5227.640.251.00.06
9recoveredjoint123535.542.870.4131.23missingmissing3507.3316.1911.981.01.0-0.14
10reported casesjoint1231.140.481.870.3missingmissing61.150.0169.990.01.0-0.66
11suspected deathsjoint141.590.390.5770.12missingmissing41.120.00.471.01.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.

julia
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
29×16 DataFrame
Rowstreamhorizonfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31Int64String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
1confirmed cases7confirmed1097.92.170.1842.25missingmissing93.740.024.141.01.0-0.09
2confirmed cases7joint15114.182.770.2833.030.760.6993.6219.990.570.81.00.2
3confirmed cases14confirmed6217.512.870.1942.65missingmissing212.431.983.11.01.00.01
4confirmed cases14joint11273.693.650.3063.260.720.69217.0656.410.220.821.00.21
5confirmed cases21confirmed2386.921.10.2110.9missingmissing386.490.40.031.01.00.02
6confirmed cases21joint7792.032.910.4031.650.780.71620.07171.960.00.711.00.34
7confirmed cases28joint513072.025.220.5371.26missingmissing12698.8373.210.00.61.00.46
8confirmed deaths7confirmed_deaths1070.362.550.2442.47missingmissing63.683.273.411.01.0-0.03
9confirmed deaths7joint1594.723.990.7376.261.552.4834.330.3860.010.270.93-0.55
10confirmed deaths14confirmed_deaths6192.894.380.2643.38missingmissing165.4327.460.00.831.00.18
11confirmed deaths14joint11181.93.070.5943.381.072.189.153.1789.590.361.0-0.43
12confirmed deaths21confirmed_deaths2529.653.180.371.76missingmissing339.13190.520.00.51.00.41
13confirmed deaths21joint7400.271.910.551.110.61.43308.5815.5976.10.571.0-0.25
14confirmed deaths28joint513617.933.090.5660.64missingmissing13521.947.448.581.01.0-0.09
15isolation beds7joint1372.621.930.0971.820.990.7427.1834.6910.750.230.920.32
16isolation beds7treatment1074.472.530.1323.15missingmissing70.460.473.551.01.0-0.08
17isolation beds14joint973.772.090.0961.931.30.9529.2834.889.620.220.890.25
18isolation beds14treatment663.542.270.112.86missingmissing61.410.151.981.01.0-0.08
19isolation beds21joint563.951.380.0881.331.791.5123.2623.9416.750.20.80.02
20isolation beds21treatment243.391.210.0651.38missingmissing40.380.03.01.01.0-0.1
21isolation beds28joint370.361.280.1111.37missingmissing21.2419.1929.920.00.67-0.25
22onset reports7joint4127.610.440.5460.11.020.9147.4460.7119.460.251.00.19
23onset reports7onsets4125.010.430.5990.11missingmissing64.8532.5227.640.251.00.06
24recovered7joint326.962.130.3191.72missingmissing20.931.284.761.01.0-0.18
25recovered14joint389.72.140.3931.56missingmissing68.015.8115.871.01.0-0.23
26recovered21joint3297.163.470.4221.23missingmissing269.916.0511.21.01.0-0.09
27recovered28joint313728.272.350.5170.92missingmissing13670.541.6216.091.01.0-0.06
28reported cases7joint1231.140.481.870.3missingmissing61.150.0169.990.01.0-0.66
29suspected deaths7joint141.590.390.5770.12missingmissing41.120.00.471.01.0-0.08
Scores by release
86×16 DataFrame
Rowmade_datestreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
12026-06-07confirmed casesjoint4826.775.560.8233.65missingmissing765.6561.120.01.01.00.2
22026-06-10confirmed casesjoint4833.236.470.7583.93missingmissing666.38166.850.01.01.00.31
32026-07-01confirmed casesjoint415862.878.70.5832.91missingmissing15377.3485.470.00.01.00.66
42026-07-06confirmed casesjoint4421.281.40.2691.01missingmissing311.85109.430.00.51.00.49
52026-07-08confirmed casesjoint4390.981.30.2531.0missingmissing239.5151.470.00.251.00.52
62026-07-23confirmed casesconfirmed3219.981.00.1931.07missingmissing214.980.274.731.01.0-0.08
72026-07-23confirmed casesjoint3220.521.00.1570.871.00.81201.5318.160.831.01.00.09
82026-07-25confirmed casesconfirmed3252.732.110.212.13missingmissing251.361.30.071.01.00.02
92026-07-25confirmed casesjoint3145.561.210.1251.270.580.6144.481.080.01.01.00.04
102026-07-26confirmed casesconfirmed2146.773.30.2063.61missingmissing131.930.014.841.01.0-0.22
112026-07-26confirmed casesjoint2108.222.430.1272.230.740.62106.171.340.711.01.00.05
122026-07-27confirmed casesconfirmed2135.072.580.1612.23missingmissing134.260.740.081.01.00.02
132026-07-27confirmed casesjoint2114.162.180.1271.760.850.79109.854.320.01.01.00.12
142026-07-31confirmed casesconfirmed2189.023.980.2063.37missingmissing186.092.930.01.01.00.05
152026-07-31confirmed casesjoint2116.192.450.1372.250.610.67110.615.290.31.01.00.06
162026-08-01confirmed casesconfirmed2159.753.420.193.31missingmissing159.30.450.01.01.00.03
172026-08-01confirmed casesjoint2110.012.360.1232.140.690.65107.911.890.221.01.00.05
182026-08-02confirmed casesconfirmed191.772.590.1632.48missingmissing89.220.02.551.01.0-0.11
192026-08-02confirmed casesjoint178.022.20.1241.890.850.7670.887.140.01.01.00.23
202026-08-03confirmed casesconfirmed188.11.170.1641.06missingmissing87.140.00.961.01.0-0.06
212026-08-03confirmed casesjoint170.420.930.120.780.80.7369.361.060.01.01.00.08
222026-08-04confirmed casesconfirmed194.852.930.1673.08missingmissing94.70.00.151.01.0-0.02
232026-08-04confirmed casesjoint169.682.150.1162.150.730.768.481.20.01.01.00.07
242026-08-07confirmed casesconfirmed1103.831.560.1931.6missingmissing91.730.012.11.01.0-0.21
252026-08-07confirmed casesjoint173.821.110.1311.090.710.6867.80.06.021.01.0-0.18
262026-06-07confirmed deathsjoint4120.570.990.7251.09missingmissing117.80.02.761.01.0-0.11
272026-06-10confirmed deathsjoint4144.921.120.5981.09missingmissing140.543.760.621.01.00.02
282026-07-01confirmed deathsjoint417196.079.910.5491.21missingmissing17103.192.890.01.01.00.28
292026-07-06confirmed deathsjoint4197.20.890.8712.3missingmissing110.350.086.850.251.0-0.49
302026-07-08confirmed deathsjoint4177.590.790.7131.86missingmissing84.290.093.30.251.0-0.61
312026-07-23confirmed deathsconfirmed_deaths3427.714.690.3912.79missingmissing249.45178.260.00.331.00.53
322026-07-23confirmed deathsjoint3197.532.170.4933.520.461.26107.620.089.911.01.0-0.48
332026-07-25confirmed deathsconfirmed_deaths3150.092.450.2212.17missingmissing145.494.610.01.01.00.12
342026-07-25confirmed deathsjoint3190.393.110.5135.031.272.3271.50.0118.90.01.0-0.58
352026-07-26confirmed deathsconfirmed_deaths2105.514.90.2224.14missingmissing104.481.040.01.01.00.06
362026-07-26confirmed deathsjoint2144.076.690.539.871.372.3843.580.0100.490.01.0-0.68
372026-07-27confirmed deathsconfirmed_deaths2123.884.820.2263.32missingmissing110.5313.350.01.01.00.19
382026-07-27confirmed deathsjoint2133.975.210.5237.681.082.3151.310.082.660.01.0-0.62
392026-07-31confirmed deathsconfirmed_deaths2117.922.90.252.76missingmissing117.210.570.141.01.00.01
402026-07-31confirmed deathsjoint2162.864.010.5626.211.382.2553.220.0109.640.01.0-0.65
412026-08-01confirmed deathsconfirmed_deaths2103.761.910.2111.67missingmissing103.760.00.01.01.00.0
422026-08-01confirmed deathsjoint2195.823.60.6965.491.893.2943.730.0152.10.01.0-0.74
432026-08-02confirmed deathsconfirmed_deaths170.732.750.2522.62missingmissing69.50.01.231.01.0-0.08
442026-08-02confirmed deathsjoint196.073.740.4965.151.361.9733.230.062.840.01.0-0.66
452026-08-03confirmed deathsconfirmed_deaths160.781.130.2311.1missingmissing54.930.05.851.01.0-0.2
462026-08-03confirmed deathsjoint1109.992.040.6132.921.812.6534.890.075.10.01.0-0.59
472026-08-04confirmed deathsconfirmed_deaths168.664.090.275.07missingmissing60.320.08.341.01.0-0.24
482026-08-04confirmed deathsjoint1132.467.890.68712.891.932.5428.470.0103.990.01.0-0.74
492026-08-07confirmed deathsconfirmed_deaths184.532.390.3563.14missingmissing66.140.018.391.01.0-0.34
502026-08-07confirmed deathsjoint1192.365.431.0018.82.282.8123.40.0168.960.00.0-0.92
512026-07-01isolation bedsjoint490.191.490.1381.52missingmissing13.480.076.710.00.0-0.99
522026-07-06isolation bedsjoint442.870.630.0660.64missingmissing21.070.021.80.01.0-0.64
532026-07-08isolation bedsjoint454.751.440.0731.44missingmissing11.6743.080.00.01.00.8
542026-07-23isolation bedsjoint377.112.80.0972.661.250.9627.2149.90.00.01.00.6
552026-07-23isolation bedstreatment361.652.240.1012.76missingmissing51.160.010.491.01.0-0.27
562026-07-25isolation bedsjoint388.42.080.1132.011.811.4128.759.690.00.331.00.61
572026-07-25isolation bedstreatment348.781.150.081.42missingmissing48.520.010.251.01.0-0.02
582026-07-26isolation bedsjoint285.223.780.113.521.461.0729.6755.550.00.01.00.62
592026-07-26isolation bedstreatment258.452.590.1023.29missingmissing57.70.460.291.01.00.01
602026-07-27isolation bedsjoint276.073.790.0973.491.220.8833.4442.620.00.01.00.55
612026-07-27isolation bedstreatment262.43.110.113.95missingmissing61.760.00.641.01.0-0.07
622026-07-31isolation bedsjoint268.162.810.0842.580.890.6341.7726.40.00.51.00.46
632026-07-31isolation bedstreatment276.853.170.1334.08missingmissing72.50.04.351.01.0-0.16
642026-08-01isolation bedsjoint259.182.170.0751.890.770.5245.0214.160.01.01.00.33
652026-08-01isolation bedstreatment276.792.820.1433.62missingmissing76.690.00.11.01.0-0.02
662026-08-02isolation bedsjoint174.443.750.0953.430.990.738.3536.090.00.01.00.6
672026-08-02isolation bedstreatment175.183.780.1364.91missingmissing74.350.00.831.01.0-0.05
682026-08-03isolation bedsjoint154.952.770.072.570.730.5336.9218.030.01.01.00.41
692026-08-03isolation bedstreatment174.863.770.1334.87missingmissing74.860.00.01.01.00.0
702026-08-04isolation bedsjoint1142.952.230.2132.061.51.2533.51109.440.00.01.00.88
712026-08-04isolation bedstreatment195.261.490.171.65missingmissing90.624.640.01.01.00.12
722026-08-07isolation bedsjoint141.491.30.0561.30.480.3835.70.05.791.01.0-0.25
732026-08-07isolation bedstreatment187.192.720.1463.38missingmissing77.660.09.531.01.0-0.26
742026-08-02onset reportsjoint152.820.110.140.020.650.4943.980.08.841.01.0-0.29
752026-08-02onset reportsonsets181.880.180.2830.05missingmissing63.320.018.561.01.0-0.28
762026-08-03onset reportsjoint1175.691.430.7680.161.211.1547.9127.790.00.01.00.85
772026-08-03onset reportsonsets1145.591.180.6670.14missingmissing64.4281.170.00.01.00.64
782026-08-04onset reportsjoint1167.422.040.940.211.411.2652.38115.040.00.01.00.86
792026-08-04onset reportsonsets1118.931.450.7470.17missingmissing70.0248.910.00.01.00.51
802026-08-07onset reportsjoint1114.510.230.3380.050.750.4845.520.068.990.01.0-0.65
812026-08-07onset reportsonsets1153.640.310.7020.11missingmissing61.660.091.980.01.0-0.61
822026-07-01recoveredjoint410457.7153.440.5391.87missingmissing10409.148.570.01.01.00.29
832026-07-06recoveredjoint485.760.850.4070.92missingmissing68.390.017.371.01.0-0.33
842026-07-08recoveredjoint463.080.80.2921.07missingmissing44.510.018.571.01.0-0.38
852026-05-18reported casesjoint1231.140.481.870.3missingmissing61.150.0169.990.01.0-0.66
862026-05-18suspected deathsjoint141.590.390.5770.12missingmissing41.120.00.471.01.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
julia
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), is scored the same way as the cross-release forecasts above, 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
julia
frozen_scores_df = _release_data("forecast_scores_frozen.csv",
    (; release = String, made_date = Date, stream = String, horizon = Int,
        target_date = Date, fit = String, crps = Float64,
        log_crps = Float64, dispersion = Float64, overprediction = Float64,
        underprediction = Float64, coverage_50 = Float64,
        coverage_90 = Float64,
        bias = Float64, n_samples = Int,
        log_rel_to_baseline = Float64))
frozen_overlay_df = _release_data("forecast_overlay_frozen.csv",
    (; release = String, made_date = Date, stream = String, horizon = Int,
        target_date = Date, fit = String, observed = Float64,
        median = Float64, lo30 = Float64, hi30 = Float64, lo60 = Float64,
        hi60 = Float64, lo90 = Float64, hi90 = Float64))
# 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)
59×13 DataFrame
Rowmade_datestreamncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
12026-05-20confirmed cases44277.410.760.3720.25225.4951.920.00.751.00.42
22026-05-23confirmed cases44184.40.450.2190.16156.127.590.711.01.00.09
32026-05-27confirmed cases44299.870.580.8690.5751.60.0248.270.00.75-0.83
42026-06-08confirmed cases44628.94.430.4352.16339.44289.460.00.361.00.6
52026-07-16confirmed cases4294.680.480.1680.31283.4610.980.241.01.00.07
62026-07-18confirmed cases4361.260.830.1850.55333.8527.410.01.01.00.15
72026-07-19confirmed cases3299.441.290.1930.89244.8954.550.01.01.00.28
82026-07-20confirmed cases3246.30.890.1670.64211.2235.080.01.01.00.2
92026-07-24confirmed cases3280.441.590.1741.18224.7655.680.01.01.00.31
102026-07-25confirmed cases3290.572.50.1881.96228.562.070.01.01.00.33
112026-07-26confirmed cases2146.913.090.1462.42130.216.710.01.01.00.24
122026-07-27confirmed cases2155.942.720.162.03140.2415.70.01.01.00.26
132026-07-28confirmed cases2145.081.750.1391.47132.7812.30.01.01.00.19
142026-07-31confirmed cases2171.133.560.1672.73142.5328.60.01.01.00.31
152026-08-04confirmed cases180.422.450.1272.3974.615.810.01.01.00.18
162026-05-20confirmed deaths4434.750.350.3990.1921.150.013.60.821.0-0.34
172026-05-23confirmed deaths4442.180.350.4920.2319.950.022.230.251.0-0.58
182026-05-27confirmed deaths44123.650.841.7960.925.60.0118.050.00.0-0.97
192026-06-08confirmed deaths4477.990.580.2330.3676.141.060.791.01.0-0.05
202026-07-16confirmed deaths4337.01.131.6183.3583.740.0253.250.01.0-0.75
212026-07-18confirmed deaths4284.841.341.5165.1111.230.0173.610.01.0-0.66
222026-07-19confirmed deaths3230.82.181.6488.8983.420.0147.380.01.0-0.66
232026-07-20confirmed deaths3264.862.031.9878.5773.140.0191.720.01.0-0.68
242026-07-24confirmed deaths3255.363.242.17218.0680.450.0174.910.331.0-0.68
252026-07-25confirmed deaths3257.213.942.22120.3285.990.0171.220.01.0-0.64
262026-07-26confirmed deaths2255.7210.93.24754.6231.750.0223.960.01.0-0.78
272026-07-27confirmed deaths2248.368.93.10641.5537.00.0211.360.01.0-0.71
282026-07-28confirmed deaths2278.35.762.97928.4238.30.0239.990.01.0-0.76
292026-07-31confirmed deaths2256.786.672.86433.5951.020.0205.750.01.0-0.7
302026-08-04confirmed deaths1287.6716.374.99488.291.530.0286.140.00.0-0.93
312026-06-08isolation beds4474.590.440.1520.3939.122.333.170.51.0-0.21
322026-07-16isolation beds473.222.560.0962.4520.0153.220.00.01.00.6
332026-07-18isolation beds477.782.540.1032.3918.7159.070.00.01.00.61
342026-07-19isolation beds3104.53.970.1353.7219.0485.460.00.01.00.68
352026-07-20isolation beds389.563.430.1143.2216.4673.10.00.01.00.67
362026-07-24isolation beds384.723.20.1062.998.4476.280.00.01.00.78
372026-07-25isolation beds3123.383.10.1572.9511.61111.780.00.00.670.8
382026-07-26isolation beds2123.615.850.1575.3615.36108.250.00.00.50.81
392026-07-27isolation beds295.613.860.1213.5527.8967.720.00.01.00.66
402026-07-28isolation beds2198.852.770.2682.4730.38168.470.00.00.50.9
412026-07-31isolation beds2135.335.680.1595.0535.12100.210.00.00.50.74
422026-08-04isolation beds1169.732.830.2512.631.9137.830.00.00.00.95
432026-07-26onset reports294.330.20.1930.0365.2229.110.00.51.00.43
442026-07-27onset reports2124.720.270.2050.0356.6667.740.320.51.00.34
452026-07-28onset reports2148.650.310.2360.0461.3986.460.80.51.00.36
462026-07-31onset reports2100.670.170.1580.0375.7224.960.01.01.00.33
472026-08-04onset reports1151.091.840.8820.243.53107.560.00.01.00.8
482026-06-08recovered44114.321.020.4420.3192.022.320.01.01.00.37
492026-07-16recovered4139.341.090.4691.08135.50.03.841.01.0-0.1
502026-07-18recovered4149.612.390.4552.4146.942.670.01.01.00.1
512026-07-19recovered3119.042.840.4672.79117.971.070.01.01.00.08
522026-07-20recovered3114.752.820.4062.63114.340.410.01.01.00.05
532026-07-24recovered3136.273.110.42.6131.195.080.01.01.00.16
542026-07-25recovered3124.192.650.3652.25120.323.870.01.01.00.12
552026-07-26recovered269.223.390.3643.0568.70.00.521.01.0-0.06
562026-07-27recovered299.213.360.4462.7698.130.01.081.01.0-0.08
572026-07-28recovered282.951.140.3480.8681.820.470.661.01.0-0.0
582026-07-31recovered2111.842.370.3691.54108.043.80.01.01.00.12
592026-08-04recovered145.372.350.4123.4841.70.03.671.01.0-0.16

The headline frozen table pools one row per stream across cut-offs and horizons, scored against the persistence baseline on both scales, with the CRPS decomposition, coverage and bias columns described above. There is no model column, since only one model is scored.

5×12 DataFrame
Rowstreamncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
1confirmed cases205511.91.50.2890.29332.73179.150.020.731.00.36
2confirmed deaths205231.421.871.6831.1389.330.34141.750.320.98-0.49
3isolation beds73107.00.930.1520.624.6269.113.290.010.860.45
4onset reports9120.870.260.2740.0562.3958.230.250.561.00.41
5recovered73134.071.490.4260.45124.918.520.641.01.00.1

The same relative skill against the baseline, by horizon, for the frozen cut-offs.

julia
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
18×13 DataFrame
Rowstreamhorizonncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31Int64Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
1confirmed cases75593.321.360.1460.3483.1410.10.091.01.00.19
2confirmed cases1454226.261.020.1780.21191.4334.830.01.01.00.26
3confirmed cases2150574.931.290.3490.28369.32205.610.00.61.00.45
4confirmed cases28461279.211.830.5260.32757.28521.930.00.221.00.59
5confirmed deaths755210.339.463.5935.369.560.0200.760.00.91-0.84
6confirmed deaths1454254.733.871.7071.4170.930.0183.80.071.0-0.63
7confirmed deaths2150229.711.40.7360.39118.40.0111.310.51.0-0.33
8confirmed deaths2846231.130.860.4010.17174.71.5354.90.781.0-0.08
9isolation beds722111.852.840.1441.517.2894.570.00.00.550.83
10isolation beds1421113.122.450.1481.3221.7391.390.00.051.00.68
11isolation beds211787.430.490.130.3326.2449.411.80.01.00.27
12isolation beds2813114.510.420.20.3539.5715.7359.210.01.0-0.32
13onset reports7580.920.270.3120.0646.6433.820.450.61.00.3
14onset reports144170.810.260.2270.0482.0788.740.00.51.00.56
15recovered72249.13.290.4022.447.120.571.421.01.0-0.0
16recovered1421115.642.120.4050.53112.193.040.411.01.00.07
17recovered2117169.291.350.4330.33164.784.320.191.01.00.1
18recovered2813261.571.150.490.24224.9836.290.31.01.00.3
Scores by frozen cut-off
59×13 DataFrame
Rowmade_datestreamncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
12026-05-20confirmed cases44277.410.760.3720.25225.4951.920.00.751.00.42
22026-05-23confirmed cases44184.40.450.2190.16156.127.590.711.01.00.09
32026-05-27confirmed cases44299.870.580.8690.5751.60.0248.270.00.75-0.83
42026-06-08confirmed cases44628.94.430.4352.16339.44289.460.00.361.00.6
52026-07-16confirmed cases4294.680.480.1680.31283.4610.980.241.01.00.07
62026-07-18confirmed cases4361.260.830.1850.55333.8527.410.01.01.00.15
72026-07-19confirmed cases3299.441.290.1930.89244.8954.550.01.01.00.28
82026-07-20confirmed cases3246.30.890.1670.64211.2235.080.01.01.00.2
92026-07-24confirmed cases3280.441.590.1741.18224.7655.680.01.01.00.31
102026-07-25confirmed cases3290.572.50.1881.96228.562.070.01.01.00.33
112026-07-26confirmed cases2146.913.090.1462.42130.216.710.01.01.00.24
122026-07-27confirmed cases2155.942.720.162.03140.2415.70.01.01.00.26
132026-07-28confirmed cases2145.081.750.1391.47132.7812.30.01.01.00.19
142026-07-31confirmed cases2171.133.560.1672.73142.5328.60.01.01.00.31
152026-08-04confirmed cases180.422.450.1272.3974.615.810.01.01.00.18
162026-05-20confirmed deaths4434.750.350.3990.1921.150.013.60.821.0-0.34
172026-05-23confirmed deaths4442.180.350.4920.2319.950.022.230.251.0-0.58
182026-05-27confirmed deaths44123.650.841.7960.925.60.0118.050.00.0-0.97
192026-06-08confirmed deaths4477.990.580.2330.3676.141.060.791.01.0-0.05
202026-07-16confirmed deaths4337.01.131.6183.3583.740.0253.250.01.0-0.75
212026-07-18confirmed deaths4284.841.341.5165.1111.230.0173.610.01.0-0.66
222026-07-19confirmed deaths3230.82.181.6488.8983.420.0147.380.01.0-0.66
232026-07-20confirmed deaths3264.862.031.9878.5773.140.0191.720.01.0-0.68
242026-07-24confirmed deaths3255.363.242.17218.0680.450.0174.910.331.0-0.68
252026-07-25confirmed deaths3257.213.942.22120.3285.990.0171.220.01.0-0.64
262026-07-26confirmed deaths2255.7210.93.24754.6231.750.0223.960.01.0-0.78
272026-07-27confirmed deaths2248.368.93.10641.5537.00.0211.360.01.0-0.71
282026-07-28confirmed deaths2278.35.762.97928.4238.30.0239.990.01.0-0.76
292026-07-31confirmed deaths2256.786.672.86433.5951.020.0205.750.01.0-0.7
302026-08-04confirmed deaths1287.6716.374.99488.291.530.0286.140.00.0-0.93
312026-06-08isolation beds4474.590.440.1520.3939.122.333.170.51.0-0.21
322026-07-16isolation beds473.222.560.0962.4520.0153.220.00.01.00.6
332026-07-18isolation beds477.782.540.1032.3918.7159.070.00.01.00.61
342026-07-19isolation beds3104.53.970.1353.7219.0485.460.00.01.00.68
352026-07-20isolation beds389.563.430.1143.2216.4673.10.00.01.00.67
362026-07-24isolation beds384.723.20.1062.998.4476.280.00.01.00.78
372026-07-25isolation beds3123.383.10.1572.9511.61111.780.00.00.670.8
382026-07-26isolation beds2123.615.850.1575.3615.36108.250.00.00.50.81
392026-07-27isolation beds295.613.860.1213.5527.8967.720.00.01.00.66
402026-07-28isolation beds2198.852.770.2682.4730.38168.470.00.00.50.9
412026-07-31isolation beds2135.335.680.1595.0535.12100.210.00.00.50.74
422026-08-04isolation beds1169.732.830.2512.631.9137.830.00.00.00.95
432026-07-26onset reports294.330.20.1930.0365.2229.110.00.51.00.43
442026-07-27onset reports2124.720.270.2050.0356.6667.740.320.51.00.34
452026-07-28onset reports2148.650.310.2360.0461.3986.460.80.51.00.36
462026-07-31onset reports2100.670.170.1580.0375.7224.960.01.01.00.33
472026-08-04onset reports1151.091.840.8820.243.53107.560.00.01.00.8
482026-06-08recovered44114.321.020.4420.3192.022.320.01.01.00.37
492026-07-16recovered4139.341.090.4691.08135.50.03.841.01.0-0.1
502026-07-18recovered4149.612.390.4552.4146.942.670.01.01.00.1
512026-07-19recovered3119.042.840.4672.79117.971.070.01.01.00.08
522026-07-20recovered3114.752.820.4062.63114.340.410.01.01.00.05
532026-07-24recovered3136.273.110.42.6131.195.080.01.01.00.16
542026-07-25recovered3124.192.650.3652.25120.323.870.01.01.00.12
552026-07-26recovered269.223.390.3643.0568.70.00.521.01.0-0.06
562026-07-27recovered299.213.360.4462.7698.130.01.081.01.0-0.08
572026-07-28recovered282.951.140.3480.8681.820.470.661.01.0-0.0
582026-07-31recovered2111.842.370.3691.54108.043.80.01.01.00.12
592026-08-04recovered145.372.350.4123.4841.70.03.671.01.0-0.16

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
julia
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.

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

Individual fits against the baseline

This section repeats the cross-release forecast scoring from Forecast scoring across releases above, filtered to each stream's own individual fit rather than the joint, against the same persistence baseline. Recovered has no individual fit, so it does not appear here. These are the individual-fit rows of the cross-release scores above, not a separate computation. As in Forecast scoring across releases above, every table and figure in this section is currently empty for the same reason. It will populate once those targets resolve.

Individual-fit rows of the cross-release scores
julia
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", :]
34×16 DataFrame
Rowmade_datestreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
12026-07-23confirmed casesconfirmed3219.981.00.1931.07missingmissing214.980.274.731.01.0-0.08
22026-07-25confirmed casesconfirmed3252.732.110.212.13missingmissing251.361.30.071.01.00.02
32026-07-26confirmed casesconfirmed2146.773.30.2063.61missingmissing131.930.014.841.01.0-0.22
42026-07-27confirmed casesconfirmed2135.072.580.1612.23missingmissing134.260.740.081.01.00.02
52026-07-31confirmed casesconfirmed2189.023.980.2063.37missingmissing186.092.930.01.01.00.05
62026-08-01confirmed casesconfirmed2159.753.420.193.31missingmissing159.30.450.01.01.00.03
72026-08-02confirmed casesconfirmed191.772.590.1632.48missingmissing89.220.02.551.01.0-0.11
82026-08-03confirmed casesconfirmed188.11.170.1641.06missingmissing87.140.00.961.01.0-0.06
92026-08-04confirmed casesconfirmed194.852.930.1673.08missingmissing94.70.00.151.01.0-0.02
102026-08-07confirmed casesconfirmed1103.831.560.1931.6missingmissing91.730.012.11.01.0-0.21
112026-07-23confirmed deathsconfirmed_deaths3427.714.690.3912.79missingmissing249.45178.260.00.331.00.53
122026-07-25confirmed deathsconfirmed_deaths3150.092.450.2212.17missingmissing145.494.610.01.01.00.12
132026-07-26confirmed deathsconfirmed_deaths2105.514.90.2224.14missingmissing104.481.040.01.01.00.06
142026-07-27confirmed deathsconfirmed_deaths2123.884.820.2263.32missingmissing110.5313.350.01.01.00.19
152026-07-31confirmed deathsconfirmed_deaths2117.922.90.252.76missingmissing117.210.570.141.01.00.01
162026-08-01confirmed deathsconfirmed_deaths2103.761.910.2111.67missingmissing103.760.00.01.01.00.0
172026-08-02confirmed deathsconfirmed_deaths170.732.750.2522.62missingmissing69.50.01.231.01.0-0.08
182026-08-03confirmed deathsconfirmed_deaths160.781.130.2311.1missingmissing54.930.05.851.01.0-0.2
192026-08-04confirmed deathsconfirmed_deaths168.664.090.275.07missingmissing60.320.08.341.01.0-0.24
202026-08-07confirmed deathsconfirmed_deaths184.532.390.3563.14missingmissing66.140.018.391.01.0-0.34
212026-07-23isolation bedstreatment361.652.240.1012.76missingmissing51.160.010.491.01.0-0.27
222026-07-25isolation bedstreatment348.781.150.081.42missingmissing48.520.010.251.01.0-0.02
232026-07-26isolation bedstreatment258.452.590.1023.29missingmissing57.70.460.291.01.00.01
242026-07-27isolation bedstreatment262.43.110.113.95missingmissing61.760.00.641.01.0-0.07
252026-07-31isolation bedstreatment276.853.170.1334.08missingmissing72.50.04.351.01.0-0.16
262026-08-01isolation bedstreatment276.792.820.1433.62missingmissing76.690.00.11.01.0-0.02
272026-08-02isolation bedstreatment175.183.780.1364.91missingmissing74.350.00.831.01.0-0.05
282026-08-03isolation bedstreatment174.863.770.1334.87missingmissing74.860.00.01.01.00.0
292026-08-04isolation bedstreatment195.261.490.171.65missingmissing90.624.640.01.01.00.12
302026-08-07isolation bedstreatment187.192.720.1463.38missingmissing77.660.09.531.01.0-0.26
312026-08-02onset reportsonsets181.880.180.2830.05missingmissing63.320.018.561.01.0-0.28
322026-08-03onset reportsonsets1145.591.180.6670.14missingmissing64.4281.170.00.01.00.64
332026-08-04onset reportsonsets1118.931.450.7470.17missingmissing70.0248.910.00.01.00.51
342026-08-07onset reportsonsets1153.640.310.7020.11missingmissing61.660.091.980.01.0-0.61
4×15 DataFrame
Rowstreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
1confirmed casesconfirmed18169.881.90.191.98missingmissing165.830.723.331.01.0-0.05
2confirmed deathsconfirmed_deaths18162.243.340.2652.54missingmissing128.232.141.890.891.00.09
3isolation bedstreatment1867.372.270.1172.84missingmissing64.10.312.961.01.0-0.09
4onset reportsonsets4125.010.430.5990.11missingmissing64.8532.5227.640.251.00.06

The same relative skill against the baseline, by horizon, one panel per stream (dataset), for each stream's own individual fit.

julia
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
10×16 DataFrame
Rowstreamhorizonfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31Int64String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
1confirmed cases7confirmed1097.92.170.1842.25missingmissing93.740.024.141.01.0-0.09
2confirmed cases14confirmed6217.512.870.1942.65missingmissing212.431.983.11.01.00.01
3confirmed cases21confirmed2386.921.10.2110.9missingmissing386.490.40.031.01.00.02
4confirmed deaths7confirmed_deaths1070.362.550.2442.47missingmissing63.683.273.411.01.0-0.03
5confirmed deaths14confirmed_deaths6192.894.380.2643.38missingmissing165.4327.460.00.831.00.18
6confirmed deaths21confirmed_deaths2529.653.180.371.76missingmissing339.13190.520.00.51.00.41
7isolation beds7treatment1074.472.530.1323.15missingmissing70.460.473.551.01.0-0.08
8isolation beds14treatment663.542.270.112.86missingmissing61.410.151.981.01.0-0.08
9isolation beds21treatment243.391.210.0651.38missingmissing40.380.03.01.01.0-0.1
10onset reports7onsets4125.010.430.5990.11missingmissing64.8532.5227.640.251.00.06
Scores by release
34×16 DataFrame
Rowmade_datestreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
12026-07-23confirmed casesconfirmed3219.981.00.1931.07missingmissing214.980.274.731.01.0-0.08
22026-07-25confirmed casesconfirmed3252.732.110.212.13missingmissing251.361.30.071.01.00.02
32026-07-26confirmed casesconfirmed2146.773.30.2063.61missingmissing131.930.014.841.01.0-0.22
42026-07-27confirmed casesconfirmed2135.072.580.1612.23missingmissing134.260.740.081.01.00.02
52026-07-31confirmed casesconfirmed2189.023.980.2063.37missingmissing186.092.930.01.01.00.05
62026-08-01confirmed casesconfirmed2159.753.420.193.31missingmissing159.30.450.01.01.00.03
72026-08-02confirmed casesconfirmed191.772.590.1632.48missingmissing89.220.02.551.01.0-0.11
82026-08-03confirmed casesconfirmed188.11.170.1641.06missingmissing87.140.00.961.01.0-0.06
92026-08-04confirmed casesconfirmed194.852.930.1673.08missingmissing94.70.00.151.01.0-0.02
102026-08-07confirmed casesconfirmed1103.831.560.1931.6missingmissing91.730.012.11.01.0-0.21
112026-07-23confirmed deathsconfirmed_deaths3427.714.690.3912.79missingmissing249.45178.260.00.331.00.53
122026-07-25confirmed deathsconfirmed_deaths3150.092.450.2212.17missingmissing145.494.610.01.01.00.12
132026-07-26confirmed deathsconfirmed_deaths2105.514.90.2224.14missingmissing104.481.040.01.01.00.06
142026-07-27confirmed deathsconfirmed_deaths2123.884.820.2263.32missingmissing110.5313.350.01.01.00.19
152026-07-31confirmed deathsconfirmed_deaths2117.922.90.252.76missingmissing117.210.570.141.01.00.01
162026-08-01confirmed deathsconfirmed_deaths2103.761.910.2111.67missingmissing103.760.00.01.01.00.0
172026-08-02confirmed deathsconfirmed_deaths170.732.750.2522.62missingmissing69.50.01.231.01.0-0.08
182026-08-03confirmed deathsconfirmed_deaths160.781.130.2311.1missingmissing54.930.05.851.01.0-0.2
192026-08-04confirmed deathsconfirmed_deaths168.664.090.275.07missingmissing60.320.08.341.01.0-0.24
202026-08-07confirmed deathsconfirmed_deaths184.532.390.3563.14missingmissing66.140.018.391.01.0-0.34
212026-07-23isolation bedstreatment361.652.240.1012.76missingmissing51.160.010.491.01.0-0.27
222026-07-25isolation bedstreatment348.781.150.081.42missingmissing48.520.010.251.01.0-0.02
232026-07-26isolation bedstreatment258.452.590.1023.29missingmissing57.70.460.291.01.00.01
242026-07-27isolation bedstreatment262.43.110.113.95missingmissing61.760.00.641.01.0-0.07
252026-07-31isolation bedstreatment276.853.170.1334.08missingmissing72.50.04.351.01.0-0.16
262026-08-01isolation bedstreatment276.792.820.1433.62missingmissing76.690.00.11.01.0-0.02
272026-08-02isolation bedstreatment175.183.780.1364.91missingmissing74.350.00.831.01.0-0.05
282026-08-03isolation bedstreatment174.863.770.1334.87missingmissing74.860.00.01.01.00.0
292026-08-04isolation bedstreatment195.261.490.171.65missingmissing90.624.640.01.01.00.12
302026-08-07isolation bedstreatment187.192.720.1463.38missingmissing77.660.09.531.01.0-0.26
312026-08-02onset reportsonsets181.880.180.2830.05missingmissing63.320.018.561.01.0-0.28
322026-08-03onset reportsonsets1145.591.180.6670.14missingmissing64.4281.170.00.01.00.64
332026-08-04onset reportsonsets1118.931.450.7470.17missingmissing70.0248.910.00.01.00.51
342026-08-07onset reportsonsets1153.640.310.7020.11missingmissing61.660.091.980.01.0-0.61

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 alone and what the joint adds.

Per-stream infection-count table
julia
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,
    "onsets (DRC)" => posterior_C_onsets,
    "joint" => posterior_C_joint);
7×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1exports3184.010943.026440.0130691.0375903.03.11655e6
2deaths (DRC)20067.029181.037070.056801.073918.0126298.0
3cases (DRC)24712.027690.030917.040375.051525.098515.0
4confirmed (DRC)35273.043573.050179.065704.084270.0142550.0
5isolation (DRC)8336.09824.010949.014264.017322.030398.0
6onsets (DRC)17014.026130.035013.059907.085155.0137207.0
7joint7983.09241.010172.012058.013333.016383.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
julia
# 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),
        (; label = "onsets (DRC)", trajs = _cuminf(chn_onsets),
            last_day = _last_day(obs.onset_curve_history.report_days),
            colour = :mediumpurple)];
    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
julia
# 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,
    "onsets (DRC)" => posterior_C_onsets,
    "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. 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
julia
# 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
julia
# 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),
        (; label = "onsets (DRC)", chn = chn_onsets, rt_start = 1,
            rt_walk_start = 1, colour = :mediumpurple)];
    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 is drawn as a discrete estimate, a median with nested 30/60/90% interval bars. The current fit's daily over its established window is drawn as the continuous band, and   is marked.

Reproduction number per release with the current-fit band
julia
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, and   is marked. Each release's cut-off value is a median with nested 30/60/90% interval bars. A dataset the report also fits on its own carries that fit's current-model band behind its points, built as in the overview above. Confirmed deaths carries no band, so its panel shows release points alone. Only the most recent releases published these per-dataset estimates, so every panel spans a much shorter window than the overview above rather than a different history.

Reproduction number per release by fit
julia
# 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", "onsets", "exports"]
_fit_labels = Dict("joint" => "joint", "cases" => "cases (DRC)",
    "deaths" => "deaths (DRC)", "confirmed" => "confirmed (DRC)",
    "confirmed_deaths" => "confirmed deaths (DRC)",
    "treatment" => "isolation (DRC)", "onsets" => "onsets (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),
    "onsets" => (; chn = chn_onsets, 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 estimated at each release, the initial-transmission counterpart of the reproduction number above, before the time-varying decline. Released estimates are blue and the current model frozen at earlier cut-offs is red, each a median with nested 30/60/90% interval bars. The current fit sits behind both as a flat band, and   is marked. Releases only began publishing this quantity recently, so the short blue history reflects that rather than any failed release. The frozen series carries the comparison meanwhile.

Basic reproduction number per release with frozen re-fits and the current-fit band
julia
# 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, and   is marked. Each release is a median with nested 30/60/90% interval bars. Every fit the report runs on its own also carries a current-model reference band. Panels fill in from the first release that publishes this quantity per dataset, so a fit with nothing saved yet is left out rather than drawn empty.

Basic reproduction number per release by fit
julia
# 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. The 20 May update also corrected the 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 on the same dates, not 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. Each scenario sits beside our estimate for the date it was made: 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.

McCabe scenarios with uncertainty against our estimates
julia
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)
julia
# 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 and anchored on the 598 confirmed cases reported by 8 June. It is 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. It therefore sits below them: 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. We roll its confirmed-case stream forward to the dates Chamla report, using 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
julia
# 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
julia
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)
julia
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;
2×4 DataFrame
RowDateChamla central (90% PI)Our projection (90% CrI)Observed confirmed
StringStringStringString
110 June648 (470–812)680 (618–825)676
224 June990 (709–1293)1684 (868–4131)4945 (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.

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
julia
# 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. This delay 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
julia
# 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])) : nothing
1000-element Vector{Float64}:
 10632.537206424577
 11283.607026976368
 11624.93993988455
 10829.293391015963
 12267.495308879432
  8890.281119643525
 12813.026935068274
 10826.548809773532
  6828.659822583069
  7952.333528985428
 10680.810642709106
 11652.367362672732
 13607.803761135647
 13029.233854630284
 14542.476324907155
 14123.691028190924
 10896.61303922698
 11207.137483097056
 11412.640213474711
  9251.299669325388
 11714.381611850406
 13393.943349278177
 13517.649138686867
 10046.862580092937
 12874.139971998518
 12587.001986383784
  9885.239860165055
 12981.993614672736
 10051.519599617845
 11151.170139318561
 10422.070692351026
 10413.929782808475
  8199.656278014085
  8356.99843446822
  7574.741381002474
  8726.34535009154
  8714.17366309482
  8637.33245506873
 12910.218529644864
 14298.136400751911
  8115.048371640211
  8646.928786817412
 10676.974748447956
  8963.425700993817
  8403.726802475961
  8730.79824053098
  8696.330271693532
  6966.065041912457
  9118.200099971013
  7892.555544922882
 13418.619376045588
 12072.898881056299
 10769.447096202945
 10099.209616792907
 12656.544911596093
 11187.788243620613
  9566.519029213094
  8361.597419733462
  8206.169646419286
  8660.57343355256
  9111.102655808232
  8956.332036147685
  9092.567973943755
 10866.399910821372
 15240.468687345538
 11540.560278438137
  9568.142788504118
 13364.179711319834
 10536.658986208768
 11861.70520191364
  7271.953939288677
  8831.800012668926
  8624.694891837975
  8381.728427635571
 11768.105652862943
  9229.338244745106
 10126.342439097427
  8493.577538822412
 10124.969228167478
 10410.153943815652
 13752.900927178358
 13079.082033854336
 11976.276019228137
 11266.317360108715
 13604.234964934582
 12009.125837038944
  7947.559132193963
  7433.131384673237
 14941.989422319582
  9313.695265961715
  8874.46840839478
  9916.443172805486
 12195.166128814313
  7507.3893485317885
  8378.819478939182
  7915.79153326147
  8923.70904033543
 16103.272663837168
 10936.814156006372
  8117.25242558596
  8357.328521926207
  9198.542714006293
 10159.122851706037
  8539.438170002051
  8224.295290584161
  8441.062854059668
 12076.521087491223
 10513.33561915695
  7817.714172405614
  8460.59139610297
  8886.895669259879
  8883.478292595675
  9366.950088854514
  9208.038113412396
 15598.755521914138
 11004.474665877748
 10080.833863547654
 10204.033368616852
  7466.616413974688
  8111.312596718938
  8849.935982723355
  8726.438404874752
  8869.26606802902
  6863.431468612496
  7673.771902948694
  9481.55867149255
  8908.71357626398
 10157.371417627055
  9572.892351015402
 16451.95073525203
 11150.697069714592
 14876.128768152768
  9640.156416786587
 14750.321747101709
  7658.008297343092
 12526.957318037721
 11737.195764610158
 12473.963447459713
 15217.260372775345
 15191.570441419939
  8552.250298151303
 10705.733203634656
  8964.925078540859
  9119.537487474841
 11152.20372597219
  9171.10298992843
  7644.26783511409
  7976.760057385417
  8365.707754584742
 11802.270628511911
 12333.826154933959
  8600.266810743577
 10007.401089522838
 10952.100464815983
  9802.742244422505
  8414.13288857385
  8868.188032524617
  9506.970482123828
  8821.174856965252
  8373.655536677448
  7753.400025416013
  8150.65363390765
 10297.146198441083
  9417.243868546517
  8925.354564359817
 13473.17992777221
  8831.14640717416
 13147.424953777645
 20308.7457023023
 15118.351385654014
 14368.980508505716
 11085.24048985388
 10369.773640703197
 11588.981579617212
 11020.330477061723
 13801.608437553645
 15246.208367288988
 13105.862635301137
 14347.019872368928
  9204.51712839386
  9399.606442274653
  9410.724349210792
  8172.362704028965
  9850.121162182279
 10602.168407376488
 10108.007701531025
 11260.20784372248
 11726.664797233449
  9258.831925514916
 15301.745509545748
  8344.108746051292
  9576.909008858225
 14007.129558485702
 14929.148553588973
 12984.686795154863
  9021.943562929038
 14934.792584949186
 11826.847467736206
  9509.523449453536
 10469.997022453388
  8841.116315592664
  8401.665843398605
 10996.205992009385
 11624.900784175737
  9836.237611783405
 10680.57945424421
  9334.87237599776
  9312.807616954498
 12424.334918424827
  8126.056976042746
  8359.951587330259
  9572.245262601393
  8690.896645278899
 10255.9048125615
  9878.596295978157
  9107.00105820004
  7261.435760375273
  9614.480266997456
 10173.21625038175
  9270.266524270084
 12960.668455049028
  9658.912756089332
 11197.6093036021
 11028.772524815047
  8263.60633518175
 14612.385462153961
  9422.178702950894
  9049.185234047698
 10863.751490558692
  9589.767450231162
 12744.236903664067
 11680.340151468052
 12325.413860811774
 10706.992583732695
  8630.106172915124
  8479.378417209255
  7904.064770552455
  9187.079531410322
  8382.20271179638
  8508.78027025349
 19880.434942334345
 16408.640590343428
 15293.136101107491
 12263.44368682357
 16095.874898772725
 14564.579060032553
  9621.091394428504
  8704.550723842493
 10753.873195235285
  8636.763183127656
 10843.491576167175
 12411.141831036784
  8740.468493159864
 13775.506367501206
  9969.756207372531
 12805.86257263569
 10332.43096250318
  8769.716140295433
  9608.82449884646
 13379.90645509437
 14492.016651020813
  8843.636294990923
 10921.859737058341
  9472.049260723019
 15589.41424151006
 14876.529543045694
 11728.196803173847
 13001.348682294629
 11601.141129840707
  8641.95992405505
 13587.096059383188
  8178.044590114791
  9732.22885197648
 11397.832778402319
  9301.946707974967
  8234.209921430262
  8369.092530896212
  8511.923134818604
  9012.747533807635
  7203.545058883268
  6978.893330783769
  7201.156006644664
  7525.871091745492
  9693.252307828803
  8848.707030553533
 12397.79971187136
  7491.305514407672
 14787.65374345456
 15732.286905559213
 14614.49352052011
 14268.452538011428
 11014.033988312232
 12242.240091133055
 10062.80491756324
 11525.27067480459
  8470.29438731237
  8521.682181918355
  7741.086486859099
  7816.521652564639
 10794.505341814696
  8141.883329390777
  9715.620804089489
 15166.909403319323
 16129.15561930787
 10452.66693405797
 11880.266596928968
  7908.906778917018
  8268.81960400472
 12635.881334360893
 12031.841435149903
  9221.256216886213
  9956.938539582456
 12819.075728311347
 11315.506510654237
 11823.01365405847
 15002.107395066598
 13511.816860642473
 11161.029005364948
 15927.619209678993
 10797.126200957033
  9176.944327064044
 10388.37240611718
 10244.05185495981
  8843.21263433318
  9988.371604183232
  8884.378445905139
 10292.956929216989
 14106.517707917308
  9362.180118488704
 11774.591401459726
 12620.5282666053
 10378.44826851118
 13132.294444693147
 11659.138332094242
 10655.628386273489
 13471.45748533711
  8652.753067831369
  9044.87387136503
 14205.268928327381
 15070.615467628257
 14192.010139057444
 18513.651674957837
 16641.876016292503
  9980.08496674658
 10691.061300989635
 11058.535500182363
  9137.221944626086
 15201.797576255109
  6701.282790187535
 10047.794125712213
 10721.2141598196
 10395.462290738375
 11970.820718478868
 10850.825990404604
  9718.01001172335
  9602.0230716591
  9780.002620114927
 11187.167867968123
 11092.456688115522
 10066.992290087846
 11489.313469891595
  9349.291785645459
  9209.68338366366
  9121.866478010921
 11154.618478687315
 14201.357658243987
  8893.0157006176
  9591.479974876906
  7894.929758626521
 10777.935642212926
 13313.313670589068
 12129.900206724406
 10116.498701033432
 10122.549251344471
 13130.781689190228
 10493.985491974412
 10301.134194506427
  9866.123742261825
  8486.076131013882
  8773.147681921537
 12060.218948269841
  8986.06268972074
  7635.761118860175
 10782.488245619923
 10197.759526928588
  8965.147329270478
  8807.725019734362
  8729.595944406125
  8094.9080588173765
 12988.851679150648
  8701.664697281876
 11643.055900075673
 11626.013759289519
 10898.020254897281
 10120.954316784506
  9143.501169784335
  9617.729642048716
  8585.848584641622
 10677.866952784094
  8885.23238685238
 12099.941351679281
 11064.936356355907
 10907.113205221383
 11188.828403587562
 13851.183796434045
 16652.75047201836
 10747.308098361398
 12386.852839526455
 11688.008337495816
  9169.108159973195
  7409.48824382883
  7260.900015677748
  8798.904733733878
 14433.909379778741
 20850.106571344582
 18176.662817087195
 12596.674626953354
 12858.439135890902
  8921.495267413753
  8288.249419961456
  7906.389812917213
  7335.438871459062
  8480.069079105171
  9193.763439191845
  9350.809461082372
  9849.423210695824
 10237.582198263426
 10730.538421762547
  7154.309959914862
 10346.867294287771
  8634.972430218073
 10520.787287691686
  8293.598869425992
  8338.369530354938
 10996.26292265694
  7862.661694238033
  8975.805833895976
 11315.710780413614
 10117.607399053166
  9796.842197029706
  9494.738887302996
 11312.37613495044
 13903.99894103678
 13027.779556936113
  9614.491326946361
 12174.499024179462
 12248.637521765137
 15308.794108346872
 11133.85343484901
  8788.248257463765
  8193.699045436091
  9011.929978698752
  9960.345743775577
 17835.87121854838
 10985.382285872352
 10466.6856569791
 15178.171114401035
 12105.461634733625
 15131.35979693663
 12258.085738590682
 12987.281547520608
 14862.978322175244
 12564.35443069998
 10465.987284139437
  9966.366885434127
 15606.990106033893
  9527.798991468733
  9459.183538036023
 11433.04838774965
 10649.18131653526
 13615.52620378252
 16240.981193717496
 15513.564799681873
 11982.03246788051
 10331.030098233758
 12640.72897379387
 11417.319139081459
 10249.010942439947
 15850.085792641597
  9570.399962045236
  7820.917262111036
  7529.16559722318
  7665.226371910361
  7449.949897228707
  7648.113140997971
  9020.835332808074
 12814.835112325109
  8391.860793567634
  7290.286097722601
  9131.285603459086
  9223.395046247373
 12081.379060031537
 11251.462312798822
 11027.896717511394
 15722.968725172112
 10202.783108308551
 12162.312507371775
 13275.664125513784
 11055.382756374122
  9221.4807316713
 13690.502467570466
 12695.409520104426
 12409.022090661703
  8676.590546328556
 10519.97007842747
 15312.340902258411
 16535.413487744165
 10434.257604767214
 13211.786210435514
  8588.319417717925
 12906.971129682926
 12500.01751521001
 10454.62891623361
 10508.097838295997
 11050.05978550801
 12626.208506438248
 11160.905025759605
 11195.40102800502
 13872.57706937082
  9986.253265160913
  8540.522122175154
 10820.06501763447
  9022.25175473209
  9204.227220974317
 10691.937875557913
 11323.490439876488
 13058.828745225519
  9346.278042484779
  8177.170340265574
  9011.283737707037
  7143.174578043505
  8981.987122945584
  7698.156152148425
  7818.292806764009
  7356.418706450508
  7742.359562204912
 15446.70910364013
  7714.989818377116
  8461.90804797484
  7462.336668159352
 10893.724166731858
  8852.147074716742
 11259.309723988075
 15562.579804026414
 13185.61599598366
  8904.297618482691
 12809.69702437165
 14283.581416742858
 14106.059373506929
 12086.9563765284
  9871.663410741381
 13240.424734574855
  9235.471132357088
 10078.795616965463
  8666.869476429783
 15953.62481132278
 11969.67736114648
  8147.589039035924
  8155.349924606748
 17577.92777947747
 15270.780588486694
 18297.542796716694
 10035.599617204845
  9701.95897188773
  7071.0815443874935
 17539.95636518836
 18746.414005768856
 13623.557938532778
  8425.082668134568
  7943.969603829746
 12076.669311418618
  7975.8359578054105
 12130.828171542355
 11564.607988674607
 12970.818953300302
  9751.457316803126
 12553.291011452211
 19173.712333295367
 13600.287538870147
  9020.873476852057
 12138.927162860899
 11499.795700726932
 10361.222494662901
  9556.556531680091
 14404.454612358408
 24705.71149067331
 14987.79827416944
 14546.603685068922
  8801.856140487947
 13194.51036007067
 11989.267274571197
  8767.429110974856
 12036.85458421614
 11060.583472750832
  9540.739937513052
  8044.802876632302
  8770.564980375857
 11521.378315673148
 10260.914127259584
 11718.118386354927
  7218.4094054488
 10812.943555501086
 10434.380030220484
 13570.530614173409
 15690.758944769606
 11222.423809035594
 14413.815516093866
 12930.113337561621
  9556.496621249622
 11295.308822950277
  8546.766699386784
 10564.826029338665
 12324.736800395218
 11461.396204163808
  8799.82533036411
 11149.488133204022
  7358.484478212419
 14010.204145140286
 11124.72843073944
 12793.153793948566
 10697.044956137763
 10879.178231236212
  9962.10071795187
  9523.078111537363
 14334.19573653884
  9789.1302113603
 12148.57077556933
  9917.140716601773
  9219.749080325992
 10384.932746143142
 11341.209960081307
  8098.157559712576
  7971.743747934935
  8515.613064254747
  9839.621076490223
  9322.150854446025
  9694.215627210966
 13077.865805934656
  8847.962397398947
  8550.351378034618
  9595.863723206048
  8617.74420434296
 11562.244883946545
  8844.26759833411
  9347.778583697916
 12092.727348513718
  9207.013373086762
  8152.68723081552
  8107.743354607696
 10593.199341833986
 10592.501145316972
 11789.730468315644
  9100.868157785064
 12009.686872926874
 11571.302308498527
  9845.4852013352
 10293.034040382969
 13269.812901833275
 11268.998954147648
  8692.57062642415
 10207.536493301764
  7622.715289864725
  8949.522513749973
 11336.546424782848
 10804.936160573578
 13846.714078099674
 11855.933943541813
  8948.621979862937
 10879.286809672112
  8800.644270223267
  8360.033553477093
  9500.324046910531
  9871.615311678106
  8886.69309619541
 11835.62202888529
 10269.576211199352
  7240.805621989553
  7512.6105860359885
  9052.982779080794
 11306.125230744141
  8819.646142754998
 11848.247773542535
  7941.201293978249
 17855.56957631434
 11098.923064654318
 13009.349192778585
  8222.474803577628
  9812.953201641823
 13584.156066753503
  8671.712897752106
 11516.413594935086
 14213.593326227558
 10937.280640613366
 12160.45675808198
 11073.841772162656
  8916.450306064711
 10786.143964806894
  9308.655749519641
 10936.847236441685
  9182.049495664034
 13102.785098673625
 10893.41149476199
  9931.574869755923
 10684.4216122282
 10292.138433197042
  8442.380253741401
 12709.275838948142
 11249.354099342427
 17664.80648930463
 14905.665623046589
 16232.24928177066
  7722.511442540008
 15808.846443319868
  7839.783636739974
 24441.74094392399
 11452.729770519627
 13200.472916214381
  9206.156492791368
  9322.84522844477
 12019.089787415467
  9329.722948272689
 10019.964064092112
  9335.189299044257
 12459.065631125464
 12038.259252209988
 10239.480067276549
  9142.54847315636
  7924.3213226760745
  7632.71056097715
 13664.628611181772
  8524.029886535573
 12606.175483158313
  9355.344442692713
 18360.181098822613
 11386.905686453747
 11322.93494754856
  8155.998679898314
  9932.950924197947
  8762.063421712437
 10296.723293416719
  9110.095477959298
  9618.467305090568
  7373.678567320272
  7707.7483231688975
  9058.508423442983
 10827.227193852596
 10618.400819335899
 13121.617903588785
 10853.96715403711
 16604.602390339503
 11484.620183958748
  9355.586061328344
  8262.173946455023
 10167.760942139425
 10115.802729720586
  9407.001343578086
  9668.458560756779
  8467.533348070607
  9179.856830201448
 11325.337035072298
  8375.75650696404
 11402.453509267094
 10858.882412338677
 11114.404502858433
  9999.239099221346
  9735.632579294232
 12688.519366095954
  8330.839421830833
 10607.004472072767
 11990.948330459922
 11482.627425522449
 11108.295901045096
  9469.246403137122
 10822.209549552535
 11432.007987691535
 10702.711038176782
  9020.765359462132
 11834.466911239619
 11758.040207742328
  8246.969446070107
 11293.12056390893
 11156.220987574263
  7430.1870141257195
 11518.402538022958
  9818.74128786626
  8185.129917032101
  9272.328638498286
 10749.227248333116
  8103.5492546264995
 12670.539619311376
 12646.485566189793
 15798.819278745883
  7831.981252833917
 10189.616863781954
  7302.901508477077
  9532.054036297799
  7829.6694971906
  8269.272058173487
 11223.78135182403
 14406.361982109995
 11337.758616284225
 11013.763979615222
  8708.060981205432
 17383.726213439782
 10003.996690407816
 15419.70233785188
 11006.586613671287
 13303.686405588482
 11033.568097614607
  8454.200791971183
  8195.160546480649
 11321.846574737754
  8289.917204689353
 11572.143043074806
 10275.75507666526
  9144.692450596718
  7654.212435048704
 10532.193461337698
 10500.33344900911
 12633.32234833739
 14763.985644003675
  8467.557010042308
 11630.33958526073
  8325.27168915592
  9756.913640963889
 10588.773581072179
  7498.5887512225545
  9081.514217970138
  8144.438298276562
  9128.596648602243
  8485.84971579958
 12025.706299041813
 10522.945427912988
 16280.413966663424
 15636.720072925858
 13130.018124610157
 11203.61290925099
 14343.350997375639
  8861.14270757986
  9543.76201535332
 17597.60257407307
 13933.364876581909
  8846.801833683854
  7644.437662094297
  9775.940938356198
  7970.947384330672
 13244.124263780346
  9397.15642389484
 13206.760716548313
 15140.33028701553
 10407.640756694904
 10173.28886782925
 12144.95896780349
  9763.986048453908
 17747.2964328505
 16321.877478108894
 15620.185802303724
 11887.69459675918
 12492.15942859388
 10270.931696605894
 14385.53751603702
 14380.594896261784
  9864.567086840394
 11996.501589894588
  9612.818280448775
 12710.143242882315
 15941.757450628445
 16210.32115707974
 14515.331028824849
 16688.323529725574
  8089.179459899234
 18502.273436203082
  8637.454529249353
 10029.544367821936
 14998.216744117555
  8533.250201970879
 12174.782634995932
 13252.963282700068
 14612.003799634498
  9455.01098776065
 10888.289665217133
 17192.772021442095
 11492.6130649417
  9896.089106995109
  9947.826223123911
 15193.195371378828
 12214.803167775555
  9518.766564066058
  9416.108479923952
 11349.246569060515
  9595.476020473914
 10334.454951771342
  8857.242589146364
 10077.3555264326
 10234.659423416117
  9307.612775676545
  9036.02771456153
  9215.339832642925
  9800.294483856755
 11025.91260143389
 10904.111493421078
  9335.572076506985
  8587.296275275952
 11034.699509130185
  9746.597904166589
  9452.150009457184
  9295.251522626535
  8556.495037639925
  8940.289373799516
 10418.44174228374
 12085.908687817022
 12042.139985041114
 12058.091380627071
  9761.48076099551
  9179.215293951214
  8941.984356584551
  7913.354378692303
  7726.54728851407
 11010.715097032666
 12638.015397076275
 13770.969771476017
  8839.541824162221
 11779.75116647057
 11157.203181663981
  9953.342984564582
  8435.6484455605
  9688.29642821766
  9156.345461341078
  8922.787017989778
  9098.056889202633
  9447.14887805639
  9816.531757845998
 11605.821711641824
 10104.747757200284
  9978.354451179332
 11649.704623255442
 10727.143430344178
  8313.704822816806
  8567.466208389791
  8251.921952041292
 11620.090889730316
 16312.45838834975
 10541.96279752078
 10607.8861551755
 10865.592616449545
 10812.329686648667
  9432.30775183757
  9622.312957002798
 12930.534643488067
  7900.80577219905
  7813.664860327864
 13695.170395172934
 11808.25788449556
 15460.701590354065
 12205.592316440308
 13482.132410200667
  9434.183685529862
  8860.475020219255
 10048.03622969762
  9684.073618657763
  7469.376649960362
  8602.291997027278
 10112.728910430395
 10346.00671831921
  7564.21703429963
 10271.935049762513
  8103.672976272278
 10716.292485544704
 13242.287549230547
 13349.367740406948
 11608.275418650193
 14072.496197403756
  8666.972912371297
 10396.522734564658
 12542.932617427174
 10615.963920383052
  9030.648766161617
 11909.128053678387
 11299.18714521622
 10493.824639514081
  9457.65855033918
  8613.494842627724
  9160.616483003058
  8022.808869548086
 10642.801675255198
 10156.546714407636
  9749.78794131869
  9918.19019100005
  9068.312806243326
  8470.220403341607
  8034.229730646009
  8168.154557240795
 12381.057743209389
  7727.610526876805
 10215.399650953526
  9302.87698561648
 13533.44074128638
 10088.850440992544
  9156.879333951518
  8672.119583794654
Delay-sensitivity infection-count table
julia
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._"
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1baseline (hospital pathway)7983.09241.010172.012058.013333.016383.0
2community pathway7726.08713.09429.011254.012648.015590.0
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1baseline (hospital pathway)7983.09241.010172.012058.013333.016383.0
2community pathway7726.08713.09429.011254.012648.015590.0
Delay-sensitivity infection-count density plot
julia
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 ( HPD 09 Feb – 12 Apr). The report also fits an Exponential growth tree prior, which dates the common ancestor about a week earlier to 08 March 2026 ( HPD 01 Feb – 05 Apr) (Mbala-Kingebeni and others, 2026). Both priors give similar evolutionary rates (  subs/site/year). We re-fit the joint model under the Exponential growth TMRCA and compare the infection count to date and the outbreak age.

Re-fit the joint under the Exponential growth tree prior
julia
# 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])) : nothing
1000-element Vector{Float64}:
 176.77001773167783
 155.8872766419388
 156.7366582844191
 166.95166848419413
 150.9787482711913
 174.52077575595806
 149.68048263898112
 151.8034488833334
 155.70722299439376
 154.21107571327673
 154.33356743567313
 159.3389002135946
 161.90942140519854
 172.3388580345478
 176.6043441730296
 147.31680227702935
 154.53796780470392
 165.15158332660783
 158.740001737569
 155.43603692607454
 154.21372012451928
 163.900199892678
 167.21483948742286
 175.30466680315007
 171.24250133920444
 160.28037879518118
 151.7576037235861
 153.308120214451
 152.97518384132846
 150.37484684837148
 161.5711088071252
 168.19604690728798
 147.7864240985308
 146.19190852196013
 158.84142182218054
 175.34359797056308
 160.74477911659596
 163.3112227857894
 150.00049117112394
 152.14798549398736
 151.10505405013927
 163.11895996195582
 174.57593951757124
 148.324538578212
 157.05843407477963
 150.70487210466766
 149.64523514245514
 151.28794112931487
 151.86166445669076
 148.94348873656668
 146.0411748823984
 146.01709667488777
 146.01951997629857
 147.25455334857918
 162.39858076610966
 173.98373469334177
 167.59266847375946
 175.25054184557783
 149.96382515398238
 160.28157263973765
 152.05935811132684
 180.1547846852343
 162.8918329328126
 163.44264352865937
 190.46127034867465
 169.1013759653328
 155.38348923669824
 151.74328031444864
 147.6692895515148
 155.67988559019562
 154.27324857482523
 149.6200272174765
 150.58929833163913
 154.34104807296896
 149.69837436934912
 171.99606589351157
 157.72472996948886
 167.31904530935535
 160.20070541694486
 177.8525237343078
 172.90506963692712
 186.56829591521299
 158.96972293819653
 147.356371273177
 146.74232850944094
 172.2419729467857
 169.36406100386165
 146.47376332229297
 166.99496749709306
 146.79702588314427
 156.3457604292784
 155.66261617913062
 149.60163325142594
 163.37357598312062
 158.89900743261197
 160.68911580146064
 156.75907143276692
 173.39708402847953
 163.5193989267077
 151.57093156276912
 151.41065023730283
 166.75917737514692
 165.82910247900548
 155.5201974351333
 157.03228099914048
 156.5552028460723
 157.30242095105154
 149.30422332179668
 157.77251212132222
 163.17259385346497
 172.56130169647753
 146.42826376270094
 149.51418856500476
 200.65588976836506
 237.10153585266457
 185.32347325785477
 153.38934819998096
 163.5018542963483
 146.21291496231794
 151.69407757739407
 163.8026904187604
 152.10230950938885
 173.48279867752404
 152.66430875371984
 150.2283982331994
 149.8917736595448
 162.7575864741246
 147.55383507538647
 164.39084310185694
 165.33780159120872
 157.61078252921027
 165.6279186467571
 169.35733342847627
 149.30261096022548
 173.15564616285533
 148.44636764462217
 157.68059900839103
 152.31912605276514
 151.2496138883874
 161.34312245508977
 158.24110475846976
 164.52248158050503
 158.06554744900018
 154.4099207256037
 157.21996687365893
 154.4406745875022
 154.87893173145872
 155.15733668501724
 160.81221698416417
 160.2746266164244
 157.20979430649592
 162.99993319151847
 153.53183038453722
 166.49183296948627
 147.47362656130306
 153.0805104410947
 148.0779958122016
 153.99743356545713
 158.20690239260387
 170.49351949006052
 153.0339764719149
 147.82900707466487
 148.07844085321875
 165.0563117746911
 154.96906990558998
 152.59528361290234
 161.74634737721223
 154.14685177016602
 154.68584689813784
 162.91961736433612
 161.69272431303614
 148.98972542315985
 160.98218954504395
 156.560824660242
 187.33197458669838
 168.69209802565942
 183.8154998361333
 148.6983905192228
 160.49599946240167
 174.20721352099596
 160.403741313638
 164.91548864233928
 162.07962807192501
 152.40959547764288
 177.0734642114972
 158.44926501470238
 155.61872871260135
 163.58491121920974
 147.99487028011583
 147.5961944588906
 162.12543595745717
 156.98351990907514
 146.60791026337378
 181.77619206884637
 158.2721219855763
 155.85792884039722
 151.5588122834508
 149.3813903283446
 161.78535716891705
 157.60412248535684
 146.19565038898506
 146.39259257258584
 183.82293120396886
 159.16133543242253
 154.05345371413296
 154.2476744966096
 166.33117014495218
 152.19785266808444
 170.0314592142055
 166.8000012705309
 160.4795691487917
 154.41702864499524
 175.45161070285167
 154.22186202942862
 162.8362541632726
 179.6208700600539
 147.32203847993418
 147.99147892043604
 174.94498766860912
 175.56765151380597
 147.74922486559285
 150.26776941195112
 155.01669607525736
 150.4303858610333
 150.68715381670285
 153.1515717807405
 155.6765924300872
 163.12836865811454
 146.72248019087675
 180.39899602278732
 146.8683921844142
 182.9961695975313
 166.40360112169628
 150.06281232639824
 151.2933616091089
 159.59667023599837
 156.20410538038098
 151.3307968583434
 166.64014217306664
 150.85775151381608
 154.6107283152922
 147.33400292457176
 159.1133304797428
 158.35053922951883
 152.62609394961325
 154.46456970691796
 155.685184531493
 153.72518619992508
 150.81379299078245
 160.94323154922353
 146.33683568167126
 156.3324670730671
 187.86536544494467
 149.10979274240682
 156.01571913173547
 153.61858514817126
 167.88906111390756
 163.19966502474503
 160.50075276034835
 175.63613684587543
 184.98876632378602
 147.02403008974227
 176.00323059778742
 165.46766800914043
 189.65792889497442
 151.8930085746542
 157.43225974071436
 151.44950113985112
 177.87233896366305
 172.53419840667794
 146.2285168990999
 146.84011847708092
 158.1039402668116
 156.6432510396488
 155.54100439536688
 154.54964382711628
 146.55600239829573
 146.43082556594413
 179.74365387501553
 148.46494129033337
 150.921943692843
 154.78515618695124
 160.54699590907228
 153.67240939721393
 149.44557872564556
 157.0641482125325
 146.0648894396985
 165.91788396026328
 147.88610782796675
 156.48214174539112
 146.30373747476997
 146.461745477656
 169.09434077866356
 160.4747716896884
 164.7781506698853
 150.02396980226047
 165.1799238834367
 156.05230974678827
 154.74964387109836
 147.0142674042829
 148.5355719342396
 149.57955106278635
 147.66399173622125
 156.02000403877543
 153.0546676486473
 155.69059570292205
 154.24479911600557
 148.1617718396219
 164.40830687957873
 151.6698088788297
 166.68886574396416
 156.3075539574682
 162.84478079641747
 171.0473377263999
 147.9241522850245
 156.46344770864968
 152.16191535164404
 157.2090768585388
 153.13900422674283
 164.31231633138418
 162.77778047852294
 182.93140156918935
 163.29282023894837
 164.20375481341964
 162.3909305004351
 155.3987455101275
 169.85948170812142
 158.79315283646474
 149.95046687096038
 163.0422266271598
 175.62406459446126
 160.4368460457401
 167.61044341213392
 157.86800064137935
 153.84417764655353
 163.90363969414287
 146.22620187494638
 188.9943041557677
 181.36584808137533
 159.41615464449072
 159.137856131204
 146.5483013673094
 149.6905045095846
 159.88759059983485
 147.30919294088085
 161.50782515912826
 149.09998610477533
 148.0159029584606
 170.23453029614106
 146.57968498179721
 152.3841720778359
 174.04970307021014
 171.3468292311719
 158.33103423343863
 150.2245342448141
 167.6429090911228
 162.2889480829364
 158.38772098886085
 150.70404313192734
 148.72682196774164
 178.93571245730072
 155.3832504739094
 152.82405909414746
 169.1279797074787
 150.14148846000614
 151.33633461896522
 151.5042391628916
 149.27291447411142
 158.5549434442178
 156.58697132574184
 165.3105731492485
 153.0410750376517
 147.1618863347038
 164.02655001489182
 161.1883173349328
 180.98528251974125
 163.96667813629952
 152.6549418647895
 152.99449161658288
 164.08499375919075
 158.81784133333954
 150.91829803855165
 154.48200417186476
 164.47024871832238
 173.60649405400417
 175.0071526502551
 161.75410372654534
 148.77870988419838
 165.91740751576032
 148.9328212738668
 170.296910331854
 172.03615857123737
 159.3493755249191
 148.2684028319696
 156.39055665932074
 159.05659649553843
 162.6640534595765
 158.94039066700685
 168.66945739439421
 147.18577637559804
 146.90404072200235
 146.78187929529412
 146.36049807282149
 163.3993757648773
 157.98960389115084
 149.4766041113701
 158.35812036203293
 158.78675808674586
 153.86816848216796
 170.4812100115563
 158.97983927587404
 246.9720423110198
 147.73014051418627
 148.46304841807407
 208.02607625426435
 147.23990293077367
 151.25424237585074
 154.2238614441232
 150.04357300644367
 163.0068117620479
 152.7905185505058
 155.84378178377713
 152.34912779348218
 151.76375718537926
 158.8416069991738
 150.07951828253402
 153.73378036577338
 149.4165925261782
 164.30407468780987
 155.3418206231146
 157.06643915270567
 187.17858361911402
 152.7475344580631
 166.05318846313256
 158.75661412406993
 172.97378276316635
 162.2295259587052
 149.66140971363976
 147.54856817825322
 153.5759447487907
 157.69507072407518
 148.9444744572947
 147.7784159093697
 161.55666303879877
 156.71204040259244
 168.3546816026389
 149.35265723980876
 158.52580229014114
 160.05605736608354
 156.1588528572084
 156.8925392720488
 166.3644828556086
 150.927300653618
 172.40573834825585
 163.32491557439036
 160.48338720433804
 148.99675886682473
 150.3925304165074
 151.11472635299697
 148.72480421860854
 172.44170786258672
 149.27760909655908
 168.73527186224192
 158.24951391032803
 174.509397893957
 147.7472475162659
 168.13765778814556
 155.93951800012877
 154.04801941340583
 170.43182247037726
 164.79864168624425
 159.32805932658547
 146.99840171895994
 164.72457484523642
 158.75075468404927
 168.01516722168785
 149.89867566290775
 157.2386423307521
 178.65445350325956
 154.18781072370552
 159.36395842139237
 147.9144502742515
 152.40815666734935
 146.77013078861776
 146.69219951993787
 146.35988973222646
 172.72952552358458
 146.27593673358078
 174.4506084659477
 148.21740507576092
 166.4718707203871
 148.30141765583718
 149.60169723125415
 146.889041911465
 147.8644574157206
 160.86339486415906
 159.73717887652754
 150.0751900987007
 146.63529145085815
 146.51679104556484
 163.46844702031368
 149.1961685839912
 174.53769908445514
 155.67397346918835
 153.87368998307983
 155.34401021579873
 160.53095686304755
 158.2531305012295
 154.64351829449657
 146.02265503502068
 151.89600160254508
 152.09130079042956
 170.10579707147747
 182.29276486777871
 152.08208198611047
 147.91555795762318
 148.6255665816708
 157.41288752299917
 150.07683171260163
 158.27802348592994
 146.60667065049137
 146.35341044621924
 146.35075755954415
 146.14185194396555
 151.47982390755445
 146.89035355419895
 146.94834812127027
 152.01619768554494
 159.86069667626592
 179.0074905743219
 157.36806464293036
 154.84256482238328
 149.86668618770014
 166.78052450344506
 154.82006311156638
 150.6684420697413
 159.4117407725174
 147.69086078751027
 174.80834934323974
 149.2682801388315
 163.73190924951768
 160.86861693245947
 176.33973591808876
 175.10469445019964
 165.45273406247807
 189.03074505132594
 174.5863349205008
 150.73801611425142
 156.12295886910724
 149.55701362019425
 164.82313192826552
 147.3336219527844
 146.71792903581786
 156.93284060116054
 166.88101959702317
 169.2210309156849
 159.59133167941104
 160.79495911110314
 146.95784582736576
 146.26517987396437
 146.47283182292298
 162.68025366112403
 164.75327497938008
 157.7395968179917
 158.11104014311567
 153.3426484261353
 151.62344022597614
 150.535314359707
 155.33535509293498
 167.63140958774315
 169.73153399330357
 148.63268084912167
 155.7288255973597
 154.75755711921332
 154.95202847195026
 152.01663222298012
 163.55309579790185
 157.49176462370784
 152.09536140262662
 154.6794133118829
 152.2666936639848
 155.65730224978034
 151.9364236162762
 162.8211612851716
 147.70084755474528
 175.60382478953193
 170.604418069815
 166.95900495161672
 158.63135476020798
 153.5680817630697
 156.5260843048813
 158.47572728225487
 185.9673067341113
 173.7562167578593
 156.2274459839258
 159.12393979853454
 158.8876852430036
 156.8855405770451
 148.91339751225095
 162.28064313318527
 146.41704341449054
 168.8499795407857
 146.48263967816342
 149.18022304142335
 148.3575199017037
 166.16991421797002
 154.0886197797327
 164.04569875101677
 149.1332544150588
 149.38558703710814
 152.60628726478024
 191.663739760527
 150.7300125712883
 153.06574446931106
 165.27793511016134
 151.36694455238745
 166.90037863302814
 151.82520990263222
 159.02071081872836
 155.71189670876285
 147.51911943719242
 149.36654754434784
 160.29142828090946
 156.4605494779359
 165.29258484211158
 153.79859376927075
 148.7437100661193
 159.82947216447926
 149.02279600068903
 148.37322797153365
 147.48979069726082
 175.73761301145012
 154.7295856826332
 159.38205976765005
 163.4876336336707
 154.00829271781953
 152.844485933075
 146.40584660866153
 146.54945152037783
 174.03052240406353
 168.7500905412031
 162.8413773488646
 200.97753843168314
 161.85625165891003
 151.6958789708551
 152.63602915538044
 165.91283253838844
 161.2134337584614
 151.32183634801243
 147.08816913928348
 147.09469727775544
 149.6063581814641
 150.55371059944926
 164.7060766705111
 146.561290227521
 146.48907895576627
 147.7253190713015
 154.85752883892226
 167.57653864761215
 150.3100169981346
 147.11564848868034
 170.31736521102056
 156.94555030673143
 162.95128008391097
 159.92412481573905
 155.0420572822711
 151.09453578674544
 159.61582326524572
 148.67229112713235
 155.1552633011287
 166.1639760716561
 156.51971760095654
 155.91683703390018
 157.358136723489
 153.42664000148125
 156.55978675909628
 165.40218624838346
 150.82232054839346
 148.50959727822794
 160.0155324924972
 156.1980381161773
 154.51996659981765
 165.12574998154017
 150.43943845437067
 150.65304378289233
 162.23181815568017
 155.76310318257066
 158.22637248055793
 146.98591028480524
 164.4738505722141
 150.52125482530897
 147.46870561720456
 156.65152049914985
 170.36353063807414
 149.68807855756455
 149.95023265967632
 165.44367812129266
 160.72893041781887
 156.98976897927292
 154.77980191342002
 158.26259005072734
 150.60803194818547
 150.84246579421836
 161.39579334126603
 149.45341555439364
 149.25246166938774
 153.46926650108372
 148.43336227619517
 147.77934348697082
 152.2135693279528
 163.41840104848308
 150.68843098772197
 149.69565780451288
 161.6183774793292
 147.59812638038426
 151.34567309747538
 174.78035739066422
 165.10322788719466
 148.62731480798718
 156.7010749475124
 169.50597972181205
 147.225215417736
 163.98543532526685
 151.9727969268722
 161.54296674324308
 148.46817367522408
 161.91841332151472
 148.65219669846158
 168.91165051743957
 154.8760954656044
 171.45588330327126
 153.8774044393869
 154.9610007546839
 168.18671836954084
 156.15949475295355
 159.49747339247085
 154.30351942188594
 176.26469976566725
 178.34226177379688
 171.4181929939349
 148.13338180201143
 148.03285023509687
 150.80750031429807
 147.232982883016
 156.55126128154808
 153.50647015219667
 156.3069995633464
 171.72823561197757
 151.6378712323902
 151.85153018500426
 158.74310898506673
 147.88262149375325
 156.55741350838989
 161.19347299857668
 150.9129148378944
 147.71687822742604
 153.38059666471284
 151.36397841915726
 157.72657485606268
 147.51597326146657
 174.20157468798763
 148.02577076936018
 159.73774675838328
 154.57631858361606
 154.7930788426704
 170.0450150487462
 150.38294951636692
 169.18534752448667
 156.32614947777637
 164.05185713556438
 149.40982227611997
 152.9061385293419
 156.76385248524076
 153.52789245447784
 173.11179375210747
 148.7120804864518
 147.5633263664137
 174.89359569936647
 149.13118699900969
 165.26766903067073
 157.29951178572202
 168.86100017434183
 157.6995944610166
 148.50269123238186
 155.82434059976882
 154.30313038132186
 153.2453887853356
 147.1120981280486
 187.60313150203214
 164.72862110297513
 159.87263733727818
 150.50620642318984
 156.3987381143103
 154.03197376896176
 164.18424446584066
 157.95149108814607
 152.09473402405314
 154.12534176394942
 153.00996961194346
 178.82885859293583
 150.8681515508503
 158.8459043442532
 157.83097794636845
 154.90079511699895
 163.85004442932583
 158.02167884127934
 151.65874343494082
 163.9319061197434
 155.36706259397198
 147.90937482755157
 182.71421845991176
 149.47983704559138
 171.32122258409362
 153.8169111792431
 157.0083353102646
 182.43885091341957
 152.24544672106282
 161.96947754166303
 159.81868621639495
 157.21421218081582
 161.84373579254324
 148.90737449987316
 150.23732383859786
 151.28150776310957
 159.72753463451332
 155.61212512074638
 160.67693343206025
 179.1886489749841
 152.0328969507715
 152.78256649099953
 162.0336808910675
 155.29976087635865
 169.67040263229373
 170.25401876419247
 167.4634077048767
 161.28155697715658
 151.9367090122645
 150.31802282396418
 155.8712048979831
 162.02359963964017
 156.5835398761137
 147.34505259818215
 162.4426687052106
 149.25293472438068
 187.60559425236966
 159.3526974241259
 154.14712208244217
 159.72463934666445
 160.81447420522392
 147.70077434680078
 147.95178639406694
 154.07112446928966
 161.66429819184484
 148.1692078719704
 162.72896292491188
 148.1537410051027
 160.15559198143373
 179.85258778966823
 164.13075876267487
 158.1491253162305
 151.62999968577557
 159.18228652292765
 173.87342954643702
 163.5407949676357
 155.41974320151851
 155.74481900199348
 152.4434669452478
 162.6900109479091
 162.74065054968156
 199.7139994851814
 162.62370888062497
 154.31337317438576
 147.9896622639774
 161.7286099488556
 155.62776526455465
 169.8416715327802
 154.1212376460965
 157.69691824881772
 152.9373386831959
 156.72463018539773
 170.95143090015347
 150.59869122083663
 146.20805863736402
 164.73943281487547
 152.74339313918594
 155.99202455875601
 156.91753401655072
 148.96859811705752
 170.39319479960957
 150.2833487380185
 148.61115923768267
 181.53491236003381
 151.61965692365945
 152.38987285172504
 149.68833660485606
 161.86996023287097
 167.02238392908592
 151.3709057410067
 162.07572044531204
 173.2574321596995
 158.72588001400496
 153.69159003682765
 167.7702940079365
 160.67207961249173
 154.24233362400994
 155.78990134227539
 162.50679427408755
 155.50080776408845
 152.6884682113158
 158.16296150027665
 171.6582645390768
 165.91036490697073
 152.6275167982024
 161.8822441359894
 150.43688728805785
 165.10373611556196
 149.16930110433645
 150.62301780699548
 149.74788224495126
 149.8470057793134
 146.99990410532112
 159.42705202410355
 146.6241298980043
 179.4735656696624
 153.04624173816063
 167.72469338673434
 162.20328188998252
 151.07153436846815
 165.92734618343033
 150.837240225193
 155.69084706759125
 155.71179597528243
 165.17305801488888
 147.7953213888542
 162.9471838581999
 152.78470338379876
 150.58090581824996
 156.39817819634092
 154.48401585023524
 176.51358839720385
 150.26171471888074
 168.84331262077973
 161.35585935468916
 147.02950112292925
 182.61257284889376
 154.315420489491
 164.99252561861542
 157.9039922362013
 161.31903896581397
 160.78700309014917
 173.72531298374645
 166.5702610808224
 163.179291429429
 160.0386499138536
 170.23493685036732
 163.67465286930678
 148.03561964235553
 149.0350572625175
 146.63386614609192
 147.64628789330175
 146.77130392370165
 154.32638935131897
 149.83712227300924
 153.3318999234199
 147.41009843948365
 149.06348074592987
 153.55282514659544
 153.73056413657156
 178.1215283998057
 178.82311028325978
 155.09505115097204
 174.8206523093163
 157.32893048335796
 151.96525085803574
 149.02786885754392
 151.90130961199256
 148.85152192487604
 150.42544025396222
 153.64916421888083
 153.80749273737948
 165.86355735788825
 152.90177384485796
 154.32244249622298
 164.05878188368112
 188.51869825313
 153.89156532690947
 149.91802389825824
 148.96304408438502
 149.80472758716922
 156.7057313335247
 162.6204134673359
 150.61581400915958
 171.30231549286054
 162.16696736573377
 156.60414166175548
 163.92120976314214
 158.28094304456386
 169.21758007317553
 156.4725309933671
 169.44841458853165

The 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
julia
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._"
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Skygrid (baseline)7983.09241.010172.012058.013333.016383.0
2Exponential growth7798.08871.09682.011712.013167.016694.0
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Skygrid (baseline)7983.09241.010172.012058.013333.016383.0
2Exponential growth7798.08871.09682.011712.013167.016694.0
Tree-prior infection-count density plot
julia
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
julia
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._"
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Skygrid (baseline)141.0144.0147.0154.0159.0170.0
2Exponential growth147.0150.0153.0160.0165.0177.0
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Skygrid (baseline)141.0144.0147.0154.0159.0170.0
2Exponential growth147.0150.0153.0160.0165.0177.0
Tree-prior outbreak-age density plot
julia
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
julia
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)