Skip to content

Sensitivity and comparison analyses

This page continues the main analysis from the one-week-ahead forecast onward: how last week's forecast held up, how every release's forecasts scored against what has since been observed, the reproduction number by release, the outbreak size each data stream implies on its own, how the estimate has evolved across releases, comparisons with McCabe et al. and Chamla et al., and the delay and tree-prior sensitivity re-fits. The tree-prior sensitivity compares the Skygrid and Exponential growth TMRCA estimates (Mbala-Kingebeni and others, 2026) (~1.1E-3 subs/site/year, 139 BDBV genomes). It renders from the same fitted chains as the main analysis, loaded through the shared setup, so no model is re-fit here beyond the frozen and (gated) sensitivity re-fits.

Load packages, data and fitted chains
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 (last week versus now)

How last week's forecast held up against the data since observed, using the frozen re-fit and one-week projection defined in the methods forecast-versus-frozen evaluation. The frozen fit also conditions on the isolation beds, so the projected bed occupancy is scored against the beds held a week later. The bed validation is weak at a one-week-back freeze: the reported occupancy rate starts only on 9 June, so the capacity has no implied-capacity anchor and rides its random walk back to the freeze date, widening the projected bed interval. Unlike the scores further down, the confirmed new-count rows here keep any retrospective harmonisation step the week contained, so a week holding one reads high against a forecast that never predicted it.

Fit one week back and validate the one-week-ahead forecast
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.
validation_forecast = forecast_reported(frozen_lastweek.chn;
    horizon = 7,
    obs_cases = frozen_lastweek.o.reported_cases,
    obs_deaths = frozen_lastweek.o.total_deaths,
    obs_confirmed = frozen_lastweek.o.confirmed_cases,
    obs_confirmed_deaths = frozen_lastweek.o.confirmed_deaths,
    obs_recovered = frozen_lastweek.o.recovered_cases);

# Each frozen individual (single-stream) fit's own one-week-ahead new-count
# forecast at the same cut-off as `frozen_lastweek`, from
# [`forecast_stream`](@ref) (the same per-stream forecaster
# `stream_forecasts.csv` uses), so the validation plots below can show the
# individual fit alongside the joint rather than the joint alone. Recovered
# has no individual fit and is absent here, as it is throughout this report.
function _validation_individual_new(sid, stream::Symbol, obs_value)
    f = frozen_lastweek_streams[sid]
    bp = f.o.n - f.o.who_first_sitrep_days
    return Float64.(forecast_stream(f.chn, stream; horizon = 7,
        obs_value = obs_value, n = f.o.n, breakpoint = bp,
        rt_start = 1, rt_walk_start = 1))
end
validation_individual = (;
    cases_new = _validation_individual_new(
        "cases", :reported_cases,
        frozen_lastweek_streams["cases"].o.reported_cases),
    deaths_new = _validation_individual_new(
        "deaths", :suspected_deaths,
        frozen_lastweek_streams["deaths"].o.total_deaths),
    confirmed_new = _validation_individual_new(
        "confirmed", :confirmed_cases,
        frozen_lastweek_streams["confirmed"].o.confirmed_cases),
    confirmed_deaths_new = _validation_individual_new(
        "confirmed_deaths", :confirmed_deaths,
        frozen_lastweek_streams["confirmed_deaths"].o.confirmed_deaths))
# The frozen individual (treatment-only) fit's own bed-occupancy forecast,
# anchored on the beds occupied at ITS OWN cut-off (the frozen fit's own
# `o`, not the current `obs`), matching how the joint frozen forecast is
# itself anchored.
_validation_iso_obs = let f = frozen_lastweek_streams["treatment"]
    isempty(f.o.isolation_history.counts) ? 0.0 :
    Float64(f.o.isolation_history.counts[end])
end
validation_individual_isolation = _validation_individual_new(
    "treatment", :isolation_beds, _validation_iso_obs)

# The observed beds at the current cut-off (the forecast target), so the
# frozen-fit bed forecast is scored against what the beds actually held.
_obs_beds = isempty(obs.isolation_history.counts) ? missing :
            obs.isolation_history.counts[end]
# Same observed/baseline keying as the plot below, so the table covers every
# fitted count stream (cumulative and new-count rows) plus the bed level.
validation_table = forecast_vs_truth(validation_forecast;
    observed = (cases_cum = obs.reported_cases,
        deaths_cum = obs.total_deaths,
        confirmed_cum = obs.confirmed_cases,
        confirmed_deaths_cum = obs.confirmed_deaths,
        recovered_cum = obs.recovered_cases),
    baseline = (cases_cum = frozen_lastweek.o.reported_cases,
        deaths_cum = frozen_lastweek.o.total_deaths,
        confirmed_cum = frozen_lastweek.o.confirmed_cases,
        confirmed_deaths_cum = frozen_lastweek.o.confirmed_deaths,
        recovered_cum = frozen_lastweek.o.recovered_cases),
    isolation = _obs_beds);
Forecast-versus-observed validation table
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.01335.01995.02697.04470.06482.010981.0no
2DRC reported casesnew this week0.0258.0918.01620.03393.05405.09904.0no
3DRC suspected deathscumulative by T+7246.0308.0456.0644.01070.01544.02925.0no
4DRC suspected deathsnew this week0.062.0210.0398.0824.01298.02679.0no
5DRC confirmed casescumulative by T+73748.03446.03653.03768.04072.04293.04817.0yes
6DRC confirmed casesnew this week548.0246.0453.0568.0872.01093.01617.0yes
7DRC confirmed deathscumulative by T+71657.0308.0456.0644.01070.01490.01903.0yes
8DRC confirmed deathsnew this week252.00.00.00.00.085.0498.0yes
9DRC recoveredcumulative by T+7708.0571.0602.0670.0845.0977.01469.0yes
10DRC recoverednew this week137.00.031.099.0274.0406.0898.0yes
11DRC isolation bedsoccupancy at T+7690.0735.0862.0879.0897.0907.0925.0no

The observation panels histogram the one-week-ahead forecast made from the frozen fit, a cumulative and a new-count panel for each fitted count stream the forecast carries (reported cases, suspected deaths, confirmed cases, confirmed deaths and recovered), with the 90% predictive interval shaded and the count actually observed by the current cut-off drawn as a dashed black rule. Each stream draws only when the forecast carries its column, so a fit observing fewer streams shows fewer panels. Where a stream has its own individual (single-stream) fit, that fit's own forecast, made from the same frozen cut-off, is overlaid as a dotted density alongside the joint's histogram, so both are visible rather than the joint alone; recovered has no individual fit and draws the joint alone.

Forecast-versus-observed plot
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), 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

How every release's saved one- to four-week-ahead forecast scored against the data observed since, against a persistence baseline and, where one exists, the stream's own individual fit as well as the joint (see forecast scoring against a persistence baseline for how the scores, the relative skill and the baseline are built). Recovered has no individual fit of its own, so its comparison is the baseline against the joint only. Reported cases and suspected deaths stopped being updated by the situation reports partway through the outbreak, and exports' confirmed-detection series is anchored to an earlier cut-off, so exports contributes no scored forecast at all and reported cases and suspected deaths each rest on exactly one matched forecast, a single window rather than a settled sample.

Only a minority of the daily releases examined across the outbreak contribute a row to the table below, and every one of them is a reconstruction of an earlier version of the model rather than the model as it stands today, so the table below is not a verdict on the current fit. One reconstruction is dropped from scoring entirely: its chain forecasts a near-zero median at every horizon and stream, with the upper predictive tail occasionally reaching five- and six-digit values, which is the signature of a chain that failed to sample properly rather than a genuine forecast (see scripts/score_releases.jl, _FAILED_RECONSTRUCTIONS). The releases that do carry the current model's own individual-stream forecasts are too recent for their targets to be observed yet, which is why the comparison against each stream's individual fit is still empty. It will populate once those targets resolve. Every row also rests on one to a handful of matched forecasts, shown as its own count rather than rounded away, so a ratio here should be read as an early signal rather than a settled result.

Load and summarise the cross-release forecast scores
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)
30×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.620.8233.74missingmissing765.6561.120.01.01.00.2
22026-06-10confirmed casesjoint4833.236.50.7584.06missingmissing666.38166.850.01.01.00.31
32026-07-01confirmed casesjoint415862.877.90.5832.85missingmissing15377.3485.470.00.01.00.66
42026-07-06confirmed casesjoint3279.411.510.2671.24missingmissing190.6388.780.00.331.00.52
52026-07-08confirmed casesjoint3266.991.820.2481.45missingmissing163.25103.730.00.331.00.51
62026-07-23confirmed casesconfirmed1100.252.40.2012.76missingmissing86.60.013.651.01.0-0.24
72026-07-23confirmed casesjoint182.581.980.1421.950.820.7180.090.02.491.01.0-0.11
82026-07-25confirmed casesconfirmed1107.93.270.1983.43missingmissing107.740.00.161.01.0-0.02
92026-07-25confirmed casesjoint172.172.190.1312.270.670.6672.060.110.01.01.00.02
102026-06-07confirmed deathsjoint4120.570.990.7251.07missingmissing117.80.02.761.01.0-0.11
112026-06-10confirmed deathsjoint4144.921.10.5981.06missingmissing140.543.760.621.01.00.02
122026-07-01confirmed deathsjoint417196.079.420.5491.2missingmissing17103.192.890.01.01.00.28
132026-07-06confirmed deathsjoint3150.061.110.963.37missingmissing71.580.078.490.01.0-0.53
142026-07-08confirmed deathsjoint3138.41.080.8162.86missingmissing57.070.081.330.01.0-0.64
152026-07-23confirmed deathsconfirmed_deaths1110.135.380.2814.11missingmissing82.5727.560.01.01.00.33
162026-07-23confirmed deathsjoint184.64.130.4726.910.771.6840.540.044.061.01.0-0.49
172026-07-25confirmed deathsconfirmed_deaths156.362.260.2012.26missingmissing53.832.530.01.01.00.11
182026-07-25confirmed deathsjoint179.843.210.4915.531.422.4533.040.046.80.01.0-0.52
192026-07-01isolation bedsjoint490.191.50.1381.54missingmissing13.480.076.710.00.0-0.99
202026-07-06isolation bedsjoint342.990.590.0630.58missingmissing18.380.024.610.01.0-0.69
212026-07-08isolation bedsjoint349.211.510.0651.49missingmissing10.9638.240.00.01.00.81
222026-07-23isolation bedsjoint165.23.330.0813.160.820.622.4742.730.00.01.00.64
232026-07-23isolation bedstreatment179.344.050.1365.3missingmissing58.350.020.991.01.0-0.39
242026-07-25isolation bedsjoint198.171.990.1281.911.771.3323.9374.240.00.01.00.78
252026-07-25isolation bedstreatment155.521.120.0961.43missingmissing55.490.030.01.01.00.02
262026-07-01recoveredjoint410457.7153.970.5391.89missingmissing10409.148.570.01.01.00.29
272026-07-06recoveredjoint356.790.970.41.1missingmissing39.820.016.961.01.0-0.37
282026-07-08recoveredjoint344.341.180.2881.48missingmissing29.460.014.871.01.0-0.4
292026-05-18reported casesjoint1231.140.481.870.3missingmissing61.150.0169.990.01.0-0.66
302026-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 and bias, and the relative skill against the persistence baseline for every fit and, on the joint row where the stream has one, against its own individual fit, on both the natural and the log scale (column definitions in forecast scoring against a persistence baseline).

9×15 DataFrame
Rowstreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
1confirmed casesconfirmed2104.082.790.1993.06missingmissing97.170.06.91.01.0-0.13
2confirmed casesjoint203594.2524.070.5242.80.740.683422.56171.570.120.61.00.39
3confirmed deathsconfirmed_deaths283.243.670.2413.06missingmissing68.215.050.01.01.00.22
4confirmed deathsjoint203543.7926.110.6891.590.992.03495.2719.3329.190.651.0-0.19
5isolation bedsjoint1266.731.280.0951.261.210.915.719.3131.720.00.67-0.18
6isolation bedstreatment267.431.960.1162.5missingmissing56.920.0210.51.01.0-0.18
7recoveredjoint104213.475.260.4221.5missingmissing4184.4319.439.551.01.0-0.11
8reported casesjoint1231.140.481.870.3missingmissing61.150.0169.990.01.0-0.66
9suspected 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
21×16 DataFrame
Rowstreamhorizonfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31Int64String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
1confirmed cases7confirmed2104.082.790.1993.06missingmissing97.170.06.91.01.0-0.13
2confirmed cases7joint7161.144.80.4634.780.740.68122.2838.50.360.571.00.31
3confirmed cases14joint5413.685.560.5134.27missingmissing292.87120.810.00.61.00.42
4confirmed cases21joint5987.694.130.5042.02missingmissing756.12231.570.00.61.00.42
5confirmed cases28joint321250.153.910.7131.76missingmissing20783.4466.650.00.671.00.45
6confirmed deaths7confirmed_deaths283.243.670.2413.06missingmissing68.215.050.01.01.00.22
7confirmed deaths7joint770.443.810.8536.020.992.039.990.829.650.571.0-0.35
8confirmed deaths14joint5152.511.910.6392.11missingmissing110.136.9635.420.61.0-0.2
9confirmed deaths21joint5432.871.90.5580.9missingmissing371.5521.8339.490.61.0-0.14
10confirmed deaths28joint322485.364.540.6070.6missingmissing22405.779.00.61.01.00.14
11isolation beds7joint574.111.420.0991.361.210.915.9131.426.80.00.80.1
12isolation beds7treatment267.431.960.1162.5missingmissing56.920.0210.51.01.0-0.18
13isolation beds14joint356.171.120.081.09missingmissing12.9714.3528.850.00.67-0.26
14isolation beds21joint354.831.030.0811.03missingmissing16.3710.5527.910.00.67-0.29
15isolation beds28joint197.171.790.1611.95missingmissing20.790.076.380.00.0-0.99
16recovered7joint326.962.060.3191.68missingmissing20.931.284.761.01.0-0.18
17recovered14joint389.72.130.3931.56missingmissing68.015.8115.871.01.0-0.23
18recovered21joint3297.163.50.4221.24missingmissing269.916.0511.21.01.0-0.09
19recovered28joint140892.6293.620.8181.75missingmissing40767.7124.850.01.01.00.35
20reported cases7joint1231.140.481.870.3missingmissing61.150.0169.990.01.0-0.66
21suspected deaths7joint141.590.390.5770.12missingmissing41.120.00.471.01.0-0.08
Scores by release
30×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.620.8233.74missingmissing765.6561.120.01.01.00.2
22026-06-10confirmed casesjoint4833.236.50.7584.06missingmissing666.38166.850.01.01.00.31
32026-07-01confirmed casesjoint415862.877.90.5832.85missingmissing15377.3485.470.00.01.00.66
42026-07-06confirmed casesjoint3279.411.510.2671.24missingmissing190.6388.780.00.331.00.52
52026-07-08confirmed casesjoint3266.991.820.2481.45missingmissing163.25103.730.00.331.00.51
62026-07-23confirmed casesconfirmed1100.252.40.2012.76missingmissing86.60.013.651.01.0-0.24
72026-07-23confirmed casesjoint182.581.980.1421.950.820.7180.090.02.491.01.0-0.11
82026-07-25confirmed casesconfirmed1107.93.270.1983.43missingmissing107.740.00.161.01.0-0.02
92026-07-25confirmed casesjoint172.172.190.1312.270.670.6672.060.110.01.01.00.02
102026-06-07confirmed deathsjoint4120.570.990.7251.07missingmissing117.80.02.761.01.0-0.11
112026-06-10confirmed deathsjoint4144.921.10.5981.06missingmissing140.543.760.621.01.00.02
122026-07-01confirmed deathsjoint417196.079.420.5491.2missingmissing17103.192.890.01.01.00.28
132026-07-06confirmed deathsjoint3150.061.110.963.37missingmissing71.580.078.490.01.0-0.53
142026-07-08confirmed deathsjoint3138.41.080.8162.86missingmissing57.070.081.330.01.0-0.64
152026-07-23confirmed deathsconfirmed_deaths1110.135.380.2814.11missingmissing82.5727.560.01.01.00.33
162026-07-23confirmed deathsjoint184.64.130.4726.910.771.6840.540.044.061.01.0-0.49
172026-07-25confirmed deathsconfirmed_deaths156.362.260.2012.26missingmissing53.832.530.01.01.00.11
182026-07-25confirmed deathsjoint179.843.210.4915.531.422.4533.040.046.80.01.0-0.52
192026-07-01isolation bedsjoint490.191.50.1381.54missingmissing13.480.076.710.00.0-0.99
202026-07-06isolation bedsjoint342.990.590.0630.58missingmissing18.380.024.610.01.0-0.69
212026-07-08isolation bedsjoint349.211.510.0651.49missingmissing10.9638.240.00.01.00.81
222026-07-23isolation bedsjoint165.23.330.0813.160.820.622.4742.730.00.01.00.64
232026-07-23isolation bedstreatment179.344.050.1365.3missingmissing58.350.020.991.01.0-0.39
242026-07-25isolation bedsjoint198.171.990.1281.911.771.3323.9374.240.00.01.00.78
252026-07-25isolation bedstreatment155.521.120.0961.43missingmissing55.490.030.01.01.00.02
262026-07-01recoveredjoint410457.7153.970.5391.89missingmissing10409.148.570.01.01.00.29
272026-07-06recoveredjoint356.790.970.41.1missingmissing39.820.016.961.01.0-0.37
282026-07-08recoveredjoint344.341.180.2881.48missingmissing29.460.014.871.01.0-0.4
292026-05-18reported casesjoint1231.140.481.870.3missingmissing61.150.0169.990.01.0-0.66
302026-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), scored the same way as the cross-release forecasts above and against the same persistence baseline. Only the joint model is scored here, so no individual single-stream fit appears in the tables and figures below. The May cut-offs predate the first reported bed occupancy and the first reported recoveries, so those windows are left unscored rather than scored against a series that had not started. The baseline carries a weaker data-vintage guarantee than the cross-release one, since its snapshot was taken weeks after the frozen cut-off and can hold later revisions to earlier days (see forecast scoring against a persistence baseline).

Load and summarise the frozen-fit forecast scores
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)
34×13 DataFrame
Rowmade_datestreamncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
12026-05-20confirmed cases24267.30.740.3740.25207.9159.390.00.751.00.44
22026-05-23confirmed cases24181.870.440.2230.17158.2422.650.981.01.00.06
32026-05-27confirmed cases24288.770.560.8390.5554.190.0234.580.00.75-0.82
42026-06-08confirmed cases24660.924.690.4452.23357.78303.140.00.251.00.61
52026-07-16confirmed cases2141.260.40.1590.3139.351.420.491.01.0-0.0
62026-07-18confirmed cases2143.30.810.1480.63135.987.320.01.01.00.11
72026-07-19confirmed cases1108.671.540.1611.1389.3819.290.01.01.00.26
82026-07-20confirmed cases174.090.750.1190.6374.090.00.01.01.0-0.0
92026-07-24confirmed cases1101.342.520.1461.9989.3511.990.01.01.00.26
102026-07-25confirmed cases1110.543.360.1622.8389.1521.390.01.01.00.29
112026-05-20confirmed deaths2434.230.350.4010.1920.180.014.050.881.0-0.34
122026-05-23confirmed deaths2442.060.350.4880.2319.730.022.340.251.0-0.56
132026-05-27confirmed deaths24122.120.831.7380.895.910.0116.220.00.0-0.96
142026-06-08confirmed deaths2481.760.610.230.3579.21.940.621.01.0-0.02
152026-07-16confirmed deaths2242.531.672.1655.338.110.0204.420.01.0-0.84
162026-07-18confirmed deaths2217.243.322.1412.3943.290.0173.950.01.0-0.79
172026-07-19confirmed deaths1187.26.372.81324.9615.70.0171.50.01.0-0.83
182026-07-20confirmed deaths1216.884.643.34919.5711.190.0205.690.01.0-0.83
192026-07-24confirmed deaths1198.511.193.76760.616.330.0192.170.01.0-0.87
202026-07-25confirmed deaths1182.46.263.59535.057.610.0174.790.01.0-0.81
212026-06-08isolation beds2477.470.460.1580.4140.021.2836.180.51.0-0.22
222026-07-16isolation beds261.652.470.0772.2715.2946.360.00.01.00.62
232026-07-18isolation beds275.582.440.0972.2913.062.580.00.01.00.68
242026-07-19isolation beds196.274.770.1224.4414.1682.110.00.01.00.81
252026-07-20isolation beds186.124.940.1084.5811.5374.590.00.01.00.75
262026-07-24isolation beds182.114.430.1014.16.1875.930.00.01.00.89
272026-07-25isolation beds1152.282.80.1962.668.35143.930.00.00.00.92
282026-06-08recovered24114.081.020.4520.3289.3324.750.01.01.00.42
292026-07-16recovered265.281.110.491.3759.420.05.861.01.0-0.16
302026-07-18recovered273.573.210.4312.971.781.80.01.01.00.11
312026-07-19recovered149.324.030.4524.7448.630.690.01.01.00.09
322026-07-20recovered145.565.440.3995.4745.090.470.01.01.00.07
332026-07-24recovered142.292.090.3572.2940.631.660.01.01.00.17
342026-07-25recovered146.942.610.3262.0646.010.930.01.01.00.09

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

4×12 DataFrame
Rowstreamncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
1confirmed cases104616.361.80.3830.36345.92270.40.050.421.00.47
2confirmed deaths104151.211.251.2090.7677.221.7972.20.621.0-0.27
3isolation beds3292.890.70.1520.5128.1737.5827.130.120.940.17
4recovered32105.871.160.4340.3988.1416.990.731.01.00.23

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
16×13 DataFrame
Rowstreamhorizonncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31Int64Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
1confirmed cases73093.511.280.1460.3484.518.830.161.01.00.13
2confirmed cases1426234.540.960.2690.28170.3864.160.00.541.00.39
3confirmed cases2124708.981.610.5440.4324.41384.570.00.01.00.75
4confirmed cases28241590.952.310.6410.38884.35706.60.00.01.00.71
5confirmed deaths730199.338.233.214.7511.290.0188.040.01.0-0.85
6confirmed deaths1426117.91.680.6690.4946.080.071.830.621.0-0.32
7confirmed deaths212491.960.590.2540.1290.691.270.01.01.00.05
8confirmed deaths2824186.40.720.2480.1179.926.480.01.01.00.18
9isolation beds71286.52.250.1091.1710.775.810.00.00.830.78
10isolation beds14864.441.370.1060.8127.8236.620.00.51.00.43
11isolation beds21696.710.380.1940.3444.840.051.860.01.0-0.56
12isolation beds286139.770.440.260.3946.930.092.840.01.0-0.66
13recovered71245.663.750.412.7143.290.921.461.01.00.05
14recovered14875.241.20.4190.4168.825.660.751.01.00.2
15recovered216120.110.840.4450.2390.2529.860.01.01.00.43
16recovered286252.871.080.490.21201.4951.380.01.01.00.42
Scores by frozen cut-off
34×13 DataFrame
Rowmade_datestreamncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
12026-05-20confirmed cases24267.30.740.3740.25207.9159.390.00.751.00.44
22026-05-23confirmed cases24181.870.440.2230.17158.2422.650.981.01.00.06
32026-05-27confirmed cases24288.770.560.8390.5554.190.0234.580.00.75-0.82
42026-06-08confirmed cases24660.924.690.4452.23357.78303.140.00.251.00.61
52026-07-16confirmed cases2141.260.40.1590.3139.351.420.491.01.0-0.0
62026-07-18confirmed cases2143.30.810.1480.63135.987.320.01.01.00.11
72026-07-19confirmed cases1108.671.540.1611.1389.3819.290.01.01.00.26
82026-07-20confirmed cases174.090.750.1190.6374.090.00.01.01.0-0.0
92026-07-24confirmed cases1101.342.520.1461.9989.3511.990.01.01.00.26
102026-07-25confirmed cases1110.543.360.1622.8389.1521.390.01.01.00.29
112026-05-20confirmed deaths2434.230.350.4010.1920.180.014.050.881.0-0.34
122026-05-23confirmed deaths2442.060.350.4880.2319.730.022.340.251.0-0.56
132026-05-27confirmed deaths24122.120.831.7380.895.910.0116.220.00.0-0.96
142026-06-08confirmed deaths2481.760.610.230.3579.21.940.621.01.0-0.02
152026-07-16confirmed deaths2242.531.672.1655.338.110.0204.420.01.0-0.84
162026-07-18confirmed deaths2217.243.322.1412.3943.290.0173.950.01.0-0.79
172026-07-19confirmed deaths1187.26.372.81324.9615.70.0171.50.01.0-0.83
182026-07-20confirmed deaths1216.884.643.34919.5711.190.0205.690.01.0-0.83
192026-07-24confirmed deaths1198.511.193.76760.616.330.0192.170.01.0-0.87
202026-07-25confirmed deaths1182.46.263.59535.057.610.0174.790.01.0-0.81
212026-06-08isolation beds2477.470.460.1580.4140.021.2836.180.51.0-0.22
222026-07-16isolation beds261.652.470.0772.2715.2946.360.00.01.00.62
232026-07-18isolation beds275.582.440.0972.2913.062.580.00.01.00.68
242026-07-19isolation beds196.274.770.1224.4414.1682.110.00.01.00.81
252026-07-20isolation beds186.124.940.1084.5811.5374.590.00.01.00.75
262026-07-24isolation beds182.114.430.1014.16.1875.930.00.01.00.89
272026-07-25isolation beds1152.282.80.1962.668.35143.930.00.00.00.92
282026-06-08recovered24114.081.020.4520.3289.3324.750.01.01.00.42
292026-07-16recovered265.281.110.491.3759.420.05.861.01.0-0.16
302026-07-18recovered273.573.210.4312.971.781.80.01.01.00.11
312026-07-19recovered149.324.030.4524.7448.630.690.01.01.00.09
322026-07-20recovered145.565.440.3995.4745.090.470.01.01.00.07
332026-07-24recovered142.292.090.3572.2940.631.660.01.01.00.17
342026-07-25recovered146.942.610.3262.0646.010.930.01.01.00.09

The frozen forecasts made at each cut-off against the value observed since, one panel per stream and horizon, the observed value in black. Each panel carries the frozen forecast and the persistence baseline, coloured as in the cross-release overlay above, and the x-axis is the cut-off each forecast was made from. The same per-panel axis crop and overflow marker applies here.

Frozen-fit forecasts-versus-now overlay
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), reusing the frozen-fit helper defined above.

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

The same cross-release forecast scoring as Forecast scoring across releases above, filtered to each stream's own individual single-stream fit rather than the joint, against the same persistence baseline. Recovered has no individual fit, so it does not appear here. forecast_score_overview_table and its by-horizon and by-release counterparts already carry one row per (stream, fit) for every fit scored, joint and individual alike, so this section reuses those same tables filtered to the individual-fit rows rather than building its own. As noted in Forecast scoring across releases above, the releases that carry the current model's own individual-stream forecasts are too recent for their targets to be observed yet, so every table and figure in this section is currently empty; it will populate once those targets resolve.

Individual-fit rows of the cross-release scores
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", :]
6×16 DataFrame
Rowmade_datestreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
12026-07-23confirmed casesconfirmed1100.252.40.2012.76missingmissing86.60.013.651.01.0-0.24
22026-07-25confirmed casesconfirmed1107.93.270.1983.43missingmissing107.740.00.161.01.0-0.02
32026-07-23confirmed deathsconfirmed_deaths1110.135.380.2814.11missingmissing82.5727.560.01.01.00.33
42026-07-25confirmed deathsconfirmed_deaths156.362.260.2012.26missingmissing53.832.530.01.01.00.11
52026-07-23isolation bedstreatment179.344.050.1365.3missingmissing58.350.020.991.01.0-0.39
62026-07-25isolation bedstreatment155.521.120.0961.43missingmissing55.490.030.01.01.00.02
3×15 DataFrame
Rowstreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
1confirmed casesconfirmed2104.082.790.1993.06missingmissing97.170.06.91.01.0-0.13
2confirmed deathsconfirmed_deaths283.243.670.2413.06missingmissing68.215.050.01.01.00.22
3isolation bedstreatment267.431.960.1162.5missingmissing56.920.0210.51.01.0-0.18

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
3×16 DataFrame
Rowstreamhorizonfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31Int64String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
1confirmed cases7confirmed2104.082.790.1993.06missingmissing97.170.06.91.01.0-0.13
2confirmed deaths7confirmed_deaths283.243.670.2413.06missingmissing68.215.050.01.01.00.22
3isolation beds7treatment267.431.960.1162.5missingmissing56.920.0210.51.01.0-0.18
Scores by release
6×16 DataFrame
Rowmade_datestreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
12026-07-23confirmed casesconfirmed1100.252.40.2012.76missingmissing86.60.013.651.01.0-0.24
22026-07-25confirmed casesconfirmed1107.93.270.1983.43missingmissing107.740.00.161.01.0-0.02
32026-07-23confirmed deathsconfirmed_deaths1110.135.380.2814.11missingmissing82.5727.560.01.01.00.33
42026-07-25confirmed deathsconfirmed_deaths156.362.260.2012.26missingmissing53.832.530.01.01.00.11
52026-07-23isolation bedstreatment179.344.050.1365.3missingmissing58.350.020.991.01.0-0.39
62026-07-25isolation bedstreatment155.521.120.0961.43missingmissing55.490.030.01.01.00.02

Outbreak size estimated by each data stream

Each data stream constrains the latent outbreak size differently. The table below puts the posteriors over the infection count side by side, the single-stream fits and the joint, to show what each stream implies on its own and what the joint adds.

Per-stream infection-count table
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,
    "joint" => posterior_C_joint);
6×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1exports3274.011016.020297.082547.0199032.01.2919e6
2deaths (DRC)16944.022392.026597.036563.044756.065293.0
3cases (DRC)20511.022871.025237.032286.041152.070518.0
4confirmed (DRC)28279.034848.040770.052675.066424.0101928.0
5isolation (DRC)6910.08113.09080.011573.014505.025317.0
6joint5702.06375.06907.08028.08993.011219.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)];
    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,
    "joint" => posterior_C_joint;
    scenarios = [], xmax = density_xmax);

Estimate evolution across releases

How the outbreak-size estimate has moved as situation reports accrued, three series on one calendar axis. The estimate published at each release is in blue, drawn as a median with nested 30/60/90% interval bars because each release is its own fit rather than one continuous model. The current model frozen at earlier cut-offs is in red, reusing fits already made for the McCabe and Chamla comparisons and the forecast validation. The current model on current data is the green band, drawn day by day so the latest estimate reads against the earlier points, and dotted vertical rules mark the release dates. The published series switches from a closed-form integral model to a renewal model on 7 June, so a step there can reflect the change of method rather than of data.

Released estimates and the current-model frozen re-fits
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)];
    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,   is marked, and 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", "exports"]
_fit_labels = Dict("joint" => "joint", "cases" => "cases (DRC)",
    "deaths" => "deaths (DRC)", "confirmed" => "confirmed (DRC)",
    "confirmed_deaths" => "confirmed deaths (DRC)",
    "treatment" => "isolation (DRC)", "exports" => "exports")

# Group a per-fit estimate table into the label => tuples pairs the faceted
# plot takes, keyed on the date so the mixed release tag shapes
# (`results-v1.9.0` and `results-1243`) never reach the axis.
function _fit_groups(df)
    return [get(_fit_labels, f, f) =>
                [(string(r.date), r.median, r.lo30, r.hi30, r.lo60, r.hi60,
                     r.lo90, r.hi90) for r in eachrow(df) if r.fit == f]
            for f in _fit_order]
end

# Per-fit reproduction-number trajectory, reconstructing the walk exactly as
# `plot_rt_streams` does per stream.
function _stream_rt_trajectory(chn, dates; rt_start, rt_walk_start)
    mat = reconstruct_rt(chn; n = obs.n, breakpoint = _BREAKPOINT,
        rt_start = rt_start, rt_walk_start = rt_walk_start,
        ramp = RT_INTERVENTION_RAMP)
    first_date = isempty(dates) ? obs.seeding : minimum(dates)
    first_day = clamp(value(first_date - obs.seeding) + 1, rt_start, obs.n)
    days = first_day:obs.n
    ds = [obs.seeding + Day(d - 1) for d in days]
    q(d, p) = quantile(collect(skipmissing(@view mat[:, d])), p)
    (ds,
        [q(d, 0.35) for d in days], [q(d, 0.65) for d in days],
        [q(d, 0.20) for d in days], [q(d, 0.80) for d in days],
        [q(d, 0.05) for d in days], [q(d, 0.95) for d in days])
end

# The single-stream chains and their renewal-walk starts, keyed on the fit
# id the per-release tables use. Both the joint walk start and the day-1
# per-stream starts are the ones the per-stream implied-Rt figure above
# uses, so the bands here match it. Confirmed deaths has no trajectory
# here: its panel still draws its release points alone.
_stream_chains = (
    "joint" => (; chn = chn_joint, rt_start = _rt_start_plot,
        rt_walk_start = _rt_walk_start_joint),
    "cases" => (; chn = chn_cases, rt_start = 1, rt_walk_start = 1),
    "deaths" => (; chn = chn_deaths, rt_start = 1, rt_walk_start = 1),
    "confirmed" => (; chn = chn_confirmed, rt_start = 1, rt_walk_start = 1),
    "treatment" => (; chn = chn_treatment, rt_start = 1, rt_walk_start = 1),
    "exports" => (; chn = chn_exports, rt_start = 1, rt_walk_start = 1))

# Build a fit label => trajectory dictionary from a per-release table,
# restricted to the fits `_stream_chains` names. A fit with no row in `df`
# gets no trajectory, so its panel still draws its release points alone.
function _rt_trajectories(df)
    trajs = Dict{String, Any}()
    for (fid, cfg) in _stream_chains
        fdates = df.date[df.fit .== fid]
        isempty(fdates) && continue
        trajs[get(_fit_labels, fid, fid)] = _stream_rt_trajectory(
            cfg.chn, fdates; rt_start = cfg.rt_start,
            rt_walk_start = cfg.rt_walk_start)
    end
    return trajs
end

rt_stream_df = _release_data("rt_by_release_by_stream.csv",
    _by_stream_schema)
rt_stream_fig = plot_evolution_by_group(_fit_groups(rt_stream_df);
    trajectories = _rt_trajectories(rt_stream_df),
    ylabel = "Reproduction number",
    title = "Reproduction number as data accrued, by dataset",
    released_label = "Released estimate (per release)",
    refline = 1.0,
    empty_note = "No per-dataset reproduction numbers saved yet.");

Basic reproduction number by release

The basic reproduction number 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, and 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, each release a median with nested 30/60/90% interval bars, and   is marked. 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, with a corrected set of case-fatality ratios. McCabe's scenarios estimate cumulative cases at their report dates, though their report is not fully explicit about whether this is symptomatic cases or all infections. We take the like-for-like quantity to be our cumulative symptom onsets, the symptomatic cases, on the same dates, rather than the latent infections (which include the not-yet-symptomatic) or our current cut-off total. We read our value off the joint fit's cumulative-onset trajectory at the grid day for each report date and show it with its credible interval, the 18 May report against our 18 May value, the 20 May update against our 20 May value, and the 27 May Lancet publication against our 27 May value, so each scenario sits beside our estimate for the date it was made.

McCabe scenarios with uncertainty against our estimates
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, anchored on the 598 confirmed cases reported by 8 June, then run forward to project the confirmed-case trajectory under a low, central and high transmissibility scenario.

Their published quantity is the cumulative confirmed-case count, with the reporting fraction held at one, so it does not adjust for the cases that are infected but never laboratory-confirmed. This is a different quantity from the cumulative cases this analysis and McCabe et al. estimate, which include the unconfirmed and unascertained, and it sits below them: it is a floor on the true size rather than an estimate of it. The like-for-like comparison is therefore against our own confirmed-case projection, not against our cumulative infection count.

We compare forward projections rather than refitting to their assumptions. We take our fit frozen at 8 June, the exact date of their confirmed-case calibration anchor, and roll its confirmed-case stream forward to the dates Chamla report with the same machinery as the one-week-ahead forecast. Setting our projection, their projection and the confirmed cases observed since on one timeline shows how each projection has held up against the data.

Project the 8 June fit forward and assemble the Chamla comparison
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)679 (621–815)676
224 June990 (709–1293)1690 (872–4098)3748 (23 June)

Beyond the comparison window their central scenario continues to roughly 8200 confirmed cases by mid-September, with the high scenario far higher; those longer projections are not set against data here. Their confirmed-case projections are a floor on the outbreak size, so they sit below our cumulative infection count, which adds the unconfirmed and unascertained cases on top.

Reproduction number behind the projection

The forward projection above is carried by the reproduction-number trajectory our 8 June fit estimated, a quantity we report in its own right rather than as a comparison. The figure shows that trajectory, the time-varying reproduction number from the renewal walk with its credible intervals, as the fit saw it at 8 June. It declines over the weeks leading to the cut-off, and that decline is what bends the projected trajectory away from sustained early growth.

Reproduction number as estimated by the 8 June fit
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, which is shorter (implied mean about 8 d). Both pathways come from the line list, so this varies the actual delay assumption rather than an arbitrary scenario. The re-fit uses the full headline settings (1000 draws across two chains).

The infection count to date shifts with the assumed delay, and the table and overlaid densities below show how far.

Re-fit the joint under the community-pathway onset-to-death delay
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}:
  5085.764277041447
  7673.963621376773
  8902.557707649876
  8034.562198542198
  6894.031846318285
  6620.920339964601
  5887.7625698318625
  7001.645667734614
  8187.990681132155
  7978.19368957122
  9223.693788992541
  4782.239104326803
  8254.833232628765
  5021.972417547306
  5602.6938471782105
 13050.088348597446
 12124.55348529482
  6100.155308429465
  6716.31504967074
  6852.74012541257
  9188.37505805805
 10556.853268231955
  5711.8965647055165
  6014.8884256357605
  8759.969865105373
 10171.207760807578
  6461.524549669196
  9306.189528622504
  6929.902019442229
  5865.342926984478
  9663.500139571623
  6459.584951348632
  7666.2943892909125
  9535.726963909396
  6706.733520840806
  6787.841592651661
  6376.799974707088
  6246.127824486882
  5525.084725135105
  7946.786995907042
  6694.148299894555
  6099.092989556827
  6893.726221970348
  5396.117900905476
  5281.948122703548
 11553.682838288505
 11268.085226334768
  7397.48969074769
  6023.879439362502
  4984.62132054618
 11466.904248915262
  5424.619007865526
  6935.3906152804775
  8434.806822798228
  5840.972559770786
 10807.579464090477
  7372.311149420273
  7982.954361592162
  5478.1495752002875
  6497.729088562753
  7110.456640827987
  6365.26730809987
  6311.891606460384
  6829.477988853286
  8160.311652768606
  6819.390821457161
  7939.76492466428
  7402.854666790536
  4898.776570885539
  5903.848144555923
  6450.875285094536
  5472.636881051579
  8921.87414321292
  9165.8427524154
  8698.509451640619
  6554.302739041052
  9450.24678695783
  7231.007144822909
  9615.541580380248
  7019.601138411541
  7462.2578264819
  6799.621509689932
  5545.5588192245505
  7706.636233253211
  7804.3592947637735
  6727.090689005259
  6218.980442304035
  8264.328652849454
  9095.953179977343
 13190.492333658514
  7699.378063101561
  8227.838718108436
  9247.757592163056
  6810.9013802645395
  8413.76181761812
 10187.938898860652
  9259.476508033533
  9326.165402372453
  9230.751379449031
  7352.389136962112
  9227.377683262048
  6864.629065223225
  8005.038289683583
  5939.678940396823
  8281.741045993418
  7996.724860595969
  7910.667635626672
  8351.407371552978
  5477.386227933329
  8849.459875069806
  8316.490814525883
  8041.427944932847
  6899.968390766624
  6260.932353725043
  5620.655859622982
  6645.664427332685
  6585.648907077271
  5697.623185662401
 10198.802281313516
  7865.38906220158
 10272.313024169303
  9064.975080426633
  8850.33724449608
  8411.218140494131
  7156.234289593954
  6886.713121446636
  9852.017308501301
  7749.419286294567
  7477.261115294918
  9345.068431625832
  8724.228201260903
  6671.023075568173
  6806.548982576114
  6013.748462652946
  6090.324871677533
  7241.374348470296
  7291.784819839767
  7071.239055349244
 10021.876455038786
  7815.658155527944
  6433.603787612
  6157.31915198217
  8216.464575566235
  6371.272973081318
  6298.1850984246175
  5679.096359671527
  7923.379643064593
  6486.617122871047
  7776.364002837831
  6076.572009400463
  6540.462722629988
  9620.15096133327
  8784.386200952174
  6756.249605516079
  6216.537541519782
  6083.1439395538255
  7034.433361661072
  5371.7109759089435
 15223.096817157524
 12224.50302841146
  8911.835940751605
  8293.181329231811
  7635.331579051027
  8530.500020519237
  7533.550505910127
  7331.795014004651
  7835.203937032433
  8581.900892107278
  7739.089527737374
  7737.732112826173
  6904.004573808474
  6548.536149106926
  5893.839220506387
  6181.687404063634
  6520.920860867318
  6310.561452207391
  5257.095119762319
  5760.575052353627
  5654.402716331439
  5608.821981149695
  5842.453642361094
  6538.114795615964
  4857.023565009406
  7817.790983614928
  6270.340045909065
  7370.7836004634
  6371.249166240385
  5888.809559475787
  6671.234026844699
  7171.976170959075
 10273.957057767073
 10057.79443067402
  7401.9019312211385
  7654.64784420306
  7772.383610945956
  6724.817050574804
  6709.65324935328
  7602.4645636546975
  5091.805474280677
  6868.527361765733
  5119.1199526618
  7151.136937283445
  7489.916208232246
  7086.965905736227
  8339.759588976664
 12704.627898274563
 10623.887200741734
 12662.687586665052
  9305.603005094183
 10327.955877898714
  6100.48764595897
  6683.614621087247
  7755.7174138693435
  8259.05041514847
  6684.947900778152
  7252.622932835522
  7426.021430069558
  5747.065985193861
  7910.1736072803715
 14557.324295144968
  7103.9533241432655
  7588.266865316251
  7066.073904995324
  6221.309790745404
  7577.188557309794
  7261.08620876743
  9346.328613128358
  6652.0176800739155
  8056.437198080332
  5661.463771698581
  9296.317043311457
  5956.671733513147
  6211.493477008057
  4963.454778388979
 11263.385308155233
 10200.570899422846
 13908.95116546956
  6771.98656286568
  6036.26389079505
  6514.777448036506
  7389.25487971704
  4735.939889296867
  5286.158515582968
  7156.4836892286
  7077.739204375678
  8891.462870894125
  6735.675095811435
  6464.16845973751
  6361.733736124339
  8297.501005792838
  7620.005611492871
  6351.682369629641
  6099.370554041053
  5101.065452294655
  7355.8920473394255
  7839.177033973527
  7306.9866090735095
  5496.961425856974
  7463.46273742186
  6736.922672059665
  5390.230305292231
  8987.433123282035
  6006.219451185602
  6302.746536497704
  5995.878995970792
  5785.8867390325595
  6153.563804051773
  9174.72552564542
  8656.510322811691
  8166.254429994441
  6044.11001534716
  5708.858003729163
  7432.896069047052
 12363.845340388318
  7966.205640576813
  7978.247359710813
 10243.539529021531
 10919.434454564793
 13940.510137893885
  9904.850441269075
  8274.061357147752
  6796.222349049831
  7312.275995613671
  6846.9807470358355
  6050.19192167438
  7636.204994970376
  9464.366317637625
  8743.72860552336
  5926.202518218319
  7530.136988924911
  6385.297992113514
  7712.398046934231
  9309.47242486598
 10685.294364862444
 11922.332787751673
  5746.816711952252
 12658.101224604332
  8870.069084081739
  7558.186550685755
  8003.231441673092
  9619.573672516086
  7149.906249395242
  5225.150496071548
  6640.2023975749335
  5696.656731606194
  4916.553143778209
  9642.816741727058
  8389.408240618592
  5564.265363592901
  5212.329162330244
  5067.472351396241
 11437.794572547817
  5422.057890536032
  6206.614121994366
  7309.424861752951
  8130.562502250385
  5662.120632604549
  7396.054068416679
  5980.639970575235
  6274.501324957871
  5723.657997341566
  7370.236939200058
  7260.621464033194
 10314.228470302876
  8545.670616330523
  5974.129283122742
  8779.557819382531
  6629.165776232128
  9457.56406560442
  7420.622165065229
  5834.827392789524
  6307.853495823186
  7913.006877585776
  7441.081669658308
  5199.354453821305
  7340.531583654367
  5668.718481292821
 11746.091023751735
  9273.33467141922
  7281.1507843266745
  7947.760813611295
  6734.315480211152
  7212.420247728269
  6991.5042797860315
  7424.574326093896
  8623.350653886017
  6705.3088304276425
  6546.8082825392
  6665.693652241852
  6281.360528436258
  9311.862932439597
 11075.240582693592
  9275.045584756352
  5359.990723839583
  7685.73392979772
  6852.3122544917505
  8011.158098620021
  7180.42818535842
  7188.789041694924
  7148.9942463172765
  7606.917105688427
  8644.659110261071
  7150.65875617231
  7669.19068541522
  7622.999059257495
 11212.29846380043
  6499.682637998273
  7288.648181906534
  6854.401072585236
  6208.914165961109
  7295.384289137575
  6851.326562290553
  7388.900492168128
 12512.718828117026
  5581.6837364206995
 11282.918102283316
  4866.946539898971
  6022.534954748
  5724.4795107505615
  5723.40823470828
  5972.258242164248
  4929.815922441612
  8861.854261780823
 10113.34183340483
  6263.347842180958
  6999.280274624024
  7392.1121959726715
  6476.0838576288
  5942.254591426381
  6453.033136988629
  8890.55471006002
 10890.62487582867
  8661.947502452998
  8789.172793806458
  8487.681273068267
 10866.07150513631
  5874.556508956971
  5980.752317605461
  7977.875001754171
  6340.882540232122
  8133.774088571156
  5727.962888789454
  8368.621527599309
  8853.123987665384
  7331.164565968214
  6105.489597864812
  5156.444065418417
  6023.4636462017925
  7921.573529270572
  6819.543069439206
  7615.234906557683
  6792.166128306419
  5806.415222927657
  6825.4068488421135
  6554.1801449835475
  8357.950077908466
  8036.946102728122
  6191.331505872691
  6647.264370578989
  7892.652419870402
  7888.900421126032
  9295.775059214498
  8418.857497074914
  7408.550541870847
  7190.874298772002
  7350.057853870685
  5604.655285079767
  7934.877829753083
  6281.746343971563
  7004.544554128428
  7093.5846944553905
  7675.733534352055
  8530.878867175552
  8795.178209014444
  7884.237566548873
 10729.417419523757
  8069.371633533232
  9905.581716210138
  6228.390686281078
  6582.328201522896
  9519.427166644835
  6383.187979978567
  7647.206039767817
  9858.682775430161
  6981.899140998751
  7963.700006459147
  9086.952931721173
  9360.27048821041
  7762.347279228739
  7664.68442819268
  6072.399223182741
  9433.099375930995
 10143.225341545423
  8163.752957974717
 10361.55840813807
  6738.095256048003
  5945.332239650147
  6649.298324731436
  7850.88353540655
  6429.710383827799
  8878.260364957714
 10070.112863134222
  8219.6278779625
  6578.94016884463
 10124.179624419297
  8911.41712975959
  8999.757684757606
  5925.233436122522
  6382.042740289164
  8673.520231943297
  7222.038426589173
  7226.653483813401
  7226.653483813401
  7596.914768019955
  9283.301726279811
  5305.142277315525
  5975.271104987193
  6695.2943885627865
  7935.526947792152
  8394.015002424336
  6463.385746763585
  6636.805103859085
  6609.378298900837
  7850.047835367273
  5994.534059203679
  9768.396712341952
  7132.744651148125
  5494.772232934494
  6007.353624213436
 10133.970662766014
  8565.966748921452
  9944.225084561618
  8739.942795284464
  7004.25953225594
  8007.844194284009
  7062.519941055328
  6368.109246134409
  6079.869637949579
  6391.536863874076
  6391.589780582106
  6304.366591986304
  8984.334021795725
  5508.911961649347
  9739.641919807333
  7633.096438275698
  6508.741227366063
  6563.573137809664
  5436.473463588229
  6927.097370137597
  8866.660702982912
  6438.071477522516
  6988.573775964308
  6805.2727836579825
  7385.799730667772
  5243.006319600218
  7236.068734855731
  6752.809007332811
  5781.022530567722
 10195.535858018
  8209.061480969325
  8928.170935283242
  8111.010688279879
  5945.392533902628
  7990.963421585615
  5405.657139457888
  7399.958022544895
  8027.88653193869
 13622.256903265008
  5462.619004388565
  8449.229808404296
  5129.620370193607
  6669.911998915473
  7747.3022052117485
  8117.531393440335
 12032.461501632573
  5735.360000446959
  9216.905139474471
  6244.402258832264
  9257.457006449567
  6310.343613342844
  9458.778295048121
  7318.705610185246
  5993.233874063001
  5861.031900888526
 13663.78528649396
  5391.185582198811
  5429.641930678428
  7461.442474067464
  6393.978492685509
  8941.596920524276
  8026.48972043595
  6071.969688481912
  5308.730779633757
  8870.520545481515
  8305.30190358521
  6938.326566481419
  8680.060592062848
  7763.652854946681
  7757.490071488903
  7998.252418207945
  7761.597430122187
  5818.227011089184
  6266.553956947976
  6818.473518785098
  7348.02097049035
  6379.874346151311
  7350.843805447296
  7700.869097178031
  4330.035788226813
  8648.645464737592
  9226.172182553231
  5459.554754872294
  6320.3061743274575
  7409.209706817611
  6483.336416647301
  5840.065170428008
  7195.908700700571
  7467.486378093038
 10628.929106309843
  8722.439984251547
  8757.239007307755
  6809.296412871691
  6740.49841479949
  7026.751033304067
  6109.901844630129
  7514.537065426407
  7974.3935198028885
  7405.25587319379
  7313.102943439394
  6205.858285198621
  8037.406852543895
  7057.517436126824
  6964.217648633064
  5600.7978205335085
  6285.300095233112
  9019.335380510096
  7892.606141555845
  8026.526809629915
  7681.908517470814
  8401.081650811018
  5759.735289121191
  6342.369205520035
  6373.788779811576
  6483.350217737376
  6612.794618899878
  7695.11707831906
  8694.183240117038
  5435.367125380937
  9266.353418953933
  7536.259804419798
  7593.582423400249
  7611.454752557852
  5898.335090901296
  6140.663858422136
  6696.412238593277
  7717.29252526889
  6538.254404394207
  6900.300345806605
  7114.3580903843995
  7180.862742191815
  7771.536249965809
  5761.130448414906
  9167.437254720247
  5942.7930079389635
 10613.353073086684
  5558.256179424453
  6216.607562567499
 11062.93673958564
  6623.344887613885
  8755.504642862801
  6098.077564793534
  7184.794256828907
  6340.254269132117
  6537.859353471435
  8036.149650216374
  7664.683606692115
  8252.311951695961
  7883.745646182046
  6182.152459100931
  5709.6035942390845
  6340.59187220484
 11662.568577422355
  7653.801591615198
  7872.744349562595
  8322.150374818515
  9111.97184923511
  6566.393190033129
  6360.015937035304
  9674.575766697431
  7898.705734962825
  7156.670562760317
  9343.150171598743
  5735.180734986813
 11801.131714977082
  7235.956320101653
  8382.832344279352
  8104.196891533559
  5922.059910610099
  6614.997918194423
  7997.387703770603
  6503.933773414385
  7901.2702540272685
  7687.580825640972
  7460.060092529913
  8341.815834740119
  8340.576443467913
  8104.099201099187
  6400.00570726317
  7357.960434243624
  4949.010335520774
  6493.4001579047135
  7367.807158547165
  5970.606571330246
  8389.248870841453
  5561.149711637061
  7482.669735251703
  8270.506787497963
 10278.957914930692
  8554.20412190683
  6939.979565730487
  8778.799713540875
  6710.570519487232
  8792.99665608067
  7468.126267854611
  6953.633091013442
  5626.726818332049
  6273.354902308454
  7698.504593490946
  6458.856582339585
  6902.147986256449
  5350.473991148977
  5656.284374069644
  8430.509667117767
  7493.565864519335
  5675.2695519298195
  6464.255039238453
  7328.884984456688
  6719.805150321427
 10571.54815359989
  6163.461369803919
  8886.058779597432
  6264.111509614985
  8449.950112005992
  7102.774379205737
  5665.756431333502
  7746.802311807591
  7593.232962555895
  5509.521247910688
 12311.421687410317
  8255.995978209261
  9119.92082035031
  4859.525514932333
  5507.565590476053
  6379.445174320514
  9438.517814683952
  9236.780971296299
  6795.531781395189
  7334.264500060185
  9274.38381751156
  5667.240106786474
  6095.863808994612
  8905.75525542341
  7179.106865165063
  6555.307647750417
  6349.530370255143
  6958.802558427163
 12196.714741083257
  5989.321092525023
  8982.607060663
  6597.59362791893
  7493.6110308564275
  9691.30365829319
  8086.839134154294
  5782.238321351221
  7265.151009477697
  7816.615999421929
  5378.38931329166
  5812.8714639089285
  6399.345846346883
  9533.374995309245
  5985.155475756013
  6768.589534692804
  7822.928331065243
  7776.066874763652
  7906.697493436891
  7897.129771515444
 12442.021119433015
  8782.098012017765
  6818.047278006512
  6528.960203070528
  8239.251043061056
  7430.95449294713
  8649.327146324016
  6647.2833904762
  6757.738329984554
  6952.4797393025165
  7017.245659126435
  7289.336956659633
  7068.223005549205
  6666.856922290748
  6924.353029206856
  6842.294443196298
  6180.8349795979575
  7930.576026961762
  6129.095745606904
  8586.633801209586
  5833.2047191714755
  6446.68049995113
  5868.083174525071
  7751.9050278025925
  5936.886011236182
  8502.003412325399
  5324.643086056082
 11667.842245877495
  5977.718100544273
  6576.349930933788
  6237.291722505983
 10978.321077614897
  5432.751280688994
  7966.8946337988655
  7469.178873531358
  8245.227954668966
  6406.051470761342
  9395.09380133198
  7545.667460284665
  6232.315595669647
  8559.5371571972
  6075.05034387856
  7180.704598910583
  6577.934530507426
  5454.951251722502
  5733.3659463633
  7301.940451174974
  7525.620264063137
  8174.023391120492
  7908.843854478403
  7038.87039885012
  6255.279745076499
  8744.079069640502
  6848.884440388302
  6252.702510824235
  6552.034938175545
  5811.172410155017
 14017.3730800716
  7179.381731085018
  7045.106593852267
  6228.687732647556
  8103.183180284863
  8130.876353630824
  7603.039542458257
  7689.082983725562
  8171.503167465774
  9255.877688901368
  7571.99010558569
  6415.165956769735
  6386.885358867847
  6729.150061152102
  8562.130323386924
  6863.902852710513
  7417.880504003892
 10018.329381961852
  6582.412715701524
  7815.585921485077
  8204.000426982173
  8808.855162060076
  5958.644540091534
  8151.684724315546
  5044.118700753443
  7432.079046505756
  5412.22495673037
  7803.031627827975
  5897.182599770347
  5368.350922716104
  8228.698606870814
  6394.786761122341
  6902.538159008034
  9592.357789832997
 16308.210641646869
  5559.687961278926
  7240.7926944356095
  6628.799636599984
  5873.194175422088
  7816.019446173237
  8131.720834045002
  8223.89768085475
  6780.6422012231
  8210.308114881982
  6021.595792314834
  6452.886259669066
  6686.363367280727
  6339.539518289189
  8357.900514547295
  6895.36131240174
  7451.95232165978
  6780.135546670269
  6826.556623158148
 10178.670696576646
  8333.836120303455
  5591.2399741754225
  6822.319627602674
  6460.875847002663
  7496.474099413723
  8950.733952614457
  8964.461572767568
  7511.668567124795
  6495.466730563741
  6171.530183227284
  9929.414216382642
  7113.887801839509
  6507.378908502863
  8230.530142227233
  9491.959169227384
  8214.401563249465
  7326.1065963380115
  8189.145427135971
  4890.441529715784
  4895.372396523806
  6626.987989528923
  6645.39046309611
  7504.1400470211465
  7063.266724512207
  6440.287179893076
  4751.699899155006
  5165.50779947095
  6099.899104815303
  6765.266257780052
  4807.1751994764545
  7847.102441475443
  7944.02882154952
  5686.478484794335
  9308.092394395993
  8214.251384392544
  9044.770721532963
  8349.459342428108
  6299.640118137821
 11404.823603469571
  8269.014070399273
  5426.450835296715
  5730.977233093415
  6527.0938727145285
  5978.500033419869
  8226.24359414678
  6784.47454867366
  5092.112053451978
 10188.806623600658
  5452.888997887396
  5937.560149471831
  6186.652121602099
  8989.852020563194
  8583.42305891524
  5769.99501571036
  6936.929183235013
  7705.777230157384
  7198.5476544660905
  5843.235642312514
  7369.115852049974
  6621.999683437474
  7276.9621661209285
  6803.699243221332
  8723.98527191929
  9689.169569667067
  8120.48641418952
  7992.549813409023
  7711.125436852071
 10697.349801146313
  6232.022392778512
  9016.532730389365
  9935.665261924134
  7595.795192362808
  6841.303047535638
  7896.14999446235
  7626.271070300891
  5868.836114148345
  8402.271507257201
  6332.747758215398
  6879.756235939616
  6250.393104066803
 10394.13621547864
 10051.23151377191
  8803.319180065442
  6583.270840142833
  6085.919895955987
  7775.9437406933475
  8624.632516389695
  5574.496964716487
  6930.704838177386
  8705.521243582743
  4810.997414593621
  9182.058843398128
  6866.916303870327
  7116.261335368631
  7113.303444903821
  7648.250160630081
  6920.4336095101835
  7227.376911650629
  6165.94177782371
  7789.738411950441
  6117.6836534777085
  6500.793802570523
  8780.451323448233
  6062.536942308842
  6331.547217062481
  7711.014008356504
  7617.421280027841
  7856.017523068377
  6417.070004672004
  8591.832606740994
  6336.783868868786
  9611.781586843368
  9295.369169174475
  7387.1616928649555
  9094.268019943689
  8401.292504540485
  8822.374129664477
  8026.347597332491
  6455.433500872523
  6220.7159135080465
 12299.766439756364
  7445.737908588587
  6927.677860376865
  8313.531302763216
  6362.4357352125935
  8605.212662930115
  7335.91904403441
  7645.222294871501
  5471.883438115985
  6333.869927643756
  6020.874577847515
  6368.166658386857
  5167.722241976639
  5505.469303036447
  6334.966382052718
  6357.160693140214
  6546.193623893993
  8118.039028070018
  6048.705223667392
  6003.665996582916
  8496.833375289021
  6590.859182119258
  6056.505108013587
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)5702.06375.06907.08028.08993.011219.0
2community pathway5424.06151.06639.07820.08645.010402.0
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1baseline (hospital pathway)5702.06375.06907.08028.08993.011219.0
2community pathway5424.06151.06639.07820.08645.010402.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 very 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}:
 138.18298857851585
 136.91583188738045
 148.38952835897027
 137.57015849156588
 157.1499192908903
 136.55597452498114
 137.3870881064551
 144.21945479336793
 141.25350368882206
 139.96031453503943
 140.15345044458599
 152.68565726565458
 132.5933019617249
 140.26683399866167
 138.55662617675372
 156.7051909667075
 138.30649992522672
 148.1763080229829
 134.34667667145894
 155.79581590117394
 134.95929475926653
 135.4784138251859
 152.51011393919492
 141.67975742120242
 137.5357582835957
 134.2031220264321
 146.653670811794
 139.78251376410245
 135.55813466414205
 140.2167903450625
 162.06418717654324
 145.48591825560777
 143.48298148613065
 145.53527209490483
 152.58270370328793
 137.74783214183407
 140.81302830476207
 146.5589039751056
 133.97939038547548
 136.04053727066986
 154.18446140261034
 139.44850427211742
 140.52853337722365
 145.67530434964482
 143.02963352931368
 143.03350164883398
 133.94720881873323
 136.91610746809877
 150.25040934515116
 140.88947611192833
 135.9653911620049
 148.15635821287412
 142.98747572776972
 135.03652369306081
 145.9942333332643
 141.4699449504244
 149.68340146835786
 138.3156904064394
 154.49454861316352
 145.27076513144448
 139.63326352562015
 146.188293733055
 151.0166612273706
 141.63956934685515
 141.95602093317714
 145.8358444299448
 137.32342357062433
 140.39316125397735
 139.27615476319207
 138.7553276086246
 147.5835865880325
 141.7116031909064
 148.23263237549253
 139.43651479703453
 138.56406179577633
 143.60254737139323
 140.18553807133407
 157.37368544146727
 135.7400314222732
 134.2280914628301
 134.00040033718426
 133.8845692224694
 153.95285302238855
 138.2978113216176
 144.57458439989514
 139.3464513291748
 143.57261261366344
 133.0002540153141
 150.92083636536876
 134.68713617604513
 154.38999494658097
 133.24023169862159
 151.46317074444892
 139.30288000990518
 146.2167600924638
 134.97267406802735
 135.32358417081363
 142.49566508455567
 135.0283574158233
 136.08403266220503
 160.83575965143075
 132.39367001394416
 133.78021842047116
 155.14057870660773
 132.63952889850023
 133.39521649511798
 167.1456978689996
 150.40142910082136
 134.98969020020849
 159.1767366746774
 139.0040087868306
 150.3226207758186
 142.61405127551075
 157.33347680139732
 135.2012145217049
 151.56113488516897
 139.58794153654864
 142.8045772428114
 147.49021647641598
 139.65029331263383
 137.79273156024365
 140.89903446752564
 144.65996567699696
 141.3428437466923
 147.1697011321074
 135.89843945010819
 144.58197114193183
 146.01728791382436
 140.09857046084423
 137.8991596908404
 163.1891656365512
 153.69990176486624
 145.05822082960552
 142.52606416631028
 142.54135618522997
 141.43134839306038
 139.0788420066903
 140.58050461299587
 140.18829090223488
 145.31782601283723
 136.17920762282895
 169.59327946310918
 150.74327012163377
 140.59549944438677
 150.4554648671708
 135.9012997649346
 146.6431250764788
 134.53081802513415
 153.45376955713866
 140.00580261251264
 146.63773602862327
 143.14309068267502
 147.84759470449865
 135.91880620546283
 140.46417610998844
 147.1824612505841
 135.9648237824857
 146.0302784385136
 138.71077872485066
 142.44776190271546
 156.1595685853921
 142.71333220438694
 136.54196394365553
 149.86831607049055
 142.20199373641833
 140.18506973543634
 144.4717911527728
 138.35261423274378
 142.8027635672488
 138.90876762309134
 140.62769303598918
 139.40904285960903
 147.36417206899088
 149.2782598235517
 140.38085756366388
 137.01764687790597
 138.69434520849927
 142.213930698276
 134.01484860924157
 135.3870916802979
 141.75470595151546
 141.9274888920499
 140.4620840901672
 138.038405989229
 143.11592925047648
 136.3570265101921
 136.73143226520565
 137.90905649846079
 138.45544283194724
 144.54458615195517
 149.6717807720237
 134.97406832669768
 133.32430227083458
 156.38339550104112
 142.4201834947038
 138.20486799478468
 141.29859160592596
 146.02264640428123
 142.34936867905446
 159.50236574972223
 133.28727864481056
 146.21127390541812
 142.37636734651406
 136.47290313670442
 156.74431900441965
 148.52130962450548
 146.9649976199919
 139.70316796981086
 148.98706430118162
 141.10739776589446
 145.62426151349038
 136.57188806022927
 150.74202310897047
 134.96420671765486
 142.3652067081267
 139.0495285790527
 149.7256386349195
 145.4836858122757
 144.7300442385785
 143.45958857486576
 138.00168254448002
 142.3185867590743
 134.5169322632746
 156.12859827918206
 147.44173652058657
 134.38066059737284
 162.91743616497678
 143.294522071532
 141.49230639700517
 159.46374368946343
 132.65225629550196
 132.00728718080566
 132.36462395443542
 142.18740408496674
 145.92451550835318
 153.26601958948908
 136.52120048379038
 135.7005993906605
 146.855585984224
 139.40193005779565
 155.1952535706822
 138.15617608029075
 141.49901544984448
 135.7664459616034
 136.7056241946108
 138.92592647620683
 134.84514764047438
 146.76475532523662
 136.01662845002136
 166.949853149045
 157.6099151107233
 137.14723389273462
 148.31027986334863
 136.45463690440093
 141.37847526603707
 138.7258533836099
 144.13493272530974
 139.60154041987678
 135.66095767227426
 139.13966911130632
 147.37526796986356
 136.12481106452984
 142.19682499928388
 147.77537215263692
 134.03615178464187
 154.55289117953916
 149.3970158886978
 132.92016371900223
 133.0877450128496
 132.66896000267786
 166.31616304407575
 135.469025496874
 138.89048753068158
 155.51067337046675
 142.32839013921136
 140.1883830978836
 142.6983495569676
 133.39338032411194
 148.92794324334665
 137.5125583449603
 149.45299168296654
 133.4057387569731
 136.15522330757676
 150.3351125289596
 155.01309744853023
 138.66533871583238
 147.04333941207813
 141.1661445653613
 139.83715887202382
 135.02156555288434
 145.17171292231586
 137.07808484133508
 138.62280503110202
 143.46342511200223
 141.6201514884516
 140.39616321708644
 139.93843621393464
 137.68136613786214
 138.94910891566184
 149.5702443159178
 133.6145600938776
 151.7655259669491
 144.4754034813016
 133.91390706679223
 147.35084879329935
 143.23710463057301
 137.31293539096038
 137.07005916582355
 148.08747255595156
 143.80941411891982
 140.63922766211255
 146.05437330713355
 133.83598177441453
 132.52990684223334
 138.361031298108
 134.88881630239987
 153.57231899652177
 142.4919867110937
 139.55829373236253
 147.8488858777627
 135.7438365145498
 139.66618475330694
 142.78314141751667
 137.68439899668533
 132.48077447992068
 132.4150833180665
 132.80930596185846
 153.20200707105366
 133.36544680257424
 136.38985362834637
 142.29542562749543
 135.4938098066727
 146.6128340733686
 150.48690743522386
 142.31728692061196
 139.70090542991193
 155.45038185574694
 151.55284906957456
 163.6294685347226
 157.1869529726531
 135.28558242312445
 142.87575250991534
 136.95210874481765
 135.47357886292974
 136.54084522303728
 133.5336730758493
 140.4559590736441
 144.91060658004423
 146.4891094436213
 133.87071356840983
 133.00682294319304
 134.4546938867706
 132.29096937683278
 162.26546340251798
 137.10232050317487
 135.73432430392714
 149.50455194180853
 137.87574088916566
 142.51253021585418
 140.8522532958385
 134.23056264479504
 154.24202835404867
 133.8368439516969
 145.06939441243134
 153.08777966620582
 144.9226676781911
 139.07223948614364
 153.85223869149917
 137.16470084064403
 150.97281263243855
 149.64398415157635
 139.11853731916227
 149.45129923199113
 140.48901072693064
 133.97885721433119
 135.81005899037876
 145.65587461125145
 136.56288461683195
 140.44285388894522
 132.7057049723675
 153.30309845604597
 134.20094045519042
 157.82230000360192
 132.47612115513047
 137.53175875931944
 141.92462541087875
 141.76462647112447
 143.2394590993895
 135.7025118760957
 144.30274143259606
 138.1244626831092
 139.55640138541537
 138.12053876181363
 144.7145994601008
 152.56274094324854
 139.84439468783836
 133.8138211742227
 132.234780289268
 140.57049781744726
 139.14149621662662
 147.56978007734958
 134.66937959494825
 137.10656637565992
 159.05108048593007
 142.83051473786966
 149.73931023823732
 140.12086034637858
 139.01771283882232
 137.17720603147708
 153.9678075496257
 137.1701456625488
 149.26329775120163
 145.00331716008216
 137.78754742924767
 135.88983695565025
 137.7173639104562
 152.26259169003168
 136.8408745305863
 132.95865726673
 142.41209124089613
 149.30026524008213
 132.9848132140363
 147.8619524821042
 137.8947308055257
 152.71771875128894
 152.0007270174347
 150.51238465319406
 153.39320943743678
 136.48944373581028
 134.79341989351914
 145.4034555629047
 140.6943898805958
 142.5870723292799
 150.67491460635517
 160.78762500681745
 133.8625919360066
 144.68885956762205
 132.60007942222734
 179.65634226657892
 135.84376919307394
 147.5597486320484
 142.50669095041837
 152.81596959595902
 138.41064901949989
 142.89377260567952
 141.4717451131566
 146.90169987426953
 142.99708069514887
 136.78938700748927
 151.95305471224276
 138.9760876238238
 139.50076922072034
 136.02616348151355
 146.31245627079107
 136.96841093399365
 145.4398910971489
 138.73590001135923
 145.21774017928715
 143.89450580529333
 147.5397178178845
 135.57351254948662
 143.78450692988554
 141.18307160118906
 139.11670529685668
 136.78255736691793
 147.28916421515066
 144.15442553189703
 135.80346889239274
 152.3830104870061
 158.01892371665127
 143.98560908348432
 143.1128062807586
 141.44927440073326
 138.01117644480829
 147.22010366314737
 139.5567226034613
 140.06526823938455
 142.2321944302569
 136.9469495252663
 152.32721765931305
 135.95671457271894
 145.6497433721614
 133.85939009970272
 134.79456613661714
 140.49363112145318
 136.52227666468258
 154.17912770701778
 145.46789133328176
 149.01607562035724
 136.39773812011484
 137.53968085348285
 135.03453412406932
 138.14698417682698
 137.09526317432093
 142.40931513704064
 137.98482680740608
 141.37718480325734
 134.60560087766817
 156.63105547767782
 143.93319226859035
 139.98875575562874
 136.56081784697085
 145.1483690166902
 145.3239592848243
 133.17398309215028
 157.82010441348157
 137.66560202346562
 143.3924453476687
 139.78823269713808
 147.86082627108487
 137.63487939518006
 140.0003616652159
 143.52150360272677
 143.27004554727267
 148.37128927799452
 141.771891768558
 132.36170241475716
 132.71447279658625
 132.13289446954755
 132.34872220957263
 134.8293499728772
 142.93661367763818
 147.4595358261525
 134.38285928529493
 133.21038026257472
 147.9880456085761
 135.92101642227388
 135.97043731003626
 142.88158837722196
 142.89735411528886
 137.26199459496718
 139.85908141203674
 136.43770241074313
 160.68176651340238
 140.38781257081882
 137.56702454569816
 139.53586911443057
 162.31154017737134
 142.5503437827182
 137.82606322091394
 143.39351654085178
 139.14961451128318
 147.53041293709413
 150.02843266788673
 140.4342079719341
 153.8060810393266
 144.8064889058613
 142.14908070717357
 146.61003435806037
 142.7966716181378
 136.8935623627523
 140.08886439020898
 137.01663142035127
 137.44660749256983
 143.54328668185875
 140.56271235445703
 136.0770894785861
 158.65668091943752
 133.8732840441083
 135.0656583228362
 148.10050551568196
 155.80692141475367
 139.8442040018234
 138.34292639138968
 138.34958756686734
 148.27157449336673
 151.1389441459342
 133.87529480481479
 154.85775662163093
 137.9610171944539
 146.48678970327316
 139.18451216951365
 140.27412815475003
 141.5056303868302
 138.36613476251068
 148.53554176858555
 136.32999882595178
 133.63641376168468
 132.6516497985457
 132.83104686766137
 145.70323125084158
 139.099678254994
 136.5699000662754
 139.3913386435759
 143.56091309256544
 139.13127202779015
 139.1865969044055
 136.5478968627637
 142.78570686277655
 147.68161891963948
 150.82432536606387
 133.763266101075
 147.00279106986474
 135.7876056849119
 134.47620169192143
 136.0322996983361
 155.0136424897016
 132.78878482664152
 163.75965348950058
 137.62608140128197
 145.84098948999184
 148.38720919022086
 135.00086486996477
 132.65245290265446
 134.35064497466266
 143.63244737443895
 137.38407126158603
 146.1974661450396
 139.92438729054817
 142.10072574211173
 139.35126837789656
 140.56691299601098
 146.00260266669707
 132.13883384150873
 145.18433242390063
 165.0082002429389
 171.98412155822174
 136.05919200247703
 150.43271686014043
 132.90075308665612
 132.60594486106987
 147.3000878820682
 138.67820845151587
 134.22820421441773
 148.43200854038017
 137.4829039328529
 132.04743460804028
 136.93179611712873
 137.36253275037774
 149.35555948175184
 143.3231155290504
 134.90518774325702
 141.50443043146188
 142.77078461315122
 135.98329495224607
 134.10717784035845
 132.73759561118402
 147.34552360204336
 139.72178062399706
 149.89013581144206
 154.25424853196893
 139.6877014794308
 145.99723768818396
 155.318691811165
 134.914994526005
 142.6611309661181
 137.74430291082382
 143.5727143230035
 135.5288709028415
 159.31111919493992
 148.30879858828828
 140.16777080181924
 140.86946737302995
 146.02383146457612
 138.1982737760411
 132.95500136176508
 144.47265971605205
 138.56713037784093
 141.94201915610316
 134.11128436193675
 148.03743458575346
 133.87368152744378
 157.3544352717064
 145.95761421133201
 136.06435706265157
 137.58910306446285
 138.21047556937447
 138.920724265325
 153.37624170705706
 146.7684397359796
 133.7748831815374
 152.12943611332733
 135.63254591041292
 154.38502040375107
 134.85388128138635
 143.192341265985
 143.30904676737833
 134.40565033074364
 150.12227735325908
 135.5152226379494
 133.38355070116083
 145.25090287195454
 139.47005933321168
 139.11805138722332
 144.9317891430088
 152.7199314096984
 139.16618995884485
 155.43910282968272
 137.13432749363713
 140.70455303201223
 134.49025050676167
 138.28058893152377
 132.91898890734137
 137.78961854074845
 135.92882462167907
 132.20920764617836
 132.29030531139804
 132.74910737888666
 138.43431389830377
 135.1294402983072
 154.3599398009245
 150.97592101863546
 147.06360572779752
 143.87751298527024
 143.21128393573773
 145.97865234154963
 146.53477844415866
 147.46353493150596
 149.33362521730703
 142.67235357129297
 160.76547243767766
 135.87416785135892
 138.71778072147495
 142.0573248285483
 149.65020352592455
 133.2151386121966
 133.02823802403458
 137.45127852541287
 133.95089716351623
 139.02164010295684
 133.4087827803794
 135.25759989754806
 133.53671527366248
 141.03985134748314
 132.35759178388204
 144.5009514033462
 144.0480996549489
 133.50659381479267
 161.30530373385085
 137.6298526499011
 139.08087110514614
 146.14681839887547
 139.7020133867588
 141.52448825359352
 136.427410698648
 134.87862074617706
 141.48139605854004
 140.47456463002374
 135.36867012503964
 135.68664254408546
 152.12227718721326
 142.62220489010724
 140.58705531406108
 147.81498605940834
 145.3869723266837
 138.9754884455423
 137.3620461452437
 152.60548992336578
 144.81334201137037
 140.78257349698046
 148.77963404434857
 144.4634482987918
 135.60490263034754
 138.78121564909426
 141.92829795936652
 136.0750505351589
 138.34521402823464
 150.33386036848873
 139.65066432545734
 141.05652869540646
 143.83337177248615
 144.98443413174357
 144.16459324392483
 136.8745955348415
 139.29996478317167
 147.24333247804546
 148.0857373689608
 150.11280097103565
 134.95311364028245
 135.2311496541091
 143.65143452202284
 142.15748792070926
 141.7124011736904
 141.0366055746455
 146.6338626571871
 135.9415817419579
 138.14866935303908
 133.14628250176497
 132.68233363299797
 135.5498038359873
 149.66070261367813
 143.9147985288551
 143.84704035173388
 141.2922848573009
 147.40780808954224
 140.38349040418905
 150.9235535701223
 136.56346121671876
 140.90406629239567
 133.97413309516492
 132.68130849724514
 134.86898049408325
 146.57908810866368
 133.8531653698285
 141.06752146420868
 142.84910137633423
 143.22148392645906
 144.81482298098894
 140.1956271019424
 137.20666562376647
 145.32419581272234
 150.3359453673898
 139.992658281229
 168.69200172883333
 138.69697482060292
 151.59756677156273
 136.88219764939024
 165.0550424136085
 155.31435052294188
 135.26858075619677
 150.4587111825205
 138.39854924691173
 150.17191885835888
 146.76774087176216
 144.56576976989632
 144.79770546120395
 142.51911676419144
 147.93428788898967
 140.80266232130361
 139.90207433726454
 143.96726087623014
 139.19192734440745
 143.6543472960615
 144.35846348003076
 136.75349585133586
 137.7953850504461
 143.87178535221952
 139.97490809015466
 137.38606802780362
 147.84627987899583
 152.74790685613164
 137.91327332595841
 158.72136382770307
 154.10149404521292
 142.916207388808
 141.6593822170666
 136.40071446187142
 144.96420273281893
 139.49432237307525
 158.40973501487065
 135.04698965884714
 132.34244581497617
 132.38160545846662
 132.1429829993406
 134.67402875809282
 135.3095745965835
 140.00868887227566
 147.15644214103264
 146.68037248877314
 141.68252310269827
 142.52699755931394
 146.733095481625
 132.1768272662142
 153.91977035276182
 138.7527110454845
 148.789533660354
 138.74938062812058
 140.05302622497786
 138.45514839812103
 132.9249784733295
 146.69065204920574
 146.32740416500707
 144.6577774025126
 144.25772243167194
 137.52267326261122
 139.99967446725978
 132.76570953710043
 132.83397700392223
 137.88692421737156
 139.9135367420826
 141.56758406499176
 140.3898049515956
 153.46523866084777
 140.23845435770974
 147.48629132158368
 145.33171304593128
 147.3358355572955
 139.39565305721428
 157.5412840409359
 133.4908677016854
 142.36365701060848
 142.8865240786101
 138.86095527345938
 134.30155321263354
 156.85004496726376
 142.04608166260647
 158.43244629364554
 136.65652126867298
 134.21809602999818
 137.6647276994313
 143.88747291804196
 141.7120809609772
 138.17118150489011
 160.32702566171403
 132.91471250759213
 147.24527954131358
 138.03613008818607
 146.78696605599544
 140.28252372257958
 137.80661238653707
 139.59251770914852
 141.43507706359694
 135.78896997200417
 172.90927938356828
 139.9705445512497
 144.5547824978202
 143.33798109634301
 143.47534540160308
 136.7178650041088
 137.18514135517
 134.8439205330536
 146.24566088886533
 144.77007531047414
 145.31574683453266
 139.49025930596324
 139.96742613261748
 133.72648322286085
 132.52243249626127
 150.7047888922998
 132.54128852413038
 160.167844406659
 135.87100000209884
 150.9916755386713
 144.33925701647155
 142.24247211388277
 146.2239152625379
 147.12302474705143
 149.42128639973424
 149.13179399731183
 140.07186634676842
 137.8896986423577
 136.3039539983141
 141.5256258411044
 154.35668756745793
 141.8445535593225
 137.90557642727308
 137.86252132091974
 149.70123737826344
 152.2822817176749
 147.61088110319457
 139.0774069017167
 137.3223623710762
 141.9793382585639
 135.41640261320967
 136.3240483172738
 135.76870214604375
 141.85174543547913
 141.11981291946577
 139.60527088130328
 138.49785764645407
 145.14735787824614
 143.7682135302426
 144.03003413661287
 145.03612952399507
 150.76117953856158
 147.37887596519016
 137.1337543212457
 147.62174300295277
 135.44364172975182
 168.4359436902153
 138.49490334800964
 137.0467013327258
 147.76384747564217
 136.13637914489422
 136.61679056356508
 134.47119625021375
 135.21232643131196
 147.76702725618645
 138.50140738902155
 150.16854504417222
 159.52976863567696
 146.6177596699052
 142.04421535678986
 144.31354722602225
 143.27279379895504
 142.2262068205196
 149.13153833878533
 134.87173390178395
 134.56625731807523
 143.4872026179759
 138.97372247188235
 160.5483317416155
 141.00312494219764
 138.7329257065384
 135.5143276271026
 138.45588720892758
 140.39016427510862
 136.04950362201123
 135.67961794609835
 139.49279596913073
 138.43520230341986
 144.18402748912084
 135.94224254525963
 136.46343493581395
 134.05150829835955
 132.66354173767192
 135.18801683639884
 133.10309170375186
 132.89573643674166
 132.3878462777336

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)5702.06375.06907.08028.08993.011219.0
2Exponential growth5562.06364.06859.08063.09019.011080.0
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Skygrid (baseline)5702.06375.06907.08028.08993.011219.0
2Exponential growth5562.06364.06859.08063.09019.011080.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)127.0129.0132.0139.0143.0153.0
2Exponential growth133.0136.0138.0144.0148.0156.0
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Skygrid (baseline)127.0129.0132.0139.0143.0153.0
2Exponential growth133.0136.0138.0144.0148.0156.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)