Forecasts
Every release forecasts each DRC stream a week ahead from the joint posterior, as the one-week-ahead forecast Methods section describes. How these forecasts have scored against the data that arrived afterwards is on the evaluation page. The split by province is on the province forecasts page and the split by health zone on the health-zone forecasts page.
Load packages, data and fitted chains
# Shared setup: packages, observations and the fit registry. See
# `docs/pages/_setup.jl`.
using BVDOutbreakSize
include(joinpath(pkgdir(BVDOutbreakSize), "docs", "pages", "_setup.jl"))validation_forecast_from (generic function with 1 method)# The fits this page reads, loaded from the cache here.
chn_joint = load_fit("joint");Summary
These are the expected counts for the week after the cut-off.
Generate the one-week-ahead forecast
forecast = forecast_reported(
fit_forecast("joint");
horizon = 7,
obs_cases = obs.reported_cases,
obs_deaths = obs.total_deaths,
obs_confirmed = obs.confirmed_cases,
obs_confirmed_deaths = obs.confirmed_deaths,
obs_recovered = obs.recovered_cases
);
forecast_week_end = obs.cutoff + Day(7);
national_forecast_bullets = join(
[
"- **Confirmed cases:** $(median_interval_text(forecast.confirmed_new)) new laboratory-confirmed cases in the week to $(forecast_week_end).",
"- **Confirmed deaths:** $(median_interval_text(forecast.confirmed_deaths_new)) new confirmed deaths over the same week.",
"- **Infections:** $(median_interval_text(forecast.infections_new)) new infections, reported and unreported.",
"- **Reproduction number:** $(median_interval_text(forecast.rt_forecast; digits = 2)) on $(forecast_week_end).",
], "\n"
);Confirmed cases: about 658 (90% credible interval 475 to 901) new laboratory-confirmed cases in the week to 2026-10-27.
Confirmed deaths: about 263 (90% credible interval 202 to 336) new confirmed deaths over the same week.
Infections: about 1218 (90% credible interval 615 to 2386) new infections, reported and unreported.
Reproduction number: about 1.10 (90% credible interval 0.77 to 1.56) on 2026-10-27.
One-week-ahead forecast results
Summarise the one-week-ahead forecast
forecast_summary = forecast_table(forecast);| Row | Stream | Quantity | Lower 90% | Lower 60% | Lower 30% | Upper 30% | Upper 60% | Upper 90% |
|---|---|---|---|---|---|---|---|---|
| String | String | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | DRC confirmed cases | cumulative by T+7 | 10179.0 | 10265.0 | 10315.0 | 10412.0 | 10479.0 | 10605.0 |
| 2 | DRC confirmed cases | new this week | 475.0 | 561.0 | 611.0 | 708.0 | 775.0 | 901.0 |
| 3 | DRC confirmed deaths | cumulative by T+7 | 4889.0 | 4917.0 | 4935.0 | 4965.0 | 4986.0 | 5023.0 |
| 4 | DRC confirmed deaths | new this week | 202.0 | 230.0 | 248.0 | 278.0 | 299.0 | 336.0 |
| 5 | DRC isolation beds | demand at T+7 | 1092.0 | 1200.0 | 1267.0 | 1409.0 | 1512.0 | 1697.0 |
| 6 | DRC isolation beds | occupancy at T+7 | 932.0 | 1043.0 | 1118.0 | 1259.0 | 1357.0 | 1538.0 |
| 7 | DRC isolation admissions | daily at T+7 | 112.0 | 161.0 | 178.0 | 209.0 | 229.0 | 269.0 |
| 8 | DRC in-care deaths | daily at T+7 | 14.0 | 19.0 | 21.0 | 26.0 | 30.0 | 37.0 |
| 9 | DRC isolation rule-outs | daily at T+7 | 100.0 | 114.0 | 123.0 | 140.0 | 151.0 | 171.0 |
| 10 | DRC recovered among confirmed | cumulative by T+7 | 2717.0 | 2737.0 | 2750.0 | 2771.0 | 2784.0 | 2811.0 |
| 11 | DRC recovered among confirmed | new this week | 122.0 | 142.0 | 155.0 | 176.0 | 189.0 | 216.0 |
One-week-ahead latent forecast plot
forecast_latent_fig = plot_forecast_latent(forecast);
One-week-ahead observed forecast plot
forecast_fig = plot_forecast(forecast);
The reported count of patients in isolation is the occupied-bed count, so it measures bed use limited by supply, and bed demand is the unobserved number who need a bed. The model carries one national bed capacity, so the national shortfall understates local unmet need (see limitations). On 13 June Ituri was at 93.9% occupancy while Sud-Kivu was at 21.9%.
One-week-ahead isolation-bed forecast plot
forecast_beds_fig = plot_forecast_beds(forecast);
One-week-ahead treatment-flow forecast plot
forecast_flows_fig = plot_forecast_flows(forecast);
Symptom-onset nowcast and forecast results
The symptom-onset nowcast and forecast Methods section describes how the onset projection is built.
Generate the symptom-onset nowcast and forecast
onset_forecast = forecast_onsets(
fit_forecast("joint");
horizon = 7,
obs_value = something(obs.onset_curve_history.last_total, 0)
);
onset_forecast_summary = onset_forecast_table(onset_forecast);| Row | Quantity | Lower 90% | Lower 60% | Lower 30% | Upper 30% | Upper 60% | Upper 90% |
|---|---|---|---|---|---|---|---|
| String | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | symptom onsets to date | 14194.0 | 16960.0 | 18937.0 | 22664.0 | 25377.0 | 31106.0 |
| 2 | of those, reported by T | 6857.0 | 6958.0 | 7014.0 | 7114.0 | 7178.0 | 7288.0 |
| 3 | onsets not yet reported at T | 7115.0 | 9843.0 | 11848.0 | 15644.0 | 18266.0 | 24097.0 |
| 4 | reports this week of onsets before T | 209.0 | 259.0 | 288.0 | 342.0 | 376.0 | 442.0 |
| 5 | reports this week of onsets after T | 60.0 | 74.0 | 83.0 | 100.0 | 112.0 | 138.0 |
| 6 | new onset reports this week | 243.0 | 324.0 | 364.0 | 447.0 | 496.0 | 598.0 |
| 7 | new symptom onsets this week | 656.0 | 864.0 | 1018.0 | 1341.0 | 1559.0 | 2032.0 |
"Onsets not yet reported at T" is not a backlog that will all arrive, because ascertainment does not reach one. It holds both the reporting backlog and the cases surveillance will never confirm. The smaller "reports this week of onsets before T" row is the part the coming week should clear.
Symptom-onset nowcast and forecast plot
onset_forecast_fig = let
fig = CairoMakie.Figure(; size = (960, 420))
# Two-line tick labels rather than rotated ones: the leftmost rotated
# label overhangs the axis and is clipped at the figure edge.
ax1 = CairoMakie.Axis(
fig[1, 1];
title = "New onset reports over the coming week",
ylabel = "cases", xticks = (
1:4,
[
"already\nhappened", "not yet\nhappened", "sum of\nthe two",
"as the next\nfigure reads it",
],
)
)
# The first three bars are latent, so the third is exactly the first
# two added. The fourth is that same sum replicated through the
# observation model, which is the scored quantity and the only one
# comparable to a digitised figure; it is wider by the read error,
# which is why the three latent bars are shown as well rather than a
# decomposition that appears not to add up.
_latent_total = onset_forecast.onset_reports_backfill .+
onset_forecast.onset_reports_future
for (i, d, col) in (
(
1, onset_forecast.onset_reports_backfill,
:mediumpurple,
),
(2, onset_forecast.onset_reports_future, :mediumpurple),
(3, _latent_total, :mediumpurple),
(4, Float64.(onset_forecast.onset_reports_new), :slategray),
)
s = posterior_summary(d)
CairoMakie.rangebars!(
ax1, [Float64(i)], [s.lo90], [s.hi90];
color = col, linewidth = 3
)
CairoMakie.rangebars!(
ax1, [Float64(i)], [s.lo60], [s.hi60];
color = col, linewidth = 8
)
CairoMakie.scatter!(
ax1, [Float64(i)], [quantile(d, 0.5)];
color = :black, markersize = 9
)
end
ax2 = CairoMakie.Axis(
fig[1, 2];
title = "Symptom onsets by the cut-off",
ylabel = "cases", xticks = (
1:3,
["onsets\nto date", "reported\nby T", "not yet\nreported"],
)
)
for (i, d) in enumerate(
(
onset_forecast.onsets_to_date,
onset_forecast.onset_reports_to_date,
onset_forecast.onsets_unreported,
)
)
s = posterior_summary(d)
CairoMakie.rangebars!(
ax2, [Float64(i)], [s.lo90], [s.hi90];
color = :seagreen, linewidth = 3
)
CairoMakie.rangebars!(
ax2, [Float64(i)], [s.lo60], [s.hi60];
color = :seagreen, linewidth = 8
)
CairoMakie.scatter!(
ax2, [Float64(i)], [quantile(d, 0.5)];
color = :black, markersize = 9
)
end
# The digitised total the "reported by T" bar is a model of, so the
# reader can see the fitted reported level against the figure itself.
ismissing(obs.onset_curve_history.last_total) ||
CairoMakie.hlines!(
ax2,
[Float64(obs.onset_curve_history.last_total)];
color = :black, linestyle = :dash
)
fig
end;