Skip to content

Sensitivity and comparison analyses

This page continues the main analysis from the one-week-ahead forecast onward: forecast validation, forecast scoring across releases, the reproduction number by release, the outbreak size each data stream implies alone, how the estimate has evolved across releases, comparisons with McCabe et al. and Chamla et al., and the delay and tree-prior sensitivity re-fits. It renders from the same fitted chains as the main analysis, loaded through the shared setup, so no model is re-fit here beyond the frozen re-fits and the optional sensitivity re-fits below.

Load packages, data and fitted chains
julia
# Shared setup: packages, observations, the fit registry and every model fit
# (loaded from the content-addressed cache). See docs/examples/_setup.jl.
using BVDOutbreakSize
include(joinpath(pkgdir(BVDOutbreakSize), "docs", "examples", "_setup.jl"))
45

Forecast validation

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

Fit one week back and validate the one-week-ahead forecast
julia
# frozen_lastweek and frozen_lastweek_streams are computed in the setup
# block above.
# `obs_recovered` is passed so the frozen fit's forecast carries a
# `recovered_new` column (materialised only when the recovered origin is
# given), letting the recovered stream be scored against the observed count
# below like the other streams.
# The onset grid is the one the FROZEN fit saw, not the live one, so the
# validation forecast carries an `onset reports` row scored on the triangle
# the frozen fit was actually fitted to.
_val_onset_days = frozen_lastweek.o.onset_curve_history.onset_days
_val_grid_start = isempty(_val_onset_days) ? nothing :
                  minimum(_val_onset_days)
_val_grid_end = isnothing(_val_grid_start) ? nothing :
                max(maximum(frozen_lastweek.o.onset_curve_history.report_days),
    _val_grid_start)
validation_forecast = forecast_reported(frozen_lastweek.chn;
    horizon = 7,
    obs_cases = frozen_lastweek.o.reported_cases,
    obs_deaths = frozen_lastweek.o.total_deaths,
    obs_confirmed = frozen_lastweek.o.confirmed_cases,
    obs_confirmed_deaths = frozen_lastweek.o.confirmed_deaths,
    obs_recovered = frozen_lastweek.o.recovered_cases,
    grid_n = frozen_lastweek.o.n,
    onset_grid_start = _val_grid_start, onset_grid_end = _val_grid_end);

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

# The observed beds at the current cut-off (the forecast target), so the
# frozen-fit bed forecast is scored against what the beds actually held.
_obs_beds = isempty(obs.isolation_history.counts) ? missing :
            obs.isolation_history.counts[end]
# Same observed/baseline keying as the plot below, so the table covers every
# fitted count stream (cumulative and new-count rows) plus the bed level.
validation_table = forecast_vs_truth(validation_forecast;
    observed = (cases_cum = obs.reported_cases,
        deaths_cum = obs.total_deaths,
        confirmed_cum = obs.confirmed_cases,
        confirmed_deaths_cum = obs.confirmed_deaths,
        recovered_cum = obs.recovered_cases),
    baseline = (cases_cum = frozen_lastweek.o.reported_cases,
        deaths_cum = frozen_lastweek.o.total_deaths,
        confirmed_cum = frozen_lastweek.o.confirmed_cases,
        confirmed_deaths_cum = frozen_lastweek.o.confirmed_deaths,
        recovered_cum = frozen_lastweek.o.recovered_cases),
    isolation = _obs_beds);
Forecast-versus-observed validation table
11×10 DataFrame
RowStreamQuantityObservedLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%Within 90% PI
StringStringFloat64Float64Float64Float64Float64Float64Float64String
1DRC reported casescumulative by T+71077.01144.01464.01812.03010.04200.08011.0no
2DRC reported casesnew this week0.067.0387.0735.01933.03123.06934.0no
3DRC suspected deathscumulative by T+7246.0267.0354.0462.0798.01157.02357.0no
4DRC suspected deathsnew this week0.021.0108.0216.0552.0911.02111.0no
5DRC confirmed casescumulative by T+74567.04174.04337.04483.04728.04925.05366.0yes
6DRC confirmed casesnew this week594.0201.0364.0510.0755.0952.01393.0yes
7DRC confirmed deathscumulative by T+72128.0267.0354.0462.0798.01157.02072.0no
8DRC confirmed deathsnew this week327.00.00.00.00.00.0271.0no
9DRC recoveredcumulative by T+7918.0776.0792.0826.0938.01062.01455.0yes
10DRC recoverednew this week142.00.016.050.0162.0286.0679.0yes
11DRC isolation bedsoccupancy at T+7570.0605.0696.0751.0853.0926.01022.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). The 90% predictive interval is shaded, and the count observed by the current cut-off is a dashed black rule. Each stream draws only when the forecast carries its column, so a fit observing fewer streams shows fewer panels. Where a stream has its own individual (single-stream) fit, that fit's forecast from the same frozen cut-off is overlaid as a dotted density alongside the joint's histogram. Recovered has no individual fit and draws the joint alone.

Forecast-versus-observed plot
julia
# Observed cumulative at the target date per stream, keyed by the forecast's
# cumulative column; `baseline` is each stream's origin cumulative (the frozen
# cut-off), so the new-count panel is scored against observed minus origin.
validation_fig = plot_forecast_vs_truth(validation_forecast;
    observed = (cases_cum = obs.reported_cases,
        deaths_cum = obs.total_deaths,
        confirmed_cum = obs.confirmed_cases,
        confirmed_deaths_cum = obs.confirmed_deaths,
        recovered_cum = obs.recovered_cases),
    baseline = (cases_cum = frozen_lastweek.o.reported_cases,
        deaths_cum = frozen_lastweek.o.total_deaths,
        confirmed_cum = frozen_lastweek.o.confirmed_cases,
        confirmed_deaths_cum = frozen_lastweek.o.confirmed_deaths,
        recovered_cum = frozen_lastweek.o.recovered_cases),
    individual = validation_individual);

The bed panel scores last week's projected occupancy against the beds occupied now (the dashed rule), with the individual (treatment-only) fit's own projection overlaid as a dotted density alongside the joint.

Bed forecast-versus-observed plot
julia
validation_beds_fig = plot_forecast_beds_vs_truth(validation_forecast;
    isolation = _obs_beds, individual = validation_individual_isolation);

The latent quantities are not observed, so they are scored distribution against distribution: what the frozen fit forecast for the past week's new infections, onsets and deaths against what the current fit now estimates for the same window.

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

validation_latent_fig = plot_forecast_vs_truth_latent(
    validation_forecast; now = now_latent);

Forecast scoring across releases

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

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

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

Load and summarise the cross-release forecast scores
julia
# scripts/score_releases.jl writes these after the fits. The committed files
# are header-only until a release carries the asset, so the common path
# reads a real file to a zero-row frame; the typed `schema` is the fallback
# for a file that is absent entirely, since CSV.read throws on a missing
# path and would take the whole docs build with it.
function _release_data(name, schema::NamedTuple)
    path = joinpath(pkgdir(BVDOutbreakSize), "data", name)
    isfile(path) && return CSV.read(path, DataFrame)
    return DataFrame([k => T[] for (k, T) in pairs(schema)])
end

forecast_scores_df = _release_data("forecast_scores.csv",
    (; release = String, made_date = Date, stream = String, horizon = Int,
        target_date = Date, fit = String, crps = Float64,
        log_crps = Float64, dispersion = Float64, overprediction = Float64,
        underprediction = Float64, coverage_50 = Float64,
        coverage_90 = Float64,
        bias = Float64, n_samples = Int,
        log_rel_to_baseline = Float64))
forecast_overlay_df = _release_data("forecast_overlay.csv",
    (; release = String, made_date = Date, stream = String, horizon = Int,
        target_date = Date, fit = String, observed = Float64,
        median = Float64, lo30 = Float64, hi30 = Float64, lo60 = Float64,
        hi60 = Float64, lo90 = Float64, hi90 = Float64))
# The headline table, one row per (stream, fit) pooled over every horizon
# and release; the by-horizon and by-release detail tables carry the same
# columns at a finer grain (see src/scoring.jl).
forecast_score_overview_table = forecast_score_overview(forecast_scores_df)
forecast_score_by_horizon_table = forecast_score_by_horizon(forecast_scores_df)
forecast_score_by_release_table = forecast_score_by_release(forecast_scores_df)
76×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 casesjoint4421.281.40.2691.0missingmissing311.85109.430.00.51.00.49
52026-07-08confirmed casesjoint4390.981.30.2531.0missingmissing239.5151.470.00.251.00.52
62026-07-23confirmed casesconfirmed2143.831.350.1871.52missingmissing136.730.07.11.01.0-0.14
72026-07-23confirmed casesjoint2142.11.330.1491.210.990.8135.465.41.251.01.00.02
82026-07-25confirmed casesconfirmed2178.333.990.2083.67missingmissing176.31.950.081.01.00.03
92026-07-25confirmed casesjoint2104.132.330.1242.20.580.6103.610.520.01.01.00.04
102026-07-26confirmed casesconfirmed2146.773.460.2063.88missingmissing131.930.014.841.01.0-0.22
112026-07-26confirmed casesjoint2108.222.550.1272.40.740.62106.171.340.711.01.00.05
122026-07-27confirmed casesconfirmed2135.072.750.1612.33missingmissing134.260.740.081.01.00.02
132026-07-27confirmed casesjoint2114.162.320.1271.840.850.79109.854.320.01.01.00.12
142026-07-31confirmed casesconfirmed1119.082.780.2052.74missingmissing119.040.040.01.01.00.01
152026-07-31confirmed casesjoint188.672.070.152.010.740.7378.0910.580.01.01.00.18
162026-08-01confirmed casesconfirmed197.353.20.1893.42missingmissing97.160.190.01.01.00.02
172026-08-01confirmed casesjoint169.382.280.1192.150.710.6365.63.780.01.01.00.13
182026-08-02confirmed casesconfirmed191.772.240.1632.12missingmissing89.220.02.551.01.0-0.11
192026-08-02confirmed casesjoint178.021.90.1241.620.850.7670.887.140.01.01.00.23
202026-08-03confirmed casesconfirmed188.11.050.1640.95missingmissing87.140.00.961.01.0-0.06
212026-08-03confirmed casesjoint170.420.840.120.690.80.7369.361.060.01.01.00.08
222026-08-04confirmed casesconfirmed194.852.450.1672.61missingmissing94.70.00.151.01.0-0.02
232026-08-04confirmed casesjoint169.681.80.1161.810.730.768.481.20.01.01.00.07
242026-06-07confirmed deathsjoint4120.570.990.7251.07missingmissing117.80.02.761.01.0-0.11
252026-06-10confirmed deathsjoint4144.921.10.5981.06missingmissing140.543.760.621.01.00.02
262026-07-01confirmed deathsjoint417196.079.420.5491.2missingmissing17103.192.890.01.01.00.28
272026-07-06confirmed deathsjoint4197.20.890.8712.28missingmissing110.350.086.850.251.0-0.49
282026-07-08confirmed deathsjoint4177.590.790.7131.86missingmissing84.290.093.30.251.0-0.61
292026-07-23confirmed deathsconfirmed_deaths2239.076.170.3343.77missingmissing158.2680.820.00.51.00.45
302026-07-23confirmed deathsjoint2135.153.490.4855.470.571.4570.750.064.41.01.0-0.48
312026-07-25confirmed deathsconfirmed_deaths297.983.790.2143.06missingmissing95.022.960.01.01.00.1
322026-07-25confirmed deathsjoint2127.954.950.4957.091.312.3146.770.081.180.01.0-0.58
332026-07-26confirmed deathsconfirmed_deaths2105.514.780.2223.93missingmissing104.481.040.01.01.00.06
342026-07-26confirmed deathsjoint2144.076.530.539.361.372.3843.580.0100.490.01.0-0.68
352026-07-27confirmed deathsconfirmed_deaths2123.884.150.2262.8missingmissing110.5313.350.01.01.00.19
362026-07-27confirmed deathsjoint2133.974.490.5236.481.082.3151.310.082.660.01.0-0.62
372026-07-31confirmed deathsconfirmed_deaths167.743.430.2373.24missingmissing67.460.00.281.01.0-0.03
382026-07-31confirmed deathsjoint1101.185.120.5517.511.492.3229.490.071.690.01.0-0.71
392026-08-01confirmed deathsconfirmed_deaths156.751.690.1931.5missingmissing56.750.00.01.01.00.0
402026-08-01confirmed deathsjoint1121.963.640.6975.42.153.6127.830.094.130.01.0-0.74
412026-08-02confirmed deathsconfirmed_deaths170.732.450.2522.37missingmissing69.50.01.231.01.0-0.08
422026-08-02confirmed deathsjoint196.073.330.4964.661.361.9733.230.062.840.01.0-0.66
432026-08-03confirmed deathsconfirmed_deaths160.781.090.2311.06missingmissing54.930.05.851.01.0-0.2
442026-08-03confirmed deathsjoint1109.991.970.6132.821.812.6534.890.075.10.01.0-0.59
452026-08-04confirmed deathsconfirmed_deaths168.663.950.274.89missingmissing60.320.08.341.01.0-0.24
462026-08-04confirmed deathsjoint1132.467.620.68712.431.932.5428.470.0103.990.01.0-0.74
472026-07-01isolation bedsjoint490.191.50.1381.54missingmissing13.480.076.710.00.0-0.99
482026-07-06isolation bedsjoint442.870.620.0660.63missingmissing21.070.021.80.01.0-0.64
492026-07-08isolation bedsjoint454.751.420.0731.42missingmissing11.6743.080.00.01.00.8
502026-07-23isolation bedsjoint277.692.930.0972.81.130.8424.8352.860.00.01.00.64
512026-07-23isolation bedstreatment268.882.60.1163.33missingmissing56.150.012.731.01.0-0.29
522026-07-25isolation bedsjoint292.942.270.1192.171.741.3225.4367.510.00.01.00.68
532026-07-25isolation bedstreatment253.371.30.091.65missingmissing52.990.020.371.01.0-0.04
542026-07-26isolation bedsjoint285.223.890.113.571.461.0729.6755.550.00.01.00.62
552026-07-26isolation bedstreatment258.452.670.1023.34missingmissing57.70.460.291.01.00.01
562026-07-27isolation bedsjoint276.073.490.0973.251.220.8833.4442.620.00.01.00.55
572026-07-27isolation bedstreatment262.42.860.113.69missingmissing61.760.00.641.01.0-0.07
582026-07-31isolation bedsjoint165.322.730.0822.570.870.6236.1129.210.00.01.00.53
592026-07-31isolation bedstreatment175.13.140.1334.13missingmissing72.210.02.891.01.0-0.12
602026-08-01isolation bedsjoint148.072.030.0611.80.620.4335.6812.390.01.01.00.34
612026-08-01isolation bedstreatment176.993.250.1444.21missingmissing76.790.00.21.01.0-0.04
622026-08-02isolation bedsjoint174.444.090.0953.70.990.738.3536.090.00.01.00.6
632026-08-02isolation bedstreatment175.184.130.1365.3missingmissing74.350.00.831.01.0-0.05
642026-08-03isolation bedsjoint154.953.090.072.810.730.5336.9218.030.01.01.00.41
652026-08-03isolation bedstreatment174.864.20.1335.32missingmissing74.860.00.01.01.00.0
662026-08-04isolation bedsjoint1142.952.070.2131.931.51.2533.51109.440.00.01.00.88
672026-08-04isolation bedstreatment195.261.380.171.54missingmissing90.624.640.01.01.00.12
682026-08-02onset reportsjoint152.820.110.140.020.650.4943.980.08.841.01.0-0.29
692026-08-02onset reportsonsets181.880.180.2830.05missingmissing63.320.018.561.01.0-0.28
702026-08-03onset reportsjoint1175.691.430.7680.161.211.1547.9127.790.00.01.00.85
712026-08-03onset reportsonsets1145.591.180.6670.14missingmissing64.4281.170.00.01.00.64
722026-07-01recoveredjoint410457.7153.970.5391.89missingmissing10409.148.570.01.01.00.29
732026-07-06recoveredjoint485.760.860.4070.93missingmissing68.390.017.371.01.0-0.33
742026-07-08recoveredjoint463.080.80.2921.05missingmissing44.510.018.571.01.0-0.38
752026-05-18reported casesjoint1231.140.481.870.3missingmissing61.150.0169.990.01.0-0.66
762026-05-18suspected deathsjoint141.590.390.5770.12missingmissing41.120.00.471.01.0-0.08

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

11×15 DataFrame
Rowstreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
1confirmed casesconfirmed13130.72.350.1852.3missingmissing126.590.433.681.01.0-0.06
2confirmed casesjoint332262.2314.790.3772.230.770.72142.59119.520.120.731.00.3
3confirmed deathsconfirmed_deaths13112.123.750.2442.71missingmissing95.8115.11.210.921.00.08
4confirmed deathsjoint332211.7918.00.6341.91.132.242145.5511.7254.530.481.0-0.36
5isolation bedsjoint2572.031.720.0991.641.190.8823.6932.5815.760.080.840.18
6isolation bedstreatment1367.972.360.1192.94missingmissing65.080.432.461.01.0-0.07
7onset reportsjoint2114.260.390.4540.081.00.9645.9463.94.420.51.00.28
8onset reportsonsets2113.730.390.4750.09missingmissing63.8740.589.280.51.00.18
9recoveredjoint123535.543.150.4131.24missingmissing3507.3316.1911.981.01.0-0.14
10reported casesjoint1231.140.481.870.3missingmissing61.150.0169.990.01.0-0.66
11suspected deathsjoint141.590.390.5770.12missingmissing41.120.00.471.01.0-0.08

The same relative skill against the baseline, by horizon: one panel per stream, one series per fit role, on a log-scaled skill axis with the reference line at one. A fit that beats the baseline on average but not at every cut-off is visible as a series that crosses the line rather than sitting under it throughout.

julia
forecast_relative_skill_fig = plot_forecast_relative_skill(
    forecast_score_by_horizon_table);

The same columns as a table, broken out by horizon, and again broken out by release and averaged across horizons, are behind the two dropdowns below.

Scores by horizon
26×16 DataFrame
Rowstreamhorizonfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31Int64String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
1confirmed cases7confirmed997.242.20.1832.26missingmissing93.970.033.251.01.0-0.08
2confirmed cases7joint14117.062.930.2933.220.760.6995.4621.420.180.791.00.23
3confirmed cases14confirmed4205.992.540.1912.4missingmissing200.01.344.651.01.0-0.02
4confirmed cases14joint9301.83.90.3463.390.790.71232.768.950.160.781.00.26
5confirmed cases21joint5987.694.130.5042.02missingmissing756.12231.570.00.61.00.42
6confirmed cases28joint513072.025.170.5371.25missingmissing12698.8373.210.00.61.00.46
7confirmed deaths7confirmed_deaths968.792.420.2322.26missingmissing63.413.631.741.01.00.01
8confirmed deaths7joint1487.743.620.7185.721.452.4235.110.452.230.291.0-0.52
9confirmed deaths14confirmed_deaths4209.626.30.2734.37missingmissing168.7240.90.00.751.00.25
10confirmed deaths14joint9167.412.830.5852.990.891.8993.793.8769.760.441.0-0.38
11confirmed deaths21joint5432.871.90.5580.9missingmissing371.5521.8339.490.61.0-0.14
12confirmed deaths28joint513617.933.330.5660.64missingmissing13521.947.448.581.01.0-0.09
13isolation beds7joint1275.222.00.1011.861.060.7926.4737.5811.160.170.920.37
14isolation beds7treatment973.062.530.133.13missingmissing69.660.522.881.01.0-0.07
15isolation beds14joint774.671.970.0991.841.571.1723.1139.1912.360.00.860.22
16isolation beds14treatment456.511.970.0962.48missingmissing54.770.231.511.01.0-0.07
17isolation beds21joint354.831.030.0811.03missingmissing16.3710.5527.910.00.67-0.29
18isolation beds28joint370.361.240.1111.33missingmissing21.2419.1929.920.00.67-0.25
19onset reports7joint2114.260.390.4540.081.00.9645.9463.94.420.51.00.28
20onset reports7onsets2113.730.390.4750.09missingmissing63.8740.589.280.51.00.18
21recovered7joint326.962.060.3191.68missingmissing20.931.284.761.01.0-0.18
22recovered14joint389.72.130.3931.56missingmissing68.015.8115.871.01.0-0.23
23recovered21joint3297.163.50.4221.24missingmissing269.916.0511.21.01.0-0.09
24recovered28joint313728.273.20.5170.94missingmissing13670.541.6216.091.01.0-0.06
25reported cases7joint1231.140.481.870.3missingmissing61.150.0169.990.01.0-0.66
26suspected deaths7joint141.590.390.5770.12missingmissing41.120.00.471.01.0-0.08
Scores by release
76×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 casesjoint4421.281.40.2691.0missingmissing311.85109.430.00.51.00.49
52026-07-08confirmed casesjoint4390.981.30.2531.0missingmissing239.5151.470.00.251.00.52
62026-07-23confirmed casesconfirmed2143.831.350.1871.52missingmissing136.730.07.11.01.0-0.14
72026-07-23confirmed casesjoint2142.11.330.1491.210.990.8135.465.41.251.01.00.02
82026-07-25confirmed casesconfirmed2178.333.990.2083.67missingmissing176.31.950.081.01.00.03
92026-07-25confirmed casesjoint2104.132.330.1242.20.580.6103.610.520.01.01.00.04
102026-07-26confirmed casesconfirmed2146.773.460.2063.88missingmissing131.930.014.841.01.0-0.22
112026-07-26confirmed casesjoint2108.222.550.1272.40.740.62106.171.340.711.01.00.05
122026-07-27confirmed casesconfirmed2135.072.750.1612.33missingmissing134.260.740.081.01.00.02
132026-07-27confirmed casesjoint2114.162.320.1271.840.850.79109.854.320.01.01.00.12
142026-07-31confirmed casesconfirmed1119.082.780.2052.74missingmissing119.040.040.01.01.00.01
152026-07-31confirmed casesjoint188.672.070.152.010.740.7378.0910.580.01.01.00.18
162026-08-01confirmed casesconfirmed197.353.20.1893.42missingmissing97.160.190.01.01.00.02
172026-08-01confirmed casesjoint169.382.280.1192.150.710.6365.63.780.01.01.00.13
182026-08-02confirmed casesconfirmed191.772.240.1632.12missingmissing89.220.02.551.01.0-0.11
192026-08-02confirmed casesjoint178.021.90.1241.620.850.7670.887.140.01.01.00.23
202026-08-03confirmed casesconfirmed188.11.050.1640.95missingmissing87.140.00.961.01.0-0.06
212026-08-03confirmed casesjoint170.420.840.120.690.80.7369.361.060.01.01.00.08
222026-08-04confirmed casesconfirmed194.852.450.1672.61missingmissing94.70.00.151.01.0-0.02
232026-08-04confirmed casesjoint169.681.80.1161.810.730.768.481.20.01.01.00.07
242026-06-07confirmed deathsjoint4120.570.990.7251.07missingmissing117.80.02.761.01.0-0.11
252026-06-10confirmed deathsjoint4144.921.10.5981.06missingmissing140.543.760.621.01.00.02
262026-07-01confirmed deathsjoint417196.079.420.5491.2missingmissing17103.192.890.01.01.00.28
272026-07-06confirmed deathsjoint4197.20.890.8712.28missingmissing110.350.086.850.251.0-0.49
282026-07-08confirmed deathsjoint4177.590.790.7131.86missingmissing84.290.093.30.251.0-0.61
292026-07-23confirmed deathsconfirmed_deaths2239.076.170.3343.77missingmissing158.2680.820.00.51.00.45
302026-07-23confirmed deathsjoint2135.153.490.4855.470.571.4570.750.064.41.01.0-0.48
312026-07-25confirmed deathsconfirmed_deaths297.983.790.2143.06missingmissing95.022.960.01.01.00.1
322026-07-25confirmed deathsjoint2127.954.950.4957.091.312.3146.770.081.180.01.0-0.58
332026-07-26confirmed deathsconfirmed_deaths2105.514.780.2223.93missingmissing104.481.040.01.01.00.06
342026-07-26confirmed deathsjoint2144.076.530.539.361.372.3843.580.0100.490.01.0-0.68
352026-07-27confirmed deathsconfirmed_deaths2123.884.150.2262.8missingmissing110.5313.350.01.01.00.19
362026-07-27confirmed deathsjoint2133.974.490.5236.481.082.3151.310.082.660.01.0-0.62
372026-07-31confirmed deathsconfirmed_deaths167.743.430.2373.24missingmissing67.460.00.281.01.0-0.03
382026-07-31confirmed deathsjoint1101.185.120.5517.511.492.3229.490.071.690.01.0-0.71
392026-08-01confirmed deathsconfirmed_deaths156.751.690.1931.5missingmissing56.750.00.01.01.00.0
402026-08-01confirmed deathsjoint1121.963.640.6975.42.153.6127.830.094.130.01.0-0.74
412026-08-02confirmed deathsconfirmed_deaths170.732.450.2522.37missingmissing69.50.01.231.01.0-0.08
422026-08-02confirmed deathsjoint196.073.330.4964.661.361.9733.230.062.840.01.0-0.66
432026-08-03confirmed deathsconfirmed_deaths160.781.090.2311.06missingmissing54.930.05.851.01.0-0.2
442026-08-03confirmed deathsjoint1109.991.970.6132.821.812.6534.890.075.10.01.0-0.59
452026-08-04confirmed deathsconfirmed_deaths168.663.950.274.89missingmissing60.320.08.341.01.0-0.24
462026-08-04confirmed deathsjoint1132.467.620.68712.431.932.5428.470.0103.990.01.0-0.74
472026-07-01isolation bedsjoint490.191.50.1381.54missingmissing13.480.076.710.00.0-0.99
482026-07-06isolation bedsjoint442.870.620.0660.63missingmissing21.070.021.80.01.0-0.64
492026-07-08isolation bedsjoint454.751.420.0731.42missingmissing11.6743.080.00.01.00.8
502026-07-23isolation bedsjoint277.692.930.0972.81.130.8424.8352.860.00.01.00.64
512026-07-23isolation bedstreatment268.882.60.1163.33missingmissing56.150.012.731.01.0-0.29
522026-07-25isolation bedsjoint292.942.270.1192.171.741.3225.4367.510.00.01.00.68
532026-07-25isolation bedstreatment253.371.30.091.65missingmissing52.990.020.371.01.0-0.04
542026-07-26isolation bedsjoint285.223.890.113.571.461.0729.6755.550.00.01.00.62
552026-07-26isolation bedstreatment258.452.670.1023.34missingmissing57.70.460.291.01.00.01
562026-07-27isolation bedsjoint276.073.490.0973.251.220.8833.4442.620.00.01.00.55
572026-07-27isolation bedstreatment262.42.860.113.69missingmissing61.760.00.641.01.0-0.07
582026-07-31isolation bedsjoint165.322.730.0822.570.870.6236.1129.210.00.01.00.53
592026-07-31isolation bedstreatment175.13.140.1334.13missingmissing72.210.02.891.01.0-0.12
602026-08-01isolation bedsjoint148.072.030.0611.80.620.4335.6812.390.01.01.00.34
612026-08-01isolation bedstreatment176.993.250.1444.21missingmissing76.790.00.21.01.0-0.04
622026-08-02isolation bedsjoint174.444.090.0953.70.990.738.3536.090.00.01.00.6
632026-08-02isolation bedstreatment175.184.130.1365.3missingmissing74.350.00.831.01.0-0.05
642026-08-03isolation bedsjoint154.953.090.072.810.730.5336.9218.030.01.01.00.41
652026-08-03isolation bedstreatment174.864.20.1335.32missingmissing74.860.00.01.01.00.0
662026-08-04isolation bedsjoint1142.952.070.2131.931.51.2533.51109.440.00.01.00.88
672026-08-04isolation bedstreatment195.261.380.171.54missingmissing90.624.640.01.01.00.12
682026-08-02onset reportsjoint152.820.110.140.020.650.4943.980.08.841.01.0-0.29
692026-08-02onset reportsonsets181.880.180.2830.05missingmissing63.320.018.561.01.0-0.28
702026-08-03onset reportsjoint1175.691.430.7680.161.211.1547.9127.790.00.01.00.85
712026-08-03onset reportsonsets1145.591.180.6670.14missingmissing64.4281.170.00.01.00.64
722026-07-01recoveredjoint410457.7153.970.5391.89missingmissing10409.148.570.01.01.00.29
732026-07-06recoveredjoint485.760.860.4070.93missingmissing68.390.017.371.01.0-0.33
742026-07-08recoveredjoint463.080.80.2921.05missingmissing44.510.018.571.01.0-0.38
752026-05-18reported casesjoint1231.140.481.870.3missingmissing61.150.0169.990.01.0-0.66
762026-05-18suspected deathsjoint141.590.390.5770.12missingmissing41.120.00.471.01.0-0.08

Forecasts made at each release against the value observed since, one panel per stream and horizon, the observed value in black. The median and 90% interval are coloured by fit role: the persistence baseline, the stream's individual fit and the joint. The x-axis is the date each forecast was made, so an incident stream's observed window pairs unambiguously with the forecast that made it. Each panel's axis is cropped to a small multiple of what that stream actually reached, so one very wide interval cannot squash every other series flat. An interval or median too wide for the panel is clamped at the top and marked with an open triangle rather than silently cut off.

Forecasts-versus-now overlay
julia
forecast_overlay_fig = plot_forecast_overlay(forecast_overlay_df);

Frozen-fit forecast evaluation

The current model, frozen at earlier data cut-offs (see Forecast-versus-frozen evaluation), is scored the same way as the cross-release forecasts above, against the same persistence baseline. Only the joint model is scored here, so no individual single-stream fit appears in the tables and figures below. The May cut-offs predate the first reported bed occupancy and the first reported recoveries, so those windows are left unscored rather than scored against a series that had not started. The baseline carries a weaker data-vintage guarantee than the cross-release one, since its snapshot was taken weeks after the frozen cut-off and can hold later revisions to earlier days (see forecast scoring against a persistence baseline).

Load and summarise the frozen-fit forecast scores
julia
frozen_scores_df = _release_data("forecast_scores_frozen.csv",
    (; release = String, made_date = Date, stream = String, horizon = Int,
        target_date = Date, fit = String, crps = Float64,
        log_crps = Float64, dispersion = Float64, overprediction = Float64,
        underprediction = Float64, coverage_50 = Float64,
        coverage_90 = Float64,
        bias = Float64, n_samples = Int,
        log_rel_to_baseline = Float64))
frozen_overlay_df = _release_data("forecast_overlay_frozen.csv",
    (; release = String, made_date = Date, stream = String, horizon = Int,
        target_date = Date, fit = String, observed = Float64,
        median = Float64, lo30 = Float64, hi30 = Float64, lo60 = Float64,
        hi60 = Float64, lo90 = Float64, hi90 = Float64))
# The frozen evaluation never carries an individual single-stream fit
# (it scores only the joint model at past cut-offs), so the individual-fit
# comparison columns are dropped rather than shown as a column of missing.
frozen_score_overview_table = drop_individual_fit_columns(
    forecast_score_overview(frozen_scores_df))
frozen_score_by_horizon_table = drop_individual_fit_columns(
    forecast_score_by_horizon(frozen_scores_df))
frozen_score_by_release_table = drop_individual_fit_columns(
    forecast_score_by_release(frozen_scores_df))

# `fit` is single-valued (`FROZEN_FIT`) by construction in every one of
# these tables, not just for the releases scored so far, so it is dropped
# from the display tables below as a degenerate column. The `..._table`
# frames above keep it and still feed the relative-skill plots, which read
# it to colour each series in the joint role.
frozen_score_overview_display = drop_degenerate_fit_column(
    frozen_score_overview_table)
frozen_score_by_horizon_display = drop_degenerate_fit_column(
    frozen_score_by_horizon_table)
frozen_score_by_release_display = drop_degenerate_fit_column(
    frozen_score_by_release_table)
58×13 DataFrame
Rowmade_datestreamncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
12026-05-20confirmed cases44277.410.760.3720.25225.4951.920.00.751.00.42
22026-05-23confirmed cases44184.40.450.2190.16156.127.590.711.01.00.09
32026-05-27confirmed cases44299.870.580.8690.5751.60.0248.270.00.75-0.83
42026-06-08confirmed cases44628.94.450.4352.18339.44289.460.00.361.00.6
52026-07-16confirmed cases3192.870.410.1570.29190.392.150.331.01.00.02
62026-07-18confirmed cases3246.760.870.1720.62222.5224.240.01.01.00.15
72026-07-19confirmed cases3299.441.30.1930.89244.8954.550.01.01.00.28
82026-07-20confirmed cases3246.30.860.1670.61211.2235.080.01.01.00.2
92026-07-24confirmed cases2159.851.80.1521.45134.5525.30.01.01.00.29
102026-07-25confirmed cases2202.94.20.1853.05150.1852.720.01.01.00.34
112026-07-26confirmed cases2146.913.20.1462.53130.216.710.01.01.00.24
122026-07-27confirmed cases2155.942.650.161.95140.2415.70.01.01.00.26
132026-07-28confirmed cases2145.081.850.1391.54132.7812.30.01.01.00.19
142026-07-31confirmed cases1114.332.730.1762.4589.9524.380.01.01.00.38
152026-08-04confirmed cases180.421.990.1271.974.615.810.01.01.00.18
162026-05-20confirmed deaths4434.750.350.3990.1921.150.013.60.821.0-0.34
172026-05-23confirmed deaths4442.180.350.4920.2319.950.022.230.251.0-0.58
182026-05-27confirmed deaths44123.650.841.7960.925.60.0118.050.00.0-0.97
192026-06-08confirmed deaths4477.990.580.2330.3676.141.060.791.01.0-0.05
202026-07-16confirmed deaths3284.91.391.8134.2458.640.0226.270.01.0-0.78
212026-07-18confirmed deaths3246.711.961.7467.5872.180.0174.540.01.0-0.72
222026-07-19confirmed deaths3230.82.091.6488.6283.420.0147.380.01.0-0.66
232026-07-20confirmed deaths3264.861.951.9878.0973.140.0191.720.01.0-0.68
242026-07-24confirmed deaths2223.699.762.69746.6441.380.0182.310.01.0-0.78
252026-07-25confirmed deaths2212.118.572.738.6847.240.0164.870.01.0-0.68
262026-07-26confirmed deaths2255.7211.783.24759.4231.750.0223.960.01.0-0.78
272026-07-27confirmed deaths2248.369.513.10643.6937.00.0211.360.01.0-0.71
282026-07-28confirmed deaths2278.35.52.97927.6738.30.0239.990.01.0-0.76
292026-07-31confirmed deaths1208.2110.373.71748.6810.040.0198.170.01.0-0.77
302026-08-04confirmed deaths1287.6715.654.99483.61.530.0286.140.00.0-0.93
312026-06-08isolation beds4474.590.440.1520.3939.122.333.170.51.0-0.21
322026-07-16isolation beds371.562.780.0922.5617.5454.020.00.01.00.61
332026-07-18isolation beds377.342.570.1012.4315.7361.610.00.01.00.64
342026-07-19isolation beds3104.53.980.1353.7619.0485.460.00.01.00.68
352026-07-20isolation beds389.563.630.1143.3716.4673.10.00.01.00.67
362026-07-24isolation beds295.454.570.1194.257.0788.390.00.01.00.85
372026-07-25isolation beds2132.763.040.1692.899.86122.90.00.00.50.86
382026-07-26isolation beds2123.615.120.1574.7215.36108.250.00.00.50.81
392026-07-27isolation beds295.614.530.1214.1727.8967.720.00.01.00.66
402026-07-28isolation beds2198.852.650.2682.3630.38168.470.00.00.50.9
412026-07-31isolation beds1149.986.310.1825.7433.1116.880.00.00.00.9
422026-08-04isolation beds1169.732.50.2512.331.9137.830.00.00.00.95
432026-07-26onset reports294.330.20.1930.0365.2229.110.00.51.00.43
442026-07-27onset reports2124.720.270.2050.0356.6667.740.320.51.00.34
452026-07-28onset reports147.520.110.1170.0245.920.01.61.01.0-0.11
462026-07-31onset reports178.090.230.1880.0354.8323.260.01.01.00.36
472026-06-08recovered44114.321.020.4420.3192.022.320.01.01.00.37
482026-07-16recovered399.861.130.4761.2395.40.04.461.01.0-0.12
492026-07-18recovered3108.043.00.4452.75105.092.950.01.01.00.1
502026-07-19recovered3119.043.070.4673.15117.971.070.01.01.00.08
512026-07-20recovered3114.752.730.4062.63114.340.410.01.01.00.05
522026-07-24recovered289.564.570.3793.2583.965.60.01.01.00.18
532026-07-25recovered288.34.830.3553.1583.854.440.01.01.00.14
542026-07-26recovered269.223.270.3642.8968.70.00.521.01.0-0.06
552026-07-27recovered299.213.670.4463.1198.130.01.081.01.0-0.08
562026-07-28recovered282.951.250.3480.9281.820.470.661.01.0-0.0
572026-07-31recovered165.741.950.3581.3665.030.710.01.01.00.09
582026-08-04recovered145.372.130.4123.1941.70.03.671.01.0-0.16

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

5×12 DataFrame
Rowstreamncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
1confirmed cases200553.291.640.3240.32334.44218.820.020.641.00.41
2confirmed deaths200202.311.681.5931.0482.710.86118.730.40.98-0.42
3isolation beds68109.40.910.1610.6126.264.7218.490.030.850.37
4onset reports693.950.210.1840.0357.4236.160.370.671.00.3
5recovered68123.181.420.4270.43111.0411.50.631.01.00.12

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

julia
frozen_relative_skill_fig = plot_forecast_relative_skill(
    frozen_score_by_horizon_table);

The same columns as a table, broken out by horizon, and again broken out by frozen cut-off, are behind the two dropdowns below.

Scores by horizon
18×13 DataFrame
Rowstreamhorizonncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31Int64Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
1confirmed cases75593.321.360.1460.3483.1410.10.091.01.00.18
2confirmed cases1453225.651.00.1880.22188.5937.060.01.01.00.27
3confirmed cases2148614.861.370.4120.32356.77258.080.00.421.00.53
4confirmed cases28441455.722.110.6140.36799.89655.830.00.01.00.71
5confirmed deaths755210.339.463.5935.399.560.0200.760.00.91-0.84
6confirmed deaths1453233.353.531.5651.2666.20.0167.150.151.0-0.59
7confirmed deaths2148185.91.130.5620.29105.420.3680.110.581.0-0.22
8confirmed deaths2844172.80.670.250.1169.273.530.01.01.00.1
9isolation beds722111.852.810.1441.5117.2894.570.00.00.550.83
10isolation beds1420109.252.40.1471.3121.0588.190.00.11.00.67
11isolation beds211589.740.460.1460.3432.0137.120.640.01.00.06
12isolation beds2811131.590.420.2430.3745.450.086.140.01.0-0.69
13onset reports7463.370.180.1690.0347.4215.390.560.751.00.18
14onset reports142155.10.250.2130.0377.477.70.00.51.00.54
15recovered72249.13.340.4022.4247.120.571.421.01.0-0.0
16recovered1420108.232.030.4070.52104.962.840.431.01.00.07
17recovered2115150.741.170.4380.3142.747.770.221.01.00.14
18recovered2811260.941.120.4990.21206.7154.220.01.01.00.43
Scores by frozen cut-off
58×13 DataFrame
Rowmade_datestreamncrpsrel_to_baselinelog_crpslog_rel_to_baselinedispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31Int64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
12026-05-20confirmed cases44277.410.760.3720.25225.4951.920.00.751.00.42
22026-05-23confirmed cases44184.40.450.2190.16156.127.590.711.01.00.09
32026-05-27confirmed cases44299.870.580.8690.5751.60.0248.270.00.75-0.83
42026-06-08confirmed cases44628.94.450.4352.18339.44289.460.00.361.00.6
52026-07-16confirmed cases3192.870.410.1570.29190.392.150.331.01.00.02
62026-07-18confirmed cases3246.760.870.1720.62222.5224.240.01.01.00.15
72026-07-19confirmed cases3299.441.30.1930.89244.8954.550.01.01.00.28
82026-07-20confirmed cases3246.30.860.1670.61211.2235.080.01.01.00.2
92026-07-24confirmed cases2159.851.80.1521.45134.5525.30.01.01.00.29
102026-07-25confirmed cases2202.94.20.1853.05150.1852.720.01.01.00.34
112026-07-26confirmed cases2146.913.20.1462.53130.216.710.01.01.00.24
122026-07-27confirmed cases2155.942.650.161.95140.2415.70.01.01.00.26
132026-07-28confirmed cases2145.081.850.1391.54132.7812.30.01.01.00.19
142026-07-31confirmed cases1114.332.730.1762.4589.9524.380.01.01.00.38
152026-08-04confirmed cases180.421.990.1271.974.615.810.01.01.00.18
162026-05-20confirmed deaths4434.750.350.3990.1921.150.013.60.821.0-0.34
172026-05-23confirmed deaths4442.180.350.4920.2319.950.022.230.251.0-0.58
182026-05-27confirmed deaths44123.650.841.7960.925.60.0118.050.00.0-0.97
192026-06-08confirmed deaths4477.990.580.2330.3676.141.060.791.01.0-0.05
202026-07-16confirmed deaths3284.91.391.8134.2458.640.0226.270.01.0-0.78
212026-07-18confirmed deaths3246.711.961.7467.5872.180.0174.540.01.0-0.72
222026-07-19confirmed deaths3230.82.091.6488.6283.420.0147.380.01.0-0.66
232026-07-20confirmed deaths3264.861.951.9878.0973.140.0191.720.01.0-0.68
242026-07-24confirmed deaths2223.699.762.69746.6441.380.0182.310.01.0-0.78
252026-07-25confirmed deaths2212.118.572.738.6847.240.0164.870.01.0-0.68
262026-07-26confirmed deaths2255.7211.783.24759.4231.750.0223.960.01.0-0.78
272026-07-27confirmed deaths2248.369.513.10643.6937.00.0211.360.01.0-0.71
282026-07-28confirmed deaths2278.35.52.97927.6738.30.0239.990.01.0-0.76
292026-07-31confirmed deaths1208.2110.373.71748.6810.040.0198.170.01.0-0.77
302026-08-04confirmed deaths1287.6715.654.99483.61.530.0286.140.00.0-0.93
312026-06-08isolation beds4474.590.440.1520.3939.122.333.170.51.0-0.21
322026-07-16isolation beds371.562.780.0922.5617.5454.020.00.01.00.61
332026-07-18isolation beds377.342.570.1012.4315.7361.610.00.01.00.64
342026-07-19isolation beds3104.53.980.1353.7619.0485.460.00.01.00.68
352026-07-20isolation beds389.563.630.1143.3716.4673.10.00.01.00.67
362026-07-24isolation beds295.454.570.1194.257.0788.390.00.01.00.85
372026-07-25isolation beds2132.763.040.1692.899.86122.90.00.00.50.86
382026-07-26isolation beds2123.615.120.1574.7215.36108.250.00.00.50.81
392026-07-27isolation beds295.614.530.1214.1727.8967.720.00.01.00.66
402026-07-28isolation beds2198.852.650.2682.3630.38168.470.00.00.50.9
412026-07-31isolation beds1149.986.310.1825.7433.1116.880.00.00.00.9
422026-08-04isolation beds1169.732.50.2512.331.9137.830.00.00.00.95
432026-07-26onset reports294.330.20.1930.0365.2229.110.00.51.00.43
442026-07-27onset reports2124.720.270.2050.0356.6667.740.320.51.00.34
452026-07-28onset reports147.520.110.1170.0245.920.01.61.01.0-0.11
462026-07-31onset reports178.090.230.1880.0354.8323.260.01.01.00.36
472026-06-08recovered44114.321.020.4420.3192.022.320.01.01.00.37
482026-07-16recovered399.861.130.4761.2395.40.04.461.01.0-0.12
492026-07-18recovered3108.043.00.4452.75105.092.950.01.01.00.1
502026-07-19recovered3119.043.070.4673.15117.971.070.01.01.00.08
512026-07-20recovered3114.752.730.4062.63114.340.410.01.01.00.05
522026-07-24recovered289.564.570.3793.2583.965.60.01.01.00.18
532026-07-25recovered288.34.830.3553.1583.854.440.01.01.00.14
542026-07-26recovered269.223.270.3642.8968.70.00.521.01.0-0.06
552026-07-27recovered299.213.670.4463.1198.130.01.081.01.0-0.08
562026-07-28recovered282.951.250.3480.9281.820.470.661.01.0-0.0
572026-07-31recovered165.741.950.3581.3665.030.710.01.01.00.09
582026-08-04recovered145.372.130.4123.1941.70.03.671.01.0-0.16

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

Frozen-fit forecasts-versus-now overlay
julia
frozen_overlay_fig = plot_forecast_overlay(frozen_overlay_df);

The frozen re-fits below freeze the renewal data to an earlier cut-off and re-fit, so that a change driven by newer data can be distinguished from one driven by a change of method. Each uses the full headline settings: 1000 draws across two chains.

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

Individual fits against the baseline

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

Individual-fit rows of the cross-release scores
julia
individual_score_overview_table = forecast_score_overview_table[
    forecast_score_overview_table.fit .!= "joint", :]
individual_score_by_horizon_table = forecast_score_by_horizon_table[
    forecast_score_by_horizon_table.fit .!= "joint", :]
individual_score_by_release_table = forecast_score_by_release_table[
    forecast_score_by_release_table.fit .!= "joint", :]
29×16 DataFrame
Rowmade_datestreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
12026-07-23confirmed casesconfirmed2143.831.350.1871.52missingmissing136.730.07.11.01.0-0.14
22026-07-25confirmed casesconfirmed2178.333.990.2083.67missingmissing176.31.950.081.01.00.03
32026-07-26confirmed casesconfirmed2146.773.460.2063.88missingmissing131.930.014.841.01.0-0.22
42026-07-27confirmed casesconfirmed2135.072.750.1612.33missingmissing134.260.740.081.01.00.02
52026-07-31confirmed casesconfirmed1119.082.780.2052.74missingmissing119.040.040.01.01.00.01
62026-08-01confirmed casesconfirmed197.353.20.1893.42missingmissing97.160.190.01.01.00.02
72026-08-02confirmed casesconfirmed191.772.240.1632.12missingmissing89.220.02.551.01.0-0.11
82026-08-03confirmed casesconfirmed188.11.050.1640.95missingmissing87.140.00.961.01.0-0.06
92026-08-04confirmed casesconfirmed194.852.450.1672.61missingmissing94.70.00.151.01.0-0.02
102026-07-23confirmed deathsconfirmed_deaths2239.076.170.3343.77missingmissing158.2680.820.00.51.00.45
112026-07-25confirmed deathsconfirmed_deaths297.983.790.2143.06missingmissing95.022.960.01.01.00.1
122026-07-26confirmed deathsconfirmed_deaths2105.514.780.2223.93missingmissing104.481.040.01.01.00.06
132026-07-27confirmed deathsconfirmed_deaths2123.884.150.2262.8missingmissing110.5313.350.01.01.00.19
142026-07-31confirmed deathsconfirmed_deaths167.743.430.2373.24missingmissing67.460.00.281.01.0-0.03
152026-08-01confirmed deathsconfirmed_deaths156.751.690.1931.5missingmissing56.750.00.01.01.00.0
162026-08-02confirmed deathsconfirmed_deaths170.732.450.2522.37missingmissing69.50.01.231.01.0-0.08
172026-08-03confirmed deathsconfirmed_deaths160.781.090.2311.06missingmissing54.930.05.851.01.0-0.2
182026-08-04confirmed deathsconfirmed_deaths168.663.950.274.89missingmissing60.320.08.341.01.0-0.24
192026-07-23isolation bedstreatment268.882.60.1163.33missingmissing56.150.012.731.01.0-0.29
202026-07-25isolation bedstreatment253.371.30.091.65missingmissing52.990.020.371.01.0-0.04
212026-07-26isolation bedstreatment258.452.670.1023.34missingmissing57.70.460.291.01.00.01
222026-07-27isolation bedstreatment262.42.860.113.69missingmissing61.760.00.641.01.0-0.07
232026-07-31isolation bedstreatment175.13.140.1334.13missingmissing72.210.02.891.01.0-0.12
242026-08-01isolation bedstreatment176.993.250.1444.21missingmissing76.790.00.21.01.0-0.04
252026-08-02isolation bedstreatment175.184.130.1365.3missingmissing74.350.00.831.01.0-0.05
262026-08-03isolation bedstreatment174.864.20.1335.32missingmissing74.860.00.01.01.00.0
272026-08-04isolation bedstreatment195.261.380.171.54missingmissing90.624.640.01.01.00.12
282026-08-02onset reportsonsets181.880.180.2830.05missingmissing63.320.018.561.01.0-0.28
292026-08-03onset reportsonsets1145.591.180.6670.14missingmissing64.4281.170.00.01.00.64
4×15 DataFrame
Rowstreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
1confirmed casesconfirmed13130.72.350.1852.3missingmissing126.590.433.681.01.0-0.06
2confirmed deathsconfirmed_deaths13112.123.750.2442.71missingmissing95.8115.11.210.921.00.08
3isolation bedstreatment1367.972.360.1192.94missingmissing65.080.432.461.01.0-0.07
4onset reportsonsets2113.730.390.4750.09missingmissing63.8740.589.280.51.00.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
7×16 DataFrame
Rowstreamhorizonfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
String31Int64String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
1confirmed cases7confirmed997.242.20.1832.26missingmissing93.970.033.251.01.0-0.08
2confirmed cases14confirmed4205.992.540.1912.4missingmissing200.01.344.651.01.0-0.02
3confirmed deaths7confirmed_deaths968.792.420.2322.26missingmissing63.413.631.741.01.00.01
4confirmed deaths14confirmed_deaths4209.626.30.2734.37missingmissing168.7240.90.00.751.00.25
5isolation beds7treatment973.062.530.133.13missingmissing69.660.522.881.01.0-0.07
6isolation beds14treatment456.511.970.0962.48missingmissing54.770.231.511.01.0-0.07
7onset reports7onsets2113.730.390.4750.09missingmissing63.8740.589.280.51.00.18
Scores by release
29×16 DataFrame
Rowmade_datestreamfitncrpsrel_to_baselinelog_crpslog_rel_to_baselinerel_to_individuallog_rel_to_individualdispersionoverpredictionunderpredictioncoverage_50coverage_90bias
DateString31String31Int64Float64Float64Float64Float64Float64?Float64?Float64Float64Float64Float64Float64Float64
12026-07-23confirmed casesconfirmed2143.831.350.1871.52missingmissing136.730.07.11.01.0-0.14
22026-07-25confirmed casesconfirmed2178.333.990.2083.67missingmissing176.31.950.081.01.00.03
32026-07-26confirmed casesconfirmed2146.773.460.2063.88missingmissing131.930.014.841.01.0-0.22
42026-07-27confirmed casesconfirmed2135.072.750.1612.33missingmissing134.260.740.081.01.00.02
52026-07-31confirmed casesconfirmed1119.082.780.2052.74missingmissing119.040.040.01.01.00.01
62026-08-01confirmed casesconfirmed197.353.20.1893.42missingmissing97.160.190.01.01.00.02
72026-08-02confirmed casesconfirmed191.772.240.1632.12missingmissing89.220.02.551.01.0-0.11
82026-08-03confirmed casesconfirmed188.11.050.1640.95missingmissing87.140.00.961.01.0-0.06
92026-08-04confirmed casesconfirmed194.852.450.1672.61missingmissing94.70.00.151.01.0-0.02
102026-07-23confirmed deathsconfirmed_deaths2239.076.170.3343.77missingmissing158.2680.820.00.51.00.45
112026-07-25confirmed deathsconfirmed_deaths297.983.790.2143.06missingmissing95.022.960.01.01.00.1
122026-07-26confirmed deathsconfirmed_deaths2105.514.780.2223.93missingmissing104.481.040.01.01.00.06
132026-07-27confirmed deathsconfirmed_deaths2123.884.150.2262.8missingmissing110.5313.350.01.01.00.19
142026-07-31confirmed deathsconfirmed_deaths167.743.430.2373.24missingmissing67.460.00.281.01.0-0.03
152026-08-01confirmed deathsconfirmed_deaths156.751.690.1931.5missingmissing56.750.00.01.01.00.0
162026-08-02confirmed deathsconfirmed_deaths170.732.450.2522.37missingmissing69.50.01.231.01.0-0.08
172026-08-03confirmed deathsconfirmed_deaths160.781.090.2311.06missingmissing54.930.05.851.01.0-0.2
182026-08-04confirmed deathsconfirmed_deaths168.663.950.274.89missingmissing60.320.08.341.01.0-0.24
192026-07-23isolation bedstreatment268.882.60.1163.33missingmissing56.150.012.731.01.0-0.29
202026-07-25isolation bedstreatment253.371.30.091.65missingmissing52.990.020.371.01.0-0.04
212026-07-26isolation bedstreatment258.452.670.1023.34missingmissing57.70.460.291.01.00.01
222026-07-27isolation bedstreatment262.42.860.113.69missingmissing61.760.00.641.01.0-0.07
232026-07-31isolation bedstreatment175.13.140.1334.13missingmissing72.210.02.891.01.0-0.12
242026-08-01isolation bedstreatment176.993.250.1444.21missingmissing76.790.00.21.01.0-0.04
252026-08-02isolation bedstreatment175.184.130.1365.3missingmissing74.350.00.831.01.0-0.05
262026-08-03isolation bedstreatment174.864.20.1335.32missingmissing74.860.00.01.01.00.0
272026-08-04isolation bedstreatment195.261.380.171.54missingmissing90.624.640.01.01.00.12
282026-08-02onset reportsonsets181.880.180.2830.05missingmissing63.320.018.561.01.0-0.28
292026-08-03onset reportsonsets1145.591.180.6670.14missingmissing64.4281.170.00.01.00.64

Outbreak size estimated by each data stream

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

Per-stream infection-count table
julia
streams_C_table = streams_table(
    "exports" => posterior_C_exports,
    "deaths (DRC)" => posterior_C_deaths,
    "cases (DRC)" => posterior_C_cases,
    "confirmed (DRC)" => posterior_C_confirmed,
    "isolation (DRC)" => posterior_C_treatment,
    "onsets (DRC)" => posterior_C_onsets,
    "joint" => posterior_C_joint);
7×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1exports3316.011843.024612.0122568.0325104.03.29286e6
2deaths (DRC)19741.027310.032900.049327.065124.0113411.0
3cases (DRC)23440.026104.028605.036432.044752.078722.0
4confirmed (DRC)33798.041924.047987.063453.080877.0136872.0
5isolation (DRC)8066.09509.010555.013058.015664.024817.0
6onsets (DRC)12272.019025.025343.041489.057115.0101965.0
7joint6991.07878.08570.010324.011543.014550.0

The first figure shows each single-stream fit's cumulative-infection trajectory projected to the cut-off, with a dotted rule in each stream's colour marking where its data stops and the ribbon beyond it becomes a forward projection.

Per-stream projected-trajectory plot
julia
# Per-draw cumulative-infection trajectory carried by each single-stream
# fit out to the cut-off on day `n`, so streams whose data ends earlier are
# still projected to today.
function _cuminf(chn)
    mat = chn[:cumulative_infections]
    return [collect(v) for v in vec(collect(mat))]
end
# Grid day a stream's data last reports, used for the dotted rule. The
# suspected case and death histories freeze at 26 May; exports and confirmed
# run to the cut-off.
_last_day(days) = isempty(days) ? nothing : maximum(days)

stream_traj_fig = plot_stream_trajectories(
    [
        (; label = "exports", trajs = _cuminf(chn_exports),
            last_day = _last_day(vcat(obs.export_case_days,
                obs.export_death_days)), colour = :seagreen),
        (; label = "deaths (DRC)", trajs = _cuminf(chn_deaths),
            last_day = _last_day(obs.deaths_history.days),
            colour = :firebrick),
        (; label = "cases (DRC)", trajs = _cuminf(chn_cases),
            last_day = _last_day(obs.reported_history.days),
            colour = :steelblue),
        (; label = "confirmed (DRC)", trajs = _cuminf(chn_confirmed),
            last_day = _last_day(obs.confirmed_history.days),
            colour = :goldenrod),
        (; label = "isolation (DRC)", trajs = _cuminf(chn_treatment),
            last_day = _last_day(obs.isolation_history.days),
            colour = :darkorange),
        (; label = "onsets (DRC)", trajs = _cuminf(chn_onsets),
            last_day = _last_day(obs.onset_curve_history.report_days),
            colour = :mediumpurple)];
    n = obs.n, seeding = obs.seeding);

The second figure is the posterior density of each fit's cumulative infection count at the cut-off. The x-axis is scaled to a multiple of the joint-fit 90% upper bound so the bulk of the streams stays visible rather than being flattened by the wide, ill-defined confirmed-only tail.

Cut-off infection-count density plot
julia
# Scale the x-axis to twice the joint-fit 90% upper bound, so the joint and
# the streams that track it read clearly while the confirmed-only tail runs
# off the axis rather than dominating it.
density_xmax = 2.0 * quantile(posterior_C_joint, 0.95)

cumulative_density_fig = plot_cumulative_cases(
    "exports" => posterior_C_exports,
    "deaths (DRC)" => posterior_C_deaths,
    "cases (DRC)" => posterior_C_cases,
    "confirmed (DRC)" => posterior_C_confirmed,
    "isolation (DRC)" => posterior_C_treatment,
    "onsets (DRC)" => posterior_C_onsets,
    "joint" => posterior_C_joint;
    scenarios = [], xmax = density_xmax);

Estimate evolution across releases

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

Released estimates and the current-model frozen re-fits
julia
# Released median and 30/60/90% intervals per release, from
# `data/released_estimates.csv`. Each tuple is
# `(date, median, lo30, hi30, lo60, hi60, lo90, hi90)`.
release_evolution = [(string(r.date), r.median, r.lo30, r.hi30, r.lo60, r.hi60,
                         r.lo90, r.hi90) for r in eachrow(released_df)]

# The current model frozen at earlier cut-offs, each its own discrete
# estimate: the matched-McCabe cut-offs (20, 23, 27 May) already computed
# for the matched-in-time comparison below, the 8 June Chamla
# confirmed-case anchor computed for the Chamla comparison, and the
# one-week-back validation fit (`frozen_lastweek`, at `validation_cutoff`)
# already computed for the forecast validation above. All are reused here so
# the current-model estimate at those earlier cut-offs reads against the
# released overlay, including a recent point one week before the cut-off.
# No extra fits are run. Each tuple carries the median and 30/60/90%
# credible bounds from the frozen draws; `round_fn` rounds to a whole count
# for outbreak size, and is passed through unrounded for a continuous
# quantity such as R0.
function _ci369(xs; round_fn = x -> round(Int, x))
    q(p) = round_fn(quantile(xs, p))
    (q(0.5), q(0.35), q(0.65), q(0.20), q(0.80), q(0.05), q(0.95))
end
frozen_by_cutoff[validation_cutoff] = frozen_lastweek
# The cut-offs every frozen fit above was made at, shared by the
# outbreak-size and R0 by-release overlays below.
_frozen_matched_cutoffs = sort(union(frozen_cutoffs,
    [validation_cutoff, default_chamla_cutoff()]))
frozen_matched = [(c, _ci369(frozen_C(c))...) for c in _frozen_matched_cutoffs]

# The current-data, current-model estimate as the cumulative-infection
# trajectory over the day grid (one calendar date per grid day, day 1 is
# the seeding date), summarised by per-day 30/60/90% credible bounds. This
# is the same latent quantity the cumulative-trajectory figure shows, so
# the current estimate rises over time on the release-date axis instead of
# sitting flat. Drawn against calendar dates, it lines up with the
# release and frozen points.
infection_trajectory = let
    mat = chn_joint[:cumulative_infections]
    trajs = [collect(v) for v in vec(collect(mat))]
    # Only over the comparison window — from the earliest release date to the
    # cut-off — not back to the seeding date.
    start_day = obs.n - value(obs.cutoff - Date(release_evolution[1][1]))
    days = max(start_day, 1):obs.n
    dates = [obs.seeding + Day(d - 1) for d in days]
    q(d, p) = quantile(Float64[t[d] for t in trajs], p)
    (dates,
        [q(d, 0.35) for d in days], [q(d, 0.65) for d in days],
        [q(d, 0.20) for d in days], [q(d, 0.80) for d in days],
        [q(d, 0.05) for d in days], [q(d, 0.95) for d in days])
end

evolution_fig = plot_estimate_evolution(release_evolution;
    renewal = frozen_matched,
    renewal_label = "Current model frozen at earlier cut-offs",
    trajectory = infection_trajectory,
    title = "Outbreak-size estimate as data accrued");

Reproduction number estimated by each data stream

The reproduction number each stream implies on its own, one panel per stream with the joint fit overlaid in grey as the reference.

Per-stream implied-Rt plot
julia
# The per-stream fits walk Rt from day 1 (the default `rt_start`), while the
# joint walks from `RT_WALK_LEAD` days before the first situation report; the
# shared `display_start` is the joint renewal start so every stream reads over
# the same established window. `ramp` matches the joint Rt figure.
_rt_walk_start_joint = clamp(_BREAKPOINT - RT_WALK_LEAD, _rt_start_plot, obs.n);
stream_rt_fig = plot_rt_streams(
    [
        (; label = "exports", chn = chn_exports, rt_start = 1,
            rt_walk_start = 1, colour = :seagreen),
        (; label = "deaths (DRC)", chn = chn_deaths, rt_start = 1,
            rt_walk_start = 1, colour = :firebrick),
        (; label = "cases (DRC)", chn = chn_cases, rt_start = 1,
            rt_walk_start = 1, colour = :steelblue),
        (; label = "confirmed (DRC)", chn = chn_confirmed, rt_start = 1,
            rt_walk_start = 1, colour = :goldenrod),
        (; label = "isolation (DRC)", chn = chn_treatment, rt_start = 1,
            rt_walk_start = 1, colour = :darkorange),
        (; label = "onsets (DRC)", chn = chn_onsets, rt_start = 1,
            rt_walk_start = 1, colour = :mediumpurple)];
    joint = (; label = "joint", chn = chn_joint, rt_start = _rt_start_plot,
        rt_walk_start = _rt_walk_start_joint),
    n = obs.n, breakpoint = _BREAKPOINT,
    as_of_date = string(obs.cutoff), seeding = obs.seeding,
    display_start = _rt_start_plot, ramp = RT_INTERVENTION_RAMP);

Reproduction number by release

The reproduction number estimated at each release, the same kind of release-by-release picture as the outbreak-size evolution above. Each release's cut-off reproduction number is drawn as a discrete estimate, a median with nested 30/60/90% interval bars. The current fit's daily over its established window is drawn as the continuous band, and   is marked.

Reproduction number per release with the current-fit band
julia
rt_release_df = CSV.read(
    joinpath(pkgdir(BVDOutbreakSize), "data", "rt_by_release.csv"), DataFrame)
rt_release = [(string(r.date), r.median, r.lo30, r.hi30, r.lo60, r.hi60,
                  r.lo90, r.hi90) for r in eachrow(rt_release_df)]

# The current fit's daily Rt over its established window, summarised per day
# into a 30/60/90% band, reusing the same walk reconstruction the Rt figure
# uses so the band lines up with the per-release points on the calendar axis.
# The band is drawn only from the first release date onward, so it spans the
# same window as the per-release estimates rather than extending back to the
# renewal start. The first release day is the earliest date in
# `rt_by_release.csv` as a grid day; the walk is still reconstructed from the
# renewal start `_rt_start_plot` (the model knot grid) and the window is
# clamped into the reconstructed range so the quantiles never hit masked days.
rt_release_trajectory = let
    rt_walk_start = clamp(_BREAKPOINT - RT_WALK_LEAD, _rt_start_plot, obs.n)
    mat = reconstruct_rt(chn_joint; n = obs.n, breakpoint = _BREAKPOINT,
        rt_start = _rt_start_plot, rt_walk_start = rt_walk_start,
        ramp = RT_INTERVENTION_RAMP)
    first_release_day = clamp(
        value(minimum(rt_release_df.date) - obs.seeding) + 1,
        _rt_start_plot, obs.n)
    days = first_release_day:obs.n
    dates = [obs.seeding + Day(d - 1) for d in days]
    q(d, p) = quantile(collect(skipmissing(@view mat[:, d])), p)
    (dates,
        [q(d, 0.35) for d in days], [q(d, 0.65) for d in days],
        [q(d, 0.20) for d in days], [q(d, 0.80) for d in days],
        [q(d, 0.05) for d in days], [q(d, 0.95) for d in days])
end

rt_evolution_fig = plot_estimate_evolution(rt_release;
    trajectory = rt_release_trajectory,
    ylabel = "Reproduction number",
    title = "Reproduction number as data accrued",
    released_label = "Released estimate (per project release)",
    trajectory_label = "Current model, current data",
    refline = 1.0);

Reproduction number by release and dataset

The same release-by-release reproduction number split into one panel per dataset, so each dataset's history reads against the others and against the joint. Panels share a calendar axis and a y range, and   is marked. Each release's cut-off value is a median with nested 30/60/90% interval bars. A dataset the report also fits on its own carries that fit's current-model band behind its points, built as in the overview above. Confirmed deaths carries no band, so its panel shows release points alone. Only the most recent releases published these per-dataset estimates, so every panel spans a much shorter window than the overview above rather than a different history.

Reproduction number per release by fit
julia
# Schema of the per-release, per-fit estimate tables written by
# scripts/score_releases.jl from each release's stream_estimates.csv.
_by_stream_schema = (; release = String, date = Date, fit = String,
    median = Float64, lo30 = Float64, hi30 = Float64, lo60 = Float64,
    hi60 = Float64, lo90 = Float64, hi90 = Float64)

# Fits in a fixed order, the joint first, so the panels do not reshuffle
# between builds. Labels match the per-stream table above (in "Outbreak
# size estimated by each data stream"). Recovered is absent because it
# has no individual fit.
_fit_order = ["joint", "cases", "deaths", "confirmed", "confirmed_deaths",
    "treatment", "onsets", "exports"]
_fit_labels = Dict("joint" => "joint", "cases" => "cases (DRC)",
    "deaths" => "deaths (DRC)", "confirmed" => "confirmed (DRC)",
    "confirmed_deaths" => "confirmed deaths (DRC)",
    "treatment" => "isolation (DRC)", "onsets" => "onsets (DRC)",
    "exports" => "exports")

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

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

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

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

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

Basic reproduction number by release

The basic reproduction number estimated at each release, the initial-transmission counterpart of the reproduction number above, before the time-varying decline. Released estimates are blue and the current model frozen at earlier cut-offs is red, each a median with nested 30/60/90% interval bars. The current fit sits behind both as a flat band, and   is marked. Releases only began publishing this quantity recently, so the short blue history reflects that rather than any failed release. The frozen series carries the comparison meanwhile.

Basic reproduction number per release with frozen re-fits and the current-fit band
julia
# Per-release R0 points from r0_by_release.csv, read through the typed
# fallback so a missing or header-only file (until a release carries
# `rt_state.log_R0` in its posterior draws) does not break the build. The
# schema mirrors rt_by_release.csv.
_r0_schema = (; release = String, date = Date, median = Float64,
    lo30 = Float64, hi30 = Float64, lo60 = Float64, hi60 = Float64,
    lo90 = Float64, hi90 = Float64)
r0_release_df = _release_data("r0_by_release.csv", _r0_schema)
r0_release = [(string(r.date), r.median, r.lo30, r.hi30, r.lo60, r.hi60,
                  r.lo90, r.hi90) for r in eachrow(r0_release_df)]

# The current model frozen at earlier cut-offs, one discrete estimate per
# cut-off, reusing the same frozen fits `frozen_matched` above already
# computed. No extra fits are run. Each tuple carries the median and
# 30/60/90% credible bounds of that frozen fit's own R0 draws, unrounded
# since R0 is continuous.
frozen_r0_matched = [(c, _ci369(frozen_R0(c); round_fn = identity)...)
                     for c in _frozen_matched_cutoffs]

# The current fit's R0 posterior is a single distribution rather than a
# daily series, so it summarises into a flat 30/60/90% reference band. The
# window runs from the earliest mark on the axis, the first frozen cut-off
# or release point, to the current cut-off, so the band reads behind both
# series rather than only their recent end.
r0_reference = let
    draws = r0_walk_draws(chn_joint)
    q(p) = quantile(draws, p)
    first_date = min(minimum(Date.(_frozen_matched_cutoffs)),
        isempty(r0_release_df.date) ? obs.cutoff :
        minimum(r0_release_df.date))
    dates = [first_date, obs.cutoff]
    (dates, fill(q(0.35), 2), fill(q(0.65), 2), fill(q(0.20), 2),
        fill(q(0.80), 2), fill(q(0.05), 2), fill(q(0.95), 2))
end

r0_evolution_fig = plot_estimate_evolution(r0_release;
    renewal = frozen_r0_matched,
    renewal_label = "Current model frozen at earlier cut-offs",
    trajectory = r0_reference,
    ylabel = "Basic reproduction number",
    title = "Basic reproduction number as data accrued",
    released_label = "Released estimate (per project release)",
    trajectory_label = "Current model, current data",
    refline = 1.0);

Basic reproduction number by release and dataset

The basic reproduction number estimated at each release, one panel per fit, the by-dataset counterpart of the figure above. Panels share a calendar axis and a y range, and   is marked. Each release is a median with nested 30/60/90% interval bars. Every fit the report runs on its own also carries a current-model reference band. Panels fill in from the first release that publishes this quantity per dataset, so a fit with nothing saved yet is left out rather than drawn empty.

Basic reproduction number per release by fit
julia
# Per-fit R0 flat reference band, the by-dataset counterpart of
# `r0_reference` above, a single distribution rather than a daily walk, so
# each fit's band is flat across its own release window. `r0_walk_draws`
# probes for the walk base, so a single-stream model built without its own
# renewal walk drops its band instead of erroring.
function _r0_stream_trajectory(chn, dates)
    draws = r0_walk_draws(chn)
    isnothing(draws) && return nothing
    q(p) = quantile(draws, p)
    first_date = isempty(dates) ? obs.seeding : minimum(dates)
    ds = [first_date, obs.cutoff]
    (ds, fill(q(0.35), 2), fill(q(0.65), 2), fill(q(0.20), 2),
        fill(q(0.80), 2), fill(q(0.05), 2), fill(q(0.95), 2))
end

# Build a fit label => trajectory dictionary from a per-release R0 table,
# restricted to the fits `_stream_chains` names, the same restriction the
# reproduction-number-by-dataset trajectories use. A fit with no row in
# `df`, or whose chain carries no walk base, gets no trajectory, so its
# panel still draws its release points alone.
function _r0_trajectories(df)
    trajs = Dict{String, Any}()
    for (fid, cfg) in _stream_chains
        fdates = df.date[df.fit .== fid]
        isempty(fdates) && continue
        traj = _r0_stream_trajectory(cfg.chn, fdates)
        isnothing(traj) || (trajs[get(_fit_labels, fid, fid)] = traj)
    end
    return trajs
end

r0_stream_df = _release_data("r0_by_release_by_stream.csv",
    _by_stream_schema)
r0_stream_fig = plot_evolution_by_group(_fit_groups(r0_stream_df);
    trajectories = _r0_trajectories(r0_stream_df),
    ylabel = "Basic reproduction number",
    title = "Basic reproduction number as data accrued, by dataset",
    released_label = "Released estimate (per release)",
    refline = 1.0,
    empty_note = "No per-dataset basic reproduction numbers saved yet.");

Comparison with McCabe et al.

Our model is a discrete-time renewal model with a time-varying reproduction number and every data stream fitted jointly. McCabe et al. published their estimates as scenarios at fixed situation-report cut-offs, each scenario carrying a 95% confidence interval. We show all three, the 18 May report, the 20 May update and the 27 May Lancet publication, as one panel each, with their intervals kept. Within a panel each method and scenario family is a single line, carrying its sweep over the nuisance assumptions: the case-fatality ratio, the geographic window and the doubling time. The geographic-spread scenarios come from exported cases and travel volume. Their back-calculation-from-deaths scenarios differ between the reports, since the 18 May report used 88 reported deaths and the 20 May update 131. The 20 May update also corrected the case-fatality ratios. McCabe's scenarios estimate cumulative cases at their report dates, though their report is not fully explicit about whether this is symptomatic cases or all infections. We take the like-for-like quantity to be our cumulative symptom onsets on the same dates, not the latent infections (which include the not-yet-symptomatic) or our current cut-off total. We read our value off the joint fit's cumulative-onset trajectory at the grid day for each report date, and show it with its credible interval. Each scenario sits beside our estimate for the date it was made: the 18 May report against our 18 May value, the 20 May update against our 20 May value, and the 27 May Lancet publication against our 27 May value.

McCabe scenarios with uncertainty against our estimates
julia
function _ci90row(xs)
    (round(Int, quantile(xs, 0.5)),
        round(Int, quantile(xs, 0.05)),
        round(Int, quantile(xs, 0.95)))
end

# Our modelled cumulative symptom onsets on a McCabe report date, read off
# the joint fit's per-draw `cumulative_onsets` trajectory. The grid runs to
# the cut-off on day `n`, so the day-index for a date is `n` minus the days
# from that date back to the cut-off (`grid_day("2026-06-07") = n`,
# `"2026-05-20") = n - 18`, `"2026-05-18") = n - 20`).
_onset_trajs = let mat = chn_joint[:cumulative_onsets]
    [collect(v) for v in vec(collect(mat))]
end
# Inverse of `grid_date(day) = obs.cutoff - Day(obs.n - day)`: the day-index
# whose calendar date is `date`, using `value` (imported above) for the
# day count rather than the non-exported `Dates.date2epochdays`.
_grid_day(date) = obs.n - value(obs.cutoff - Date(date))
function _ours_on(date)
    d = _grid_day(date)
    _ci90row(Float64[t[d] for t in _onset_trajs])
end

# Our matched cumulative-onset estimate for each report date, keyed by date so
# it lands beside that vintage's scenarios in its own panel.
mccabe_ours = Dict(
    "2026-05-18" => _ours_on("2026-05-18"),
    "2026-05-20" => _ours_on("2026-05-20"),
    "2026-05-27" => _ours_on("2026-05-27"))

# One panel per report date; within a panel each method-and-family is one row,
# with the case-fatality / window / doubling-time sweep dodged onto that single
# line, so the ~40 scenarios keep their intervals without becoming ~40 rows.
matched_comparison_fig = plot_scenario_comparison(REPORT_SCENARIOS_CI;
    ours = mccabe_ours,
    date_titles = ["2026-05-18" => "18 May report",
        "2026-05-20" => "20 May update",
        "2026-05-27" => "27 May (Lancet)"],
    xlabel = "Cumulative cases");

The McCabe scenarios are outbreak-size estimates, the same quantity our renewal model and the released integral model report. Their 95% confidence intervals come from exact negative-binomial counts for the geographic-spread method and a Poisson likelihood profile for the back-calculation from deaths.

Frozen-fit C_T intervals (kept for the CSV export, not shown)
julia
# The estimate-evolution figure above already shows how the size estimate
# shifts as data accrues, so the side-by-side frozen-fit table is no longer
# rendered in the report; it is kept only to populate the published
# `frozen_matched_cutoffs.csv` export.
frozen_streams_table = streams_table(
    "frozen 20 May" => frozen_C("2026-05-20"),
    "frozen 23 May" => frozen_C("2026-05-23"),
    "frozen 27 May" => frozen_C("2026-05-27"),
    "frozen 8 June" => frozen_C(default_chamla_cutoff()),
    "current data" => posterior_C_joint);

Comparison with Chamla et al.

A second group, Chamla et al. (Chamla et al., 2026) at the World Health Organization Regional Office for Africa, published a stochastic compartmental model of the same outbreak on 25 June 2026. Their model is a discrete-time susceptible-exposed-infectious-recovered-dead ensemble, recalibrated by simulation filtering to the laboratory-confirmed case series and anchored on the 598 confirmed cases reported by 8 June. It is then run forward to project the confirmed-case trajectory under a low, central and high transmissibility scenario.

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

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

Project the 8 June fit forward and assemble the Chamla comparison
julia
# The 8 June frozen joint fit matches Chamla's confirmed-case calibration
# anchor exactly and carries the confirmed-case testing history through then,
# so we roll its confirmed-case stream forward with the one-week-ahead forecast
# machinery to the dates Chamla report.
chamla_anchor = frozen_by_cutoff["2026-06-08"]

# Our projected cumulative confirmed cases at a horizon of `h` days past the
# 8 June cut-off: a forward `forecast_reported` run (its reproduction number
# left to keep evolving), summarised as (median, 5%, 95%).
function _our_confirmed_h(h)
    fc = forecast_reported(chamla_anchor.chn;
        horizon = h,
        obs_cases = chamla_anchor.o.reported_cases,
        obs_deaths = chamla_anchor.o.total_deaths,
        obs_confirmed = chamla_anchor.o.confirmed_cases,
        obs_confirmed_deaths = chamla_anchor.o.confirmed_deaths)
    return _ci90row(float.(fc.confirmed_cum))
end

# Our projection at Chamla's forward report dates (10 and 24 June, week 12):
# the anchor day is the fitted confirmed total at 8 June, each later date a
# forward forecast. Reused for the matched-date table and the week-12 figure.
chamla_fan = map(["2026-06-08", "2026-06-10", "2026-06-24"]) do d
    h = value(Date(d) - chamla_anchor.cutoff)
    row = h == 0 ?
          (chamla_anchor.o.confirmed_cases, chamla_anchor.o.confirmed_cases,
        chamla_anchor.o.confirmed_cases) : _our_confirmed_h(h)
    (d, row...)
end
_fan_at(date) =
    let r = first(x for x in chamla_fan if x[1] == date)
        (r[2], r[3], r[4])
    end
ours_10jun = _fan_at("2026-06-10")
ours_24jun = _fan_at("2026-06-24")

# Observed confirmed cases over the comparison window: the daily cumulative
# series read off the chain's grid from 18 May (Chamla's first projected point)
# to the cut-off.
chamla_obs_series = let
    ds = [grid_date(d) for d in obs.confirmed_history.days]
    cs = obs.confirmed_history.counts
    [(string(ds[i]), cs[i]) for i in eachindex(ds) if ds[i] >= Date("2026-05-18")]
end

# Chamla's central confirmed-case projection over the comparison window; their
# later, far-larger horizons are noted in the text rather than plotted so the
# window stays legible.
chamla_central_window = CHAMLA_CONFIRMED_CENTRAL[1:4]

chamla_projection_fig = plot_projection_comparison(;
    external = chamla_central_window,
    ours = chamla_fan,
    observed = chamla_obs_series,
    external_label = "Chamla et al. central (R₀=1.71)",
    ours_label = "Our projection (from 8 June)",
    observed_label = "Observed confirmed",
    title = "Confirmed-case projections versus observed, from mid-May");

By 24 June their central scenario projected just under a thousand confirmed cases, and their low and high scenarios ranged from roughly 870 to 1360. The figure below sets that week-12 scenario spread beside our 8 June projection for the same date and the confirmed count observed by the cut-off, so each reads against their three scenarios at a glance.

Week-12 (24 June) scenario spread against ours and observed
julia
chamla_w12_rows = vcat(
    [(label, m, lo, hi) for (label, m, lo, hi) in CHAMLA_CONFIRMED_W12],
    [("Our projection (from 8 June)", ours_24jun...)],
    [("Observed by 23 June cut-off", obs.confirmed_cases,
        obs.confirmed_cases, obs.confirmed_cases)])
chamla_w12_groups = vcat(fill("Chamla et al. scenarios", 3),
    ["Our projection"], ["Observed"])

chamla_w12_fig = plot_estimate_comparison(chamla_w12_rows;
    xlabel = "Cumulative confirmed cases by 24 June",
    groups = chamla_w12_groups,
    group_colours = ["Chamla et al. scenarios" => :steelblue,
        "Our projection" => :firebrick,
        "Observed" => :black]);

The matched-date numbers behind these figures are in the dropdown below, with the observed column taken to the 23 June cut-off.

Matched-date projection numbers (10 and 24 June)
julia
chamla_comparison_table = let
    fmt(t) = string(t[1], " (", t[2], "–", t[3], ")")
    central(date) =
        let r = first(x for x in CHAMLA_CONFIRMED_CENTRAL
            if x[1] == date)
            fmt((r[2], r[3], r[4]))
        end
    DataFrame(
        "Date" => ["10 June", "24 June"],
        "Chamla central (90% PI)" => [central("2026-06-10"),
            central("2026-06-24")],
        "Our projection (90% CrI)" => [fmt(ours_10jun), fmt(ours_24jun)],
        "Observed confirmed" => [
            string(freeze_observations("2026-06-10").confirmed_cases),
            string(obs.confirmed_cases) * " (23 June)"])
end;
2×4 DataFrame
RowDateChamla central (90% PI)Our projection (90% CrI)Observed confirmed
StringStringStringString
110 June648 (470–812)680 (618–825)676
224 June990 (709–1293)1684 (868–4131)4567 (23 June)

Beyond the comparison window their central scenario continues to roughly 8200 confirmed cases by mid-September, with the high scenario far higher. Those longer projections are not set against data here.

Reproduction number behind the projection

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

Reproduction number as estimated by the 8 June fit
julia
# Reconstruct the reproduction-number trajectory the 8 June fit estimated,
# mirroring the current-data R_t figure but with the frozen vintage's own grid,
# breakpoint and renewal start.
chamla_rt_obs = chamla_anchor.o
chamla_rt_breakpoint = chamla_rt_obs.n - chamla_rt_obs.who_first_sitrep_days
chamla_rt_start = clamp(
    chamla_rt_obs.n - round(Int, chamla_rt_obs.tmrca_days) + RENEWAL_START_LEAD,
    1, chamla_rt_obs.n)
chamla_rt_fig = plot_rt(chamla_anchor.chn;
    n = chamla_rt_obs.n, breakpoint = chamla_rt_breakpoint,
    rt_start = chamla_rt_start,
    rt_walk_start = clamp(chamla_rt_breakpoint - RT_WALK_LEAD,
        chamla_rt_start, chamla_rt_obs.n),
    as_of_date = string(chamla_rt_obs.cutoff),
    seeding = chamla_rt_obs.seeding, ramp = RT_INTERVENTION_RAMP);

Delay sensitivity

The death stream dates the outbreak from how far deaths lag symptom onset, so the assumed onset-to-death delay sets the implied infection count. The baseline uses the hospital-pathway delay from the Isiro 2012 line-list reanalysis (onset to admission then admission to death, implied mean about 12 d). We re-fit the joint model under the community-pathway delay from the same reanalysis: the delay for deaths that occur in the community without a recorded admission. This delay is shorter (implied mean about 8 d). Both pathways come from the line list, so this varies the actual delay assumption rather than an arbitrary scenario. The re-fit uses the full headline settings: 1000 draws across two chains.

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

Re-fit the joint under the community-pathway onset-to-death delay
julia
# The sensitivity re-fits (community-delay variant) are
# defined in the fit registry (`docs/fits/registry.jl`) and loaded through the cache
# (when enabled) in the setup block above.
posterior_C_community_delay = RUN_SENSITIVITY ?
                              vec(Array(chn_joint_community_delay[:C_T])) : nothing
1000-element Vector{Float64}:
 11470.434558356505
 17647.852872812644
 19841.619673568635
 16946.124751239546
 13530.867115782401
 11635.628573918137
 10888.649245206483
 11983.99351336981
 11288.146504532713
 10552.984530730319
 10378.828520469007
  7343.688782650602
  7083.921673545357
  7671.450633419554
  7199.792887167545
  7998.698375876502
  7904.939728261323
  7790.105936716705
  6963.787279927257
  6524.903346143492
  7151.304827678792
  7248.075844876781
  7904.391775317243
  7710.802288195973
  8725.181522984749
  8530.675133279758
  9186.960004660341
  7389.255897040006
  7733.355097778021
 10113.44608724201
  8124.801751482269
  9303.48865624667
  9399.196350613716
  9659.795544293436
 13127.516229768908
  8015.840954921742
  9314.317137654658
  7821.488346183867
  9632.649324852217
  9996.415742567291
  9024.32450791996
  9375.949651256684
 12993.883351113163
 12048.138463235691
 11087.543659493349
  9377.996017391642
  9562.547389169933
 10891.39352708183
 12479.977953181718
 11083.50770962046
  8363.74793910918
 10246.31056438247
 11277.456245237421
 11073.028633009073
 15138.533280544452
 12309.31334194566
 17105.017154191155
 15060.699665478875
 17658.430920471856
 11867.193848958908
  8476.534047674772
 10302.437453926617
  8641.58115144213
 12311.102798248645
  9101.938153433255
 11318.23107433699
  9964.748012555241
 12207.082317602597
  9269.384872861652
  6540.0110266846505
  7657.584742729918
  6806.696371458162
  6718.734383654959
  7121.759364525644
  8009.650913046976
  8562.419886228718
  8218.342448021038
  7219.49843453378
  6553.849400545668
  7169.942297617953
  6133.135041097059
  6315.018058132472
  7034.30375092659
  6788.826974775935
  7791.47392233857
  7085.9327837199235
  7367.207389641797
  7212.827139824482
  8370.267431136897
  8148.863246159572
  9695.451353327982
  8169.950129488801
  6903.469849507669
  9194.019187105065
  9350.854073828674
  9146.7136923431
  9922.106379824874
  7804.123332173352
  8240.025653476418
  8497.238087539874
  8273.337673839078
  8362.315985656773
  8411.876972175058
  9161.881735311968
 12082.155793336566
  9740.928851755769
  8470.393454019568
  7131.052466721089
  7774.254272264608
  8720.756597902064
  7997.291413909275
  7739.230519373332
  9434.926553177298
  9906.683391042065
  9844.537018390603
 12283.477577817772
  8483.60323189627
  9952.978885709907
  8682.007809913683
  8201.27329833232
  8342.09634306206
 10849.04612223225
  9737.911187072355
 11392.000426108765
  9263.129949104268
 12862.181364027207
  8624.982315848267
  7698.653026042852
  7306.647748702779
  8079.004160765482
  8382.662071005634
  6159.6616113838245
 10258.581434839072
  9314.76743036206
 12032.67608902322
  8397.225260857178
 11037.299307097304
 10152.67251502942
  8531.196587235532
  7880.279277373684
  7822.80660052159
  7517.456029244223
  7271.669750274472
  9048.119704521527
  9282.63327510678
  7222.629004076914
  6985.555400362133
  6729.52650242002
  6457.4032507363645
  6528.2549107053665
  8733.527271396419
  7469.032956449368
  8991.611779232247
  8039.4249106382795
  8393.872730187722
  7302.2085572723
  9776.751957057626
 10720.539135064464
  8524.261275724657
  6230.82006123389
 10465.580224111925
  7891.130460778127
  8023.062664863127
 10068.61128899371
  9774.153143602833
  9687.10409221395
 12741.717255737361
 13981.596832757943
  9065.856280832148
  7855.563088265608
  9083.975293301737
  9617.420437626866
 10427.883398116312
 10018.84559505466
 13185.383499963085
 10409.224997191557
 13069.397261530807
 11456.295841226234
 11114.649475778733
 10377.677040743523
  9603.607705051598
 10327.936294973957
 10193.276847288413
  9425.203439020333
 10426.054134470985
 12796.366464655539
  9698.80835464121
  7632.234573188497
  7639.01754397523
  8361.468983156401
  7817.866174783315
  9339.854430727743
  7736.331390013123
  8146.399248798154
 11629.89775894495
  7173.581724715031
  7383.1496003718685
  7694.196307030368
  7933.7576991447395
  9935.823917427317
  9508.6742968621
  8907.668846724457
  9414.452271799555
 10722.250352218802
  9281.831946956292
  8649.42612294801
  9071.590073494655
  7547.557120173624
  8028.489874660446
  6790.867148926102
  7219.615177150186
  8693.697692259497
  9877.00424691551
  8719.269039259627
  8137.590810493594
  8891.847886185604
  9176.593506134008
 12982.47348266019
  8329.754731249166
 10446.727749962942
  9194.21636593267
  8488.218226229805
  9221.135065997398
  9430.637521620587
  7884.993714767032
  7981.719310792603
  7925.57055579704
  9372.292687072077
  8725.945091770704
 10640.534035719518
 11958.870449300628
 16228.031024774833
 11875.193642688042
 11469.297973192924
 12362.482765847839
 12376.686630946619
  7273.792337655598
  6547.651355515894
  6591.333452504169
  8635.735162769392
  7995.377557854011
  7291.570235976039
  7306.911378261249
  7457.695303528274
  6804.304679051519
  9719.918259632625
  7521.575875500336
  8521.317761394654
  7802.988795300116
  8558.890640692247
  7768.34665896699
  7828.184440215491
  7304.704013752854
  7806.76931233583
  8501.42726585547
 10158.384627225056
 14178.635564975792
  8218.0459744922
  7355.035388329058
  7184.432755222432
 13059.079979405751
 10944.812096030746
 12309.813555290155
 11816.9334929417
 14020.856808897248
 11359.684548190015
  7951.599531079462
  8446.99723879314
  7915.863756036016
  7642.860157136096
  8200.616006102598
 10576.769108984447
  9975.25721077406
  9483.291813593149
  9080.874269316493
  7858.58655874567
  8555.964908162603
  8574.435338586949
  7749.32288331217
  7607.91929324167
 10518.720137132921
 10478.791891780898
 13753.833489512945
  9690.814809832564
  8921.160065249598
 11817.705030215537
 10580.938772352529
 10259.718988200764
 10415.096775069409
  8603.482912978527
  9377.228552255425
  8795.398287349926
  8923.605275283788
  8782.313460599818
  7983.428529062681
  7045.73154385247
  8912.783736302146
  9898.877765280891
  9364.226185433186
 11104.273434899555
  9565.079596641042
 16300.68075319957
 11856.97666587357
  8984.658382339356
  9688.29156064713
  8438.187997706375
  8246.937286831551
  9508.531366788715
  9551.307036586777
  8827.22760247249
  8173.092908444735
  8920.130743479072
  8495.214338901944
  8426.61877177679
  7667.443355272255
  6168.2814720081105
  8795.094040183207
  8283.802690881379
 11103.018819348345
 10786.97199366885
  9517.587334360449
  9273.83307505195
 10473.534274013231
  7275.106781136707
  8118.723720554992
  7580.488489634395
  8604.583800695007
  7345.621586989206
  7468.131138946896
  9494.830734673837
  6994.380055963225
  7230.7463738743645
  7585.372588149527
  7974.04678092275
  7438.692855176513
  9519.746528059859
  8960.429176918133
  7681.908368751849
  8086.230763230881
 10787.398002796781
  9307.756600193794
  7730.549724541536
  8988.26075916602
  6098.96618963552
  6355.4310749937085
  7070.963433747034
  7122.929227725176
  7550.973831097331
  9575.396861631187
 14418.674436436773
  9077.763495181225
 11084.998916590284
  9302.362906443435
  9020.387407294926
  8341.369269735671
  9695.718704988312
  8538.562801603239
  8038.47456478877
 11390.78132235118
 11408.261660531656
  9217.510633199507
 11585.117683646327
  8022.805145488282
 11141.78509342215
 10014.914292051813
 11292.388344402672
  9577.90621668032
 10723.673634674487
  9460.889344647401
 11981.532594700904
  9696.621816230954
 13140.874565031807
  7868.773384916161
  9679.876994060518
  6575.217051474084
  6724.381755549946
  7584.848857263356
  6627.624471184898
  7123.525528980531
  7810.843218313852
  8340.50651790811
  8576.141276196326
  9710.008570980077
  8612.058810621973
  8012.764684609119
  7375.403950403218
  6287.120500920186
  7735.345844692861
  8234.193376811952
  8116.314061449641
 10066.502133302025
 11878.469511006559
 10359.432394629088
 10142.309832557245
  7862.176675354602
  8101.922317256901
 10335.335027700705
 10263.466074528602
  8253.964204331689
  9164.303643359954
  6923.780748580175
  7451.429902131897
  5971.295305050322
  7382.469712164665
  8139.24262888894
  7838.595922655787
  7178.957850775545
  7876.222813340969
  8717.534251892126
  9902.776932672088
 10830.752646204257
  7548.948137458147
  7940.033575638925
  7947.692981541783
  5942.544347220783
  7063.731563365567
  7135.292064967398
  5851.658907279822
  6175.775493083874
  6643.64424255374
  6319.900789804125
  9670.723075430085
  9925.028768259734
 11358.262842603941
 11741.925424314712
  8586.363586116284
  9781.927042208798
 15257.306563997767
  9946.780032190403
  7189.207060215672
  9011.560872512893
  7775.489190883804
  8064.689665186008
  7136.920753124348
  7723.363396902433
  8149.343482725584
  6598.611311240025
  7193.463156724747
  6742.96883292356
  6418.038025919264
  8209.118590296624
  8131.313804124679
  7518.263550925819
  7590.71545462456
  8847.212303047192
  8444.395159413738
  7138.0949876083105
  7534.761655129724
  7246.162856011721
 11610.99861007339
 21478.924818810458
 12102.709780088213
 11751.084468981697
  9158.553986765592
  9202.937056065544
  8993.46698857077
  9344.512956155724
  9046.63918924729
 11378.690542060123
  9926.02818097181
  9389.481767059182
  7371.898826458279
  7292.498425562162
  7304.631860141676
  6828.611230747649
  8392.72041569773
  7403.122278637507
  6724.591968725394
 10933.81595178106
  6283.834499439581
  8092.879743817521
  6045.514082698286
  6018.665559426347
  6780.180222222722
  9134.461010046973
  8693.717310216327
  8832.04247080115
  8940.648549268368
 10779.375014065285
 11751.70956642081
  9515.913423995718
  7241.397279880713
  8263.34160752154
  8176.138342068046
  7395.137098184386
 10015.81413473404
 12530.246012526777
 10113.76067811589
  9359.422719345252
  8434.840715414975
  9414.877828941479
 12386.012060712384
  9308.035323617733
  6731.343068934151
  7452.012758281711
  8432.310056099648
  7907.0324679176565
  7043.606523613414
  8086.1389038354055
  9003.666672306194
  6825.450623624853
 10888.420185975549
 10929.799220001149
  7466.927370457726
  6878.661989445836
  7489.231836171677
  6994.010395290496
  6990.097941415526
  7079.522433669575
  6785.609213673625
  6666.385658768065
  6760.009092399052
 13900.486491505939
 10191.61045035578
  8664.831362140461
  7829.412820822347
  9078.289461175142
 12608.974767728612
  8562.065979568846
 10296.630748817453
 13917.712308554506
  8681.042295260826
  8348.74858641874
  7624.719647919449
  8106.892515172932
 12164.68552216857
  9371.988142278466
  9063.428848678577
  8184.450317441601
  8476.808742985675
  7090.685186852457
  5490.083677662255
  9854.675874189505
  6424.667930561883
  8204.498315983305
  8323.138259442801
  8978.449833301403
 11032.938045309567
 14558.569287506903
  6480.141105633644
 10724.947476964982
  8590.422598835361
  9747.813457499773
  8893.949949149233
  6823.968938056461
  7474.583862293313
  8733.132065890371
 10349.796787742536
 10257.046484207656
 10135.52151166347
  7647.109141713755
  7911.825234383504
 15438.838203214425
  8418.355119143305
  9299.591525337291
 14209.554056925035
 11046.413834471716
  9276.771615267244
 10732.454156828135
  9275.839984927534
 12026.883234514542
  8543.905763418603
  7525.470729002792
  8611.67401685673
  8728.348957050624
 10975.423344004992
  8135.064216464076
  8285.696505584974
  8575.739514873876
  7663.5903789971035
  9458.394315993612
 12826.506303930644
  7865.423250039602
 10543.795172652164
 10117.176925638445
  8605.003750430982
  7427.491107526343
  6974.9756767162235
 13528.6052878315
  6626.163910214662
  7392.751597219963
  9484.43497709696
  7003.711407640945
  5883.952041926352
  7457.158928469295
 10619.210909440653
 10392.848094915607
  7002.774042842268
  6573.976819582421
  6175.790831706767
  9552.306996071582
  8912.719569834431
  8181.204452542623
  8217.539416153659
  9196.467521800521
  7748.349554782235
  7335.299237913286
 13286.745582454358
 10657.366357312922
 11064.165489319343
  6564.377006728112
  7526.922033420688
 10240.681824103225
 11312.285250213383
  6727.536787714884
 11081.004087630325
  6845.783795185666
  7672.0421656609415
  5906.8096673976515
  6413.242854342971
 11218.21912514674
  7282.362235896431
  6725.763131896914
  7899.655146682789
  6243.920914897018
  6494.097635405847
 16721.83011599198
  9173.21823786553
  7169.5146999884955
 11072.007893595926
 14650.192840458143
  7818.641754053388
  8329.883297281716
 11071.675425103684
  8662.093047104318
 13289.772086096362
  9264.112697802037
  7799.846905086368
  7732.365770913777
  9086.365974429671
  9238.178214460246
 11733.706706470297
  9215.748257699013
  9181.894435035421
  8481.1939548651
  9671.304877086026
  7914.388775935753
  9781.051582859045
  8719.41184462173
 11404.363343928559
  9001.545372888653
  8933.489365070478
  7964.842002001552
  7350.213585693367
 15530.435765832213
  8673.861276189331
  7221.709628232372
  5686.768991906578
 12370.758052676343
 10731.668677865138
  7754.925733598549
  7316.369980953159
  9818.152109754423
  7793.566411842692
  6664.294717953022
 14755.838558913658
  8762.60423306944
 10934.533835703856
  7711.511436175632
  8734.019961385047
  8306.307690124488
  9060.312519156132
 14231.622225425735
  6935.535869744885
  8718.375303099989
  7184.22039025565
  8575.867418344898
  7274.529229910949
  6870.003824862587
 14428.476133330601
 12501.917129153542
  8262.995279739638
  7524.955078511903
  7587.998504075663
  8979.550906023023
  8309.906813963837
  8864.475140836987
  7565.638499582271
  8455.633123962583
  8445.805780194893
  8253.76283336377
  7226.4282721604695
  8283.633459485583
 10737.363196031165
  8277.359090119722
  7727.311111228579
  7886.131314040654
 10433.883922309427
  9029.903076623066
 10087.752566089268
  8307.663475962623
  9375.927627955132
  7819.355586058477
  8563.861367521718
  7630.2979360966165
  8319.239280115176
  7527.677042811451
 10571.555512573263
  7646.849283328561
  9021.775627608764
 10488.400652858376
  8161.261118203704
  9688.87862353141
  8183.389139628398
  9373.27136681133
 11764.848584578964
 10769.239975850563
  7921.276589106514
  8654.527458556799
 10079.027338274682
 10671.696939146948
 12483.989903297626
  7336.828269593766
 11864.530856386855
 12190.629013562702
 10078.384383743394
  9538.21517523596
  7556.632792467154
  9312.538231270812
  8020.555599732681
  8742.860327514505
  9605.217917249287
 10430.798743754856
 10201.613218959406
  6197.90713739825
  8069.705971815565
  7891.10305815581
  6369.208378650274
  8518.842557601809
  5773.5620133568455
  8334.292857968501
  6531.276909770767
 10687.44391440346
  8099.438904937511
  8453.611432479202
  8164.6779901500295
  8545.291386489876
  6648.393000702346
  7878.417189749862
  8971.065599323934
  7980.833567922395
  9213.879340285199
 10228.73010460394
  7981.07515105513
 10565.671208799527
 10751.637321866006
 11441.16964226457
  9116.835210688874
  9419.140120293847
  9368.696876673186
 11399.512704359413
  8157.964014603891
 12552.992616276906
  9243.730645840214
  8029.491328468883
  7436.122015229488
  8893.492274943379
  8717.789917876757
  6734.083445708015
  8415.25607322002
  9556.693417362238
 10768.519951760358
  9263.166240074675
  7509.061136954393
  8406.920611866943
  7810.647095436902
  8703.311033129428
 12051.904471517628
 11436.867633241267
  8603.661727648521
  8562.916654028386
  7437.12552319722
  7155.691482871512
 10243.48318895143
  9903.462959676755
  9781.462600998522
  8268.137610560094
 10528.214651349484
  8609.767317176793
  7488.474973129615
  8878.175309222841
  9424.999577320612
  6925.788983673197
  6599.205327890588
 10050.630808492744
  9244.135832084467
 11236.007500019143
  9895.183815590355
  6873.901136117553
  6676.7075462012
  6140.174115426491
 10636.870545510312
  7927.1408958584525
 13177.93673813071
 13692.051959214852
  8863.954931818373
  9999.4565702812
  7822.4191406261225
 10177.448475705507
 23010.730229321332
  6227.914911594655
  7924.50422002433
  6021.236976127832
  8782.059400768585
  9801.388593909898
  8274.333608210956
  9402.857650733997
 10604.376633638874
 10514.961939317875
  8136.131861303404
  7695.018791658209
  8750.343195412515
 13887.566959431108
 10138.167593392873
 10349.24775201668
  9386.837537894036
  9701.320171826637
 10085.322942534367
  8380.926668657994
  7802.37969384282
 11647.060865810157
 11343.557059763638
  6868.011980614077
 12217.861067613892
 11330.519014242784
  7073.798511684181
 10528.884611916026
  7827.360279672586
  7351.704129986214
  6185.123079697612
 10820.60533426527
  6620.474512209989
 11442.94019176909
  9497.933820014132
  9568.132730785977
  8182.745265776267
  6281.009784582323
 10206.985527213796
 12251.561325624853
 11428.033235924044
  8274.784113250791
  8122.692071796326
  8898.936600139557
  7577.226526077441
  8151.4281651220335
  6734.979218920781
  9251.225673137416
 11092.778442212983
  9129.70798090559
  8391.150664935241
  9551.439967294584
  7694.390592141872
  9171.217854406987
  7628.371378105852
 11414.918784702186
  8469.020064391922
  9089.041785929085
  7394.357163095162
  7227.571679644795
  7947.236171015138
  6856.726723496992
 10521.970538872221
 11436.302610302533
  8107.451061165572
 14603.415867705386
  7261.425336140777
 12618.730527520589
  7274.3388200443815
 13573.277326568856
  8132.24044417844
  8052.880350340318
  9294.886099156842
 11724.475466334541
 12307.607520348449
  8109.2519411554085
  9912.829543928814
  9103.026173988408
  8628.145927594443
  7607.568343087471
  6515.673726825748
  6348.750606121307
  6482.610402907932
 10019.399984941498
  9525.28299304805
 11573.94791767558
  7390.268346673133
  7318.697506313203
  8651.583707797052
  7387.418310936691
  7825.933427582545
  9168.840693356366
  9505.328886511516
  8313.797266419451
 12094.13499844391
  8541.479077934315
  8825.06664419282
 10005.978700424828
 10735.715591182363
  6862.252382927998
  7741.869940699019
  7767.507602682423
 11578.473495111322
  9564.191196642532
 10274.66986841471
  9941.579760726258
  6511.673662701443
  7247.810531421767
  7969.522741630305
  7029.066359138765
  8880.3310819865
  7093.973095258437
  7087.429134177531
 11556.4206111176
  7135.703713243964
  9291.966720013988
  6750.1614508378625
  8238.292872864913
  8292.556890773989
 10777.01175751811
  7214.247718367718
  7650.713142434376
 10175.232526887683
  7273.360178603195
  9363.070271223083
  9031.082023348998
  8825.414779423541
  9944.142050890547
  8661.613237640822
  9030.01116283714
  8977.177135132664
  8337.56601288842
 10141.850334339602
  8546.618623453181
  7410.515074782262
  6572.347340125307
 10083.609269058421
 12654.637979443547
 11554.172750677219
  7121.933861273288
 10439.88131675033
  8660.384631888535
  8512.0120356354
  9538.636274253677
  9201.508108840835
  6761.557954421337
  8449.598647745126
  9234.271120987107
  6686.902174732928
  7998.82878768579
  9335.195003258706
  9793.698753380144
  7511.6997674706145
  7040.272253499823
  8574.417023127731
  8710.889726264822
  9855.234442726523
  7333.271390876398
  7074.90737382472
  7567.028797634303
  7478.819856001589
  8661.678431165836
  9686.365620656681
 10380.927261088596
 10909.16114126585
 11846.913189403962
  7601.6838272205405
  8354.527228487183
 12205.27856229888
  6669.8545818673365
  7801.38293467816
  9900.023646481075
  7456.090708871705
  9392.341214454817
  9171.50778068918
  7182.033412097328
  7776.702509908223
  8747.492267816275
  7267.472492994599
  7004.983685255049
  7308.658504641303
  8605.037774331216
  6729.641478971638
 15265.768267500302
  7678.69443706525
  7665.185344445105
  8024.504324958378
 13004.357203635522
 11699.41947840393
 10191.595188321702
 13159.863220726136
  9394.067116753497
  9427.537640184984
 10055.990865507494
  8253.500233500139
  8365.577232126088
  8327.59314790924
  8595.724831372749
  8352.102341714275
  8447.66423767584
  9078.305961792837
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)6991.07878.08570.010324.011543.014550.0
2community pathway6591.07452.08068.09381.010441.012744.0
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1baseline (hospital pathway)6991.07878.08570.010324.011543.014550.0
2community pathway6591.07452.08068.09381.010441.012744.0
Delay-sensitivity infection-count density plot
julia
delay_sensitivity_fig = RUN_SENSITIVITY ?
                        plot_cumulative_cases(
    "baseline (hospital pathway)" => posterior_C_joint,
    "community pathway" => posterior_C_community_delay; scenarios = []) :
                        Markdown.md"_Delay sensitivity analysis not shown in this build._"

Tree-prior sensitivity

The outbreak-age estimate depends on the coalescent tree prior assumed in the BEAST X analysis. The baseline uses the more flexible Skygrid non-parametric model, which dates the common ancestor to 15 March 2026 ( HPD 09 Feb – 12 Apr). The report also fits an Exponential growth tree prior, which dates the common ancestor about a week earlier to 08 March 2026 ( HPD 01 Feb – 05 Apr) (Mbala-Kingebeni and others, 2026). Both priors give similar evolutionary rates (  subs/site/year). We re-fit the joint model under the Exponential growth TMRCA and compare the infection count to date and the outbreak age.

Re-fit the joint under the Exponential growth tree prior
julia
# The Exponential-growth re-fit (and its `tmrca_days` offset) is defined in the fit
# registry (`docs/fits/registry.jl`) and loaded through the cache (when enabled) in the
# setup block above.
posterior_C_exp_growth = RUN_SENSITIVITY ?
                         vec(Array(chn_joint_exp_growth_clock[:C_T])) : nothing
T_skygrid = vec(Array(chn_joint[:T]))
T_exp_growth = RUN_SENSITIVITY ? vec(Array(chn_joint_exp_growth_clock[:T])) : nothing
1000-element Vector{Float64}:
 158.02098089232808
 157.20378323750958
 156.7214645281194
 156.94710606742476
 157.18261124183513
 156.7328096126251
 157.93970004353324
 158.69206400625333
 159.15739213200698
 159.27076527534982
 160.66841822829085
 162.50717069869359
 163.3300259922954
 162.84451868230127
 163.22517201232154
 168.75411809987287
 169.07624537299927
 171.01233137973634
 169.3917800857485
 168.69449013148648
 168.40718016542445
 170.67447883561636
 168.36754131972236
 168.00612290173808
 171.7962885778236
 171.46036584519697
 171.89344746144008
 171.56920253157793
 172.5239003508964
 172.01423694894734
 167.6915596924522
 167.36711869413614
 168.58738618766475
 167.36816428301765
 167.1783730616879
 168.62208859923683
 168.28011754340633
 169.3384541721774
 168.815265525258
 169.05339545675727
 169.1995197901862
 169.96734966943686
 172.42812554249633
 172.42555039982514
 172.98102077774715
 172.88412326614062
 174.65262953106875
 171.81074925803566
 170.07766401269814
 171.769023348718
 171.51741399318863
 171.53418612619004
 167.50017493043615
 166.34505023972784
 166.4408019643705
 171.2816944154837
 171.35519234417794
 170.8995251539172
 171.91194739553922
 171.48056459293514
 171.40064356443915
 170.65009829387108
 172.05913954441198
 177.48313486232914
 178.02248840433475
 180.59880092289006
 178.55544802619923
 175.8429007057311
 190.25173238776722
 189.02187705274457
 188.77146727744486
 188.67064665900372
 189.4305723569034
 184.40039712673803
 183.69649719290697
 178.9102243142333
 183.83041100477672
 181.8037927344053
 177.07709325255678
 177.2785935227879
 181.30853963932964
 180.95597503091807
 181.28425712130633
 181.11724287210245
 181.82676370753887
 181.7560681331142
 182.99645315942394
 183.0322219718995
 185.8636044850718
 182.40469862326742
 183.4413853791606
 183.55479533164845
 185.68897056854945
 183.7307613447239
 182.0974633325615
 180.94744991161411
 181.07734252973535
 182.51789454659496
 175.44833601301184
 174.94639992994922
 173.30465993591994
 165.44887487305567
 165.52546401484568
 164.92496480488057
 166.3457245789316
 166.23814504477488
 165.7568668599562
 165.34813053650183
 164.12849654364925
 165.54850811891737
 165.44369726597043
 165.96969647811187
 166.89962865319075
 166.86804783219907
 163.4832461397036
 162.5285842499559
 160.45419194852406
 160.2034012559394
 159.8394166535572
 159.21263860742954
 158.2221415741522
 156.93846381302248
 157.0971952413064
 158.25987440585345
 159.5402511485567
 160.2473336615123
 159.87141525009355
 159.7842701569874
 161.9719450621246
 161.3643736822273
 163.25973263538117
 165.27040699924265
 167.56663205462405
 167.58360938984785
 164.4333334330545
 163.753629961011
 163.75673140183463
 163.69636642788524
 164.1678182055815
 166.24308641568334
 166.52450219964692
 168.32088312784032
 168.5544270534707
 168.60614197288433
 168.64472620891226
 168.22764546302508
 169.55943762965563
 168.61840283895904
 168.54272590154702
 168.2962332636297
 167.49177957348596
 165.71353617260468
 165.98286007835233
 166.16577639363706
 166.67692483214043
 165.81118122886699
 165.52409272939786
 161.45256501461182
 160.98893213260752
 160.4781491257292
 160.29777069252614
 159.32231184244705
 158.10409506790575
 159.34379410199813
 161.62813669101243
 165.28578654459693
 165.39715948282876
 164.68043507300695
 163.39312723222457
 163.40606030046456
 164.05481440436148
 165.49595393676037
 165.44429768979535
 163.58345478274495
 162.850931751146
 163.94130788964384
 163.84761448070174
 164.42699805486674
 163.46187750332746
 163.0267886550926
 162.35141462257215
 161.92440949870542
 161.48409116736923
 160.74978599892037
 160.0300664337302
 160.02075471253949
 160.057955042209
 159.70212546571966
 160.77998121493178
 161.4880581616213
 162.09892250145157
 163.4113302037565
 163.5606314978775
 165.46569165712606
 164.2617112159365
 166.28407669309027
 166.91591865145196
 166.56812967833446
 166.92319552634407
 166.44366717518832
 165.61912366400418
 166.69951784073874
 166.38715479667732
 167.13908414058577
 166.7962250963652
 168.02338478650208
 168.98901192377429
 175.09840433614352
 174.9544547931889
 173.5150194097956
 173.6074000573049
 174.83634420808076
 170.70066848704758
 171.90124281093426
 171.789367435623
 171.21570938618254
 172.33461266196127
 170.14819682379826
 170.34708062141453
 174.35550593833838
 176.1354450828338
 176.30271500386357
 175.01192491943715
 175.02203119586153
 175.60783796447393
 176.78354587863714
 176.9254620549822
 176.09526125022316
 180.79585225942833
 180.9714935324358
 182.22838299507762
 185.5654282613586
 186.29536013034027
 180.21320536194412
 181.82760048274884
 182.08248724878717
 180.16528075922903
 183.95363400374933
 184.31218587578206
 180.68299577290475
 175.99317140580422
 174.47669762651003
 176.05582154413418
 177.284117144665
 176.93984159725724
 178.46477009761247
 181.2381735962279
 181.94835402399525
 182.5366916188546
 180.4907749841567
 179.63703974324517
 176.37018963816143
 173.3000695086201
 168.9441152742455
 167.66672399280358
 167.95658547873828
 165.5714981980655
 164.9993755863287
 164.69164555629135
 164.397016541412
 165.39177162771807
 165.64964146880595
 165.32384052649869
 165.29007442211793
 165.78214147577063
 165.27371524005818
 164.79459304156012
 165.37113828151732
 166.9695326474536
 169.737532036773
 169.62559156244905
 169.16297724101557
 168.56780986857518
 170.04121327299274
 171.0083073492275
 170.96955900106488
 171.6995082433353
 168.6396359221463
 167.3168338702965
 167.69837318474868
 169.30607148871448
 164.88526125324387
 165.9107743754839
 165.73807332501622
 167.46764199049295
 165.82563089854125
 166.69591327759082
 166.71432163989365
 165.81300266537795
 163.6785918388205
 164.78410532001902
 165.0401899321963
 164.9405168760966
 165.11743977749583
 163.92248942853715
 163.98908409318307
 162.26549040476016
 163.4865147150009
 162.61925721461938
 163.26296856342884
 163.24225562918645
 162.40581094942098
 162.07922781957419
 161.78995039988638
 159.7113177268513
 160.08751624556595
 158.2163467485866
 157.62089238098233
 157.80330435863306
 157.63275880106983
 156.8993126272616
 157.82626722182147
 157.8070853648112
 156.83114341192254
 156.9558365751694
 156.69133617946918
 156.7549069311426
 156.8554511789298
 157.45068194268939
 156.68457540294145
 156.9485610505586
 157.29933124422777
 157.4595297333726
 157.2653515765165
 157.19326948960065
 157.91012393737077
 157.5356854282301
 158.3383964796586
 158.54115835394487
 158.45902133568296
 158.0765261209543
 158.1646819122082
 160.38919638962093
 160.402269016214
 160.54522809244324
 158.86593459935645
 158.70703706773665
 159.53169977681878
 158.7686941757069
 158.19944162890093
 157.94620707355182
 159.28622027549397
 159.78961594560187
 158.4763893177664
 158.21839041697822
 158.80788186428532
 158.77867131364263
 158.667104488981
 158.04025492384656
 158.47080730104352
 158.43651832269933
 160.7629706411339
 161.90466581269266
 158.08408596975642
 157.91241263302135
 156.30239504461147
 155.8439103075837
 156.335111234818
 154.93653871410828
 155.57311917500078
 154.88320992638822
 154.79838363154835
 154.19767776316615
 154.33409526799176
 154.05948391028096
 153.3058789729905
 153.00582909327824
 152.94709644877267
 152.57675861365718
 153.069758053494
 153.4693684380373
 152.99502344328005
 151.55358546470916
 151.7583015660765
 151.6798608570876
 151.42717485654714
 151.1769643551453
 151.23805259078347
 151.45000529510597
 151.55255746145943
 151.47045409875756
 151.39995783348232
 151.40945415050675
 151.63050444132395
 151.3267192803324
 151.31769433976396
 151.03680267044845
 150.87429798439663
 150.70453167812525
 150.4229046659612
 150.3399814441803
 150.69928968213017
 150.97094770314942
 150.9639469349555
 151.73228084318094
 152.79560941312818
 153.42413862136223
 153.05842489823726
 153.22507070484002
 152.7198416372631
 152.22650307147805
 152.93157275047807
 152.46967366024245
 152.91828238256215
 152.82558757552832
 153.16319435636726
 153.66356003751554
 153.76335204810613
 153.01996466535397
 152.89152478852122
 153.24606552108335
 154.99163818843596
 155.39781435854437
 154.4468578490037
 154.34498305866
 154.21209050111997
 154.07957329682034
 154.76422055839978
 154.09628470524797
 154.3301285104573
 154.3729228883853
 155.0445166928529
 154.90118045917617
 154.82512541509124
 155.14817064981926
 155.53915054165563
 153.5973547621087
 153.88748619372603
 153.9231659339313
 153.99733569704932
 153.9598147718059
 154.020784165146
 154.44171850255543
 154.49314557915727
 153.64705122586255
 155.66315832681335
 155.98463570315386
 156.2284907486025
 154.75397960114375
 154.60388213974917
 155.35663303578656
 154.96158210432853
 154.56503296286684
 155.05364125320344
 155.15848477794574
 154.92383651772187
 155.25212345470783
 156.49824561660387
 156.4310007549449
 156.0869911327026
 156.3128671904041
 157.24151456735933
 157.74122218031798
 157.94461689510874
 158.29248210307975
 157.1421300093728
 157.12537817337898
 158.94969139565185
 158.64916464510134
 158.1397778647846
 158.42032964090592
 157.9242028248231
 156.4032984059578
 155.19448296404224
 155.88305088254313
 156.23710378885684
 156.6083840639231
 157.0890894284569
 157.27982043356076
 156.99829952199482
 157.37847274098124
 156.5097370696423
 158.84444896484243
 161.5792798901828
 162.47850841588277
 162.72104444354235
 161.76028302116748
 160.8434227208878
 159.92999835162632
 157.68905804135116
 157.2834225480598
 157.2343312137648
 156.6645258030818
 155.55998133559984
 155.76184049727038
 155.29537260050026
 157.83172601620817
 157.45945779140902
 157.4829382782321
 159.11846290367188
 157.64400932510026
 158.446099715186
 158.22566364000724
 158.28325753598216
 158.8589851803877
 160.18009636229397
 160.22842761571638
 159.93636051533502
 159.18518118669684
 159.84287964164932
 144.0390090045195
 144.61384402879344
 144.50684376071445
 144.43034483230917
 144.18155223856093
 144.77540535162393
 147.35007300963133
 145.47845551893022
 146.0294996421644
 144.39487503536526
 145.38084277532894
 144.89629317934885
 144.78596846755636
 146.38517253815698
 145.77074736699586
 144.7075439987068
 144.72155708625584
 145.74403758421857
 146.11372026853968
 152.75856174454222
 156.30279101362416
 160.34019444680683
 154.54481065259347
 149.97611461094226
 157.04932711321584
 158.9353099304267
 156.49686558589525
 151.64104310652868
 156.45835006686676
 151.4025014204163
 152.80745350818177
 155.0125933208377
 156.2652741561268
 156.42403844893659
 160.05135117705993
 157.48724360362684
 154.03217190716492
 152.01044564666012
 152.94206757626586
 156.36405149440247
 158.5505260598161
 161.17503213188576
 170.5481065830473
 156.89899376672255
 158.11870807887414
 161.15899257168067
 146.7795716453653
 143.9082755819603
 144.60893075262024
 144.91921261887285
 144.39251328405177
 144.43569797766463
 145.6257766322963
 144.83763361076748
 144.83004657279184
 143.86815246525993
 143.05197872738384
 144.0266848179323
 143.0665850205165
 145.02828786619864
 144.2114841304806
 147.26576545203005
 148.44292053186203
 151.99638677863493
 152.53564543279305
 147.77040999988068
 147.40536732339552
 146.03629173008093
 148.34049786728974
 150.97038557304933
 153.39679280769627
 147.47750510421616
 149.18747753764242
 145.00301850210633
 144.0610832249039
 144.36111146921584
 145.09613725450046
 147.08689606671223
 156.86630306197742
 153.9814404400286
 164.32053931898167
 153.39217263362474
 147.25234103194356
 146.515144684049
 153.6638787819767
 152.46198343286974
 152.14080419684512
 147.06147436152125
 145.98624745913742
 143.48755094216824
 146.07769983805474
 147.57578084080404
 148.94663803304627
 144.9126519177158
 144.9675689312181
 145.9108900210673
 146.25734863751245
 144.16422333758695
 146.25163613001132
 145.9302018879033
 145.63514181148676
 144.26762432835537
 147.10058286444573
 147.91998905347242
 148.04804550743762
 148.4204834661866
 146.26818430900855
 145.49390979472767
 146.01239649178663
 145.5864778336538
 145.05472096650618
 144.9293891470111
 145.44169623733976
 146.9307358570254
 148.32669590251825
 150.69924357629986
 148.85790007465013
 148.41689481520896
 149.6268452880995
 149.99349076605097
 149.49880047775883
 154.49489434824648
 154.08401907997947
 155.66733089206534
 158.979098923418
 156.5515065979196
 158.31348706498812
 154.8175052960472
 155.92582521572996
 161.8352413807972
 154.72359608655805
 155.5549824257197
 157.25312629647337
 153.51383357869915
 148.58527722011357
 149.43521007634854
 149.75477918135545
 149.6213402221436
 147.7867252567954
 146.80272184004286
 144.8380559147412
 144.6453652301855
 144.38532435968972
 149.2647734600096
 152.12290875517783
 151.13321257562174
 152.3470310945061
 156.75734564893654
 150.82325601359832
 159.7999388792774
 157.16651924077513
 153.49378937269805
 150.31037588904996
 155.70204536318445
 152.32863235629287
 157.7706993025488
 156.80732370968641
 150.675280332164
 154.40812460888634
 149.3202553466312
 149.58616926850917
 149.94251844126157
 147.64268299503985
 151.28570359536397
 153.2845036687903
 163.73607800141053
 144.09227895240332
 144.61609414542107
 145.3697001106989
 150.32838481455195
 150.53367747478774
 150.45817848053449
 152.88827392166013
 155.73110768939497
 149.17963507474244
 148.40639513270068
 149.29529306416353
 160.8089470203859
 159.2142067737057
 165.34863126731747
 157.21116201258945
 154.6475820737525
 163.31396415442828
 161.39827137370565
 151.11171774687455
 150.5698686766129
 159.42611291880075
 152.58666911018057
 152.13374008138115
 148.4267602851391
 150.9312840903675
 150.10427397942547
 168.48842103085715
 151.9705181070451
 147.7101266420969
 150.44627346930056
 150.81719948537918
 152.41326262758744
 152.56097077519908
 150.39728000112993
 150.96828847171062
 151.8826527710014
 155.2153559116344
 155.64766906428397
 159.30747857815453
 155.89727182287436
 159.40143490409986
 159.7077447725781
 156.03903007288082
 150.77439610968028
 153.61929272030486
 155.081699531794
 151.83925210720545
 156.80718824500826
 157.29199898094566
 152.97369686958922
 162.9681134229188
 157.1725236781293
 157.12865106057268
 156.07159265965785
 153.08625586175543
 154.27387525658153
 156.60424244439835
 152.7585306996998
 157.64567736841357
 155.43432798126727
 153.5619987926183
 158.10789318127263
 155.65723742372123
 154.19768465097465
 155.97905508335728
 163.9617700219114
 163.2242595349633
 153.89483227253822
 151.2383094926776
 150.23157324162537
 155.6193078248445
 161.50604733917174
 159.31299470197013
 154.7004265567914
 162.36563719524702
 160.5841147487959
 157.65977372440835
 153.0498117001309
 153.26144084188704
 149.7595091667424
 158.05469432795442
 154.51753173900815
 173.58844274298502
 147.47058635974827
 146.1868259049439
 146.1413938559865
 143.25659218599324
 143.1982284048209
 144.43419359611323
 143.61513521763672
 143.64876234523112
 145.7266312294139
 147.31984223928936
 147.79889870346148
 150.75930509185991
 152.5061762973123
 152.53254022813059
 169.96564582091014
 155.88785756748715
 156.49313480276766
 165.17206592155335
 159.7180471244608
 168.2592733089065
 169.9486514065345
 162.0224201570181
 158.3131604641391
 160.16735014344863
 153.26619728762432
 148.37222530392913
 148.81598081624045
 154.91043702957595
 166.89252700999458
 165.39287992376327
 168.49323862557193
 157.79525167234448
 152.81799626963914
 156.75564353090414
 156.05614309303104
 156.3738913034779
 155.1288181425091
 157.52967284106154
 153.731797244856
 157.7676808269977
 159.91379493236943
 152.41906357328017
 147.74642065255503
 149.20832834832007
 148.86597753013928
 144.8176183300216
 155.89490742472537
 154.38308633501006
 157.6272936219149
 160.00711451662482
 159.60240244542945
 153.6968752658499
 165.09585950937657
 157.0678879136034
 162.41600976747048
 165.02779617460078
 148.29314327961066
 147.8475741667646
 147.90489296891235
 152.88653478691324
 155.74322204339563
 162.828803217496
 157.5881801364995
 158.8788742090137
 159.43086105241596
 171.6206023609736
 171.49500567702032
 161.34123571432036
 164.99939053041248
 163.58149407285708
 162.3056347162394
 162.4815266386199
 163.11525853907972
 162.55502250843728
 158.12766831715197
 151.65149490147328
 149.00012500694385
 149.47623380261555
 148.89976063003192
 147.21332100504847
 145.8920609455311
 151.12651777367378
 153.50167794223245
 151.99983051765344
 153.70362278698371
 152.81192624685644
 151.9885898931378
 148.34010648321038
 148.96897667938492
 148.26635528158306
 150.01097850645834
 146.87129385654205
 145.435548444139
 146.33520008857235
 145.94713639957425
 145.26328394905897
 146.80868109116508
 144.47524174479153
 144.300093408601
 146.90150274151569
 145.82890043837207
 146.67642891411708
 147.19604161154692
 148.24577582280904
 147.4903632905165
 147.589128376837
 144.39135071965586
 148.19808313252562
 147.67517743733205
 147.71484974030375
 151.263974099195
 151.39080873588648
 150.11617146077526
 150.15425540954362
 148.45018272794087
 153.33342849662105
 157.0329207712164
 159.6883170090647
 153.7258419883424
 155.86411757077107
 158.50384971067643
 169.9517954367192
 150.55328684177903
 162.38555241533487
 163.60371707836129
 159.8896112976756
 157.5625114609149
 150.33547415504086
 149.3870819075719
 153.17106189557708
 153.92923933510372
 157.64931939561836
 157.02483309752455
 160.55867967998276
 172.10292518817744
 170.7316412469798
 175.30855042537667
 172.36922291596676
 170.60313978908962
 167.7000551919514
 166.1529666086001
 163.43573917141077
 159.16348529020124
 155.63173241728376
 156.5768470187734
 144.9769981989961
 144.27365478738378
 145.21528678955127
 144.97508450840078
 144.74445363002334
 143.4611526642096
 144.71329510086767
 146.4869267737821
 145.64119499552632
 147.1875612657144
 145.1333753179138
 144.5769671183969
 144.70356774567475
 149.30405225970037
 150.35323138927242
 151.9196030845873
 152.24381004817107
 148.0360437540558
 154.15767875895628
 154.85253472022254
 148.15346937280347
 149.62815575042038
 155.96453627260502
 158.51676210516996
 160.21065973504716
 158.44130838570143
 158.06539798918476
 168.97581522511047
 159.64838171953969
 162.9450785029756
 163.09162855523732
 167.73708092797932
 161.88351404525108
 166.4244347596869
 162.27857482101672
 165.42328797396112
 167.70916625264977
 171.361454863111
 170.55826850987066
 165.04960753565464
 148.11738191400786
 157.4485638127882
 151.4167798154781
 159.1145530673488
 160.7130193509013
 170.8584855270875
 176.97527413146582
 186.95144895975358
 181.56824513313978
 164.21402425821086
 154.2782394343815
 155.93512319949937
 152.78121821003919
 157.43378854361254
 164.43741122449484
 171.07096657781966
 163.7962665882708
 166.71589546685334
 175.84118886557172
 187.1604344641283
 177.187567472038
 174.59637440569952
 188.78392467473103
 208.06732492497378
 202.17601162199088
 176.01408194919125
 176.24297079877462
 168.32590854485298
 157.8709117503635
 171.39762585740502
 180.03106408358204
 182.61739010447047
 178.35060894616598
 180.48953302588336
 169.95564831768456
 166.364324202711
 157.7397917988547
 159.31904538913713
 156.64632620555903
 153.6790287447003
 153.83459612279688
 156.51163290437532
 154.64878475158534
 161.50792812549454
 154.15915982579597
 150.60196156127455
 151.34497705182883
 150.16989455282206
 147.72002838646551
 148.46511509098636
 145.63038871116723
 149.85550084126746
 148.33527471257693
 150.13799704249772
 145.88765360159712
 145.940654615753
 145.47047281164802
 146.5659752087796
 147.43378859107503
 146.205395177043
 145.89637184585146
 146.28238670828932
 147.82142338340105
 149.37114734031033
 153.52565384962563
 150.51874410687003

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)6991.07878.08570.010324.011543.014550.0
2Exponential growth7051.08333.09102.010427.011187.012552.0
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Skygrid (baseline)6991.07878.08570.010324.011543.014550.0
2Exponential growth7051.08333.09102.010427.011187.012552.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)137.0140.0143.0150.0155.0167.0
2Exponential growth145.0150.0154.0162.0167.0180.0
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Skygrid (baseline)137.0140.0143.0150.0155.0167.0
2Exponential growth145.0150.0154.0162.0167.0180.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)