Skip to content

Forecasts ​

Every release forecasts each DRC stream a week ahead from the joint posterior. The forecast is drawn from the fitted model run past the cut-off, 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 ​

The expected counts for the week after the cut-off, from the forecast below.

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 498 (90% credible interval 353 to 695) new laboratory-confirmed cases in the week to 2026-10-03.

  • Confirmed deaths: about 257 (90% credible interval 193 to 336) new confirmed deaths over the same week.

  • Infections: about 846 (90% credible interval 384 to 1747) new infections, reported and unreported.

  • Reproduction number: about 0.91 (90% credible interval 0.55 to 1.37) on 2026-10-03.

One-week-ahead forecast results ​

The table and figures below give the cumulative and new expected counts by   from the forecast defined in the one-week-ahead forecast Methods section. The summary table reports the confirmed case and death streams, the recovered total and the isolation-bed levels and daily flows. The observed-forecast plot below additionally shows the suspected case and death streams, so every projected stream appears. The situation reports no longer update those two, so their projection cannot be checked against a later observation and the forecast validation leaves them out.

Summarise the one-week-ahead forecast
julia
forecast_summary = forecast_table(forecast);
One-week-ahead forecast summary table
11×8 DataFrame
RowStreamQuantityLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringStringFloat64Float64Float64Float64Float64Float64
1DRC confirmed casescumulative by T+78420.08484.08526.08604.08656.08762.0
2DRC confirmed casesnew this week353.0417.0459.0537.0589.0695.0
3DRC confirmed deathscumulative by T+74094.04124.04142.04174.04196.04237.0
4DRC confirmed deathsnew this week193.0223.0241.0273.0295.0336.0
5DRC isolation bedsdemand at T+7683.0759.0813.0903.0963.01095.0
6DRC isolation bedsoccupancy at T+7567.0650.0697.0791.0850.0965.0
7DRC isolation admissionsdaily at T+788.0107.0118.0141.0156.0186.0
8DRC in-care deathsdaily at T+711.015.018.022.025.030.0
9DRC isolation rule-outsdaily at T+756.068.076.089.099.0119.0
10DRC recovered among confirmedcumulative by T+72198.02219.02233.02255.02269.02298.0
11DRC recovered among confirmednew this week128.0149.0163.0185.0199.0228.0

The latent figure shows the new infections, symptom onsets and deaths over the horizon, with the reproduction number left to keep evolving across it.

One-week-ahead latent forecast plot
julia
forecast_latent_fig = plot_forecast_latent(forecast);

The observed figure shows the new count each observed stream adds over the horizon: suspected cases, suspected deaths, laboratory-confirmed cases, confirmed deaths and recovered, one panel per stream the forecast carries.

One-week-ahead observed forecast plot
julia
forecast_fig = plot_forecast(forecast);

The bed figure shows the projected isolation/treatment-bed demand (the need a week ahead, under unconstrained supply) against the supply-limited occupancy the beds can actually meet. The gap between the two is the projected bed shortfall, shown in the right panel. The reported "Patients en isolement" count is the occupied-bed count (the report computes the "Taux d'occupation" as that count over the bed capacity), so isolation is bed usage, gated by supply. The demand is its unobserved counterpart, the number who need a bed. The model carries a single national bed capacity, so it cannot represent local saturation, and the national shortfall understates local unmet need. On 13 June Ituri was at 93.9% occupancy while Sud-Kivu was at 21.9%; beds free in one province cannot serve patients in another.

One-week-ahead isolation-bed forecast plot
julia
forecast_beds_fig = plot_forecast_beds(forecast);

The flow figure projects the daily isolation/treatment flows a week ahead: new admissions, in-care deaths and rule-outs, each drawn from the model run past the cut-off through the isolation dispersion.

One-week-ahead treatment-flow forecast plot
julia
forecast_flows_fig = plot_forecast_flows(forecast);

Symptom-onset nowcast and forecast results ​

The table below gives the onset stream's projection, built as described in the symptom-onset nowcast and forecast Methods section. The two halves must not be added together: the first three rows are the state of the outbreak at the cut-off, the next three the coming week.

"Onsets not yet reported at T" is not a backlog that will all arrive, because ascertainment does not reach one. The row holds two things together: the reporting backlog, and the cases surveillance will never confirm. The "reports this week of onsets before T" row is the part of it the coming week should actually clear. It is the smaller number.

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);
Symptom-onset nowcast and forecast summary table
7×7 DataFrame
RowQuantityLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1symptom onsets to date11845.014233.015853.019353.021569.025724.0
2of those, reported by T5742.05812.05858.05933.05978.06060.0
3onsets not yet reported at T5973.08361.09955.013445.015673.019738.0
4reports this week of onsets before T188.0218.0235.0266.0288.0328.0
5reports this week of onsets after T50.062.069.084.094.0112.0
6new onset reports this week239.0278.0304.0351.0382.0441.0
7new symptom onsets this week466.0656.0775.01000.01170.01563.0

The left panel splits the coming week's new onset reports into reports of onsets that had already happened by the cut-off and reports of onsets still to come, and shows their sum. The fourth bar is the same sum after it has been through the observation model, which is what the next vintage will actually print. It is wider than the sum it replicates by the two reads' error and the counting variation of the new reports. Only the fourth bar is comparable to a digitised figure, and only it is scored.

The right panel puts the nowcast itself on the same axes, the onsets that have happened against the share of them the triangle has printed.

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;