Skip to content

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
julia
# 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)
julia
# 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
julia
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
julia
forecast_summary = forecast_table(forecast);
11×8 DataFrame
RowStreamQuantityLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringStringFloat64Float64Float64Float64Float64Float64
1DRC confirmed casescumulative by T+710179.010265.010315.010412.010479.010605.0
2DRC confirmed casesnew this week475.0561.0611.0708.0775.0901.0
3DRC confirmed deathscumulative by T+74889.04917.04935.04965.04986.05023.0
4DRC confirmed deathsnew this week202.0230.0248.0278.0299.0336.0
5DRC isolation bedsdemand at T+71092.01200.01267.01409.01512.01697.0
6DRC isolation bedsoccupancy at T+7932.01043.01118.01259.01357.01538.0
7DRC isolation admissionsdaily at T+7112.0161.0178.0209.0229.0269.0
8DRC in-care deathsdaily at T+714.019.021.026.030.037.0
9DRC isolation rule-outsdaily at T+7100.0114.0123.0140.0151.0171.0
10DRC recovered among confirmedcumulative by T+72717.02737.02750.02771.02784.02811.0
11DRC recovered among confirmednew this week122.0142.0155.0176.0189.0216.0
Cumulative and new expected counts a week after the cut-off for confirmed cases and deaths, the recovered total, and the isolation-bed levels and daily flows.
One-week-ahead latent forecast plot
julia
forecast_latent_fig = plot_forecast_latent(forecast);

New infections, symptom onsets and deaths over the week after the cut-off, with the reproduction number left to evolve across it.
One-week-ahead observed forecast plot
julia
forecast_fig = plot_forecast(forecast);

The new count each observed stream adds over the week after the cut-off. The situation reports no longer update suspected cases and deaths, so their projections cannot be checked and the forecast evaluation leaves them out.

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
julia
forecast_beds_fig = plot_forecast_beds(forecast);

Projected isolation bed demand, the need under unconstrained supply, against the occupancy the beds can meet. The gap between them is the projected bed shortfall (right).
One-week-ahead treatment-flow forecast plot
julia
forecast_flows_fig = plot_forecast_flows(forecast);

Daily isolation flows over the week after the cut-off: new admissions, in-care deaths and rule-outs.

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
julia
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);
7×7 DataFrame
RowQuantityLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1symptom onsets to date14194.016960.018937.022664.025377.031106.0
2of those, reported by T6857.06958.07014.07114.07178.07288.0
3onsets not yet reported at T7115.09843.011848.015644.018266.024097.0
4reports this week of onsets before T209.0259.0288.0342.0376.0442.0
5reports this week of onsets after T60.074.083.0100.0112.0138.0
6new onset reports this week243.0324.0364.0447.0496.0598.0
7new symptom onsets this week656.0864.01018.01341.01559.02032.0
The symptom-onset nowcast and forecast. The first three rows are the outbreak at the cut-off and the next three the coming week, so the two halves must not be added.

"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
julia
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;

The coming week's new onset reports, split into onsets before and after the cut-off, with their sum and that sum after the observation model (left). Only the fourth bar is comparable to a digitised figure and only it is scored, and it is wider by the read error and the counting variation of new reports. The nowcast sets the onsets that have happened against the share the triangle has printed (right).