Skip to content

Province estimates ​

This page gives the province estimates from the joint model. The methods for this model are on the Methods page.

The model runs one renewal equation per province and fits the national streams against the summed provinces, so the national estimates are the sum of the provinces here. The per-province forecast is on the province forecasts page and its scoring is in the forecast by province.

This page is generated from docs/pages/estimates/province.jl. The model code it calls is in src/. See aim and origins and limitations.

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

Last updated: 30 September 2026.

Data as of: 26 September 2026.

Summary ​

The table below compares the provinces, from the joint posterior. Each range is an equal-tailed 90% credible interval, and the shares and probabilities are computed draw by draw. The reproduction number and the relative case ascertainment are identified only as a product, and the per-province deaths break the tie.

Compute the province comparison
julia
province_headline_md = patch_headline(chn_joint, N_PATCHES)
province_headline = Markdown.parse(province_headline_md);
ProvinceShare of infections (%)R at the cut-offP(R > 1)CFR (%)Relative ascertainment
Ituri65–750.55–1.2626%34.4–59.90.92–1.31
Nord-Kivu20–290.48–1.3126%37.5–67.20.73–1.07
Haut-Uele3–60.51–1.5737%34.6–58.80.89–1.25
Other provinces1–10.66–1.9667%34.4–59.80.79–1.2

Ituri has the most infections with probability over 99%, and 1 of 4 provinces is more likely than not to be growing. Infections imported from another province make up 0.0–0.4% of infections to date.

Maps ​

The maps shade each province by its patch's posterior median, so the pooled patch shades all its provinces alike. The reproduction number at the cut-off and the relative case ascertainment are centred on one, and a province whose 90% credible interval spans one is washed out. The Methods page defines each quantity.

Maps of infections, reproduction number and ascertainment
julia
province_map_fig = plot_province_map(
    [
        (;
            values = province_map_summary(
                chn_joint, :C_T_patch, N_PATCHES
            ).values,
            title = "Infections to date", scale = log10,
            colorbar_label = "Infections (median)",
        ),
        (;
            province_map_summary(chn_joint, :R_T_patch, N_PATCHES)...,
            title = "Reproduction number at the cut-off",
            diverging_at = 1.0, colorbar_label = "R (median)",
        ),
        (;
            province_map_summary(
                chn_joint, :province_ascertainment, N_PATCHES
            )...,
            title = "Relative case ascertainment", diverging_at = 1.0,
            scale = log10, colorbar_label = "Relative to national (median)",
        ),
    ]
);

Detail by province ​

Each province's own estimates are below, as equal-tailed 30%, 60% and 90% credible intervals, with its case-fatality ratio in the case-fatality ratio by province. The log-Rt deviation is from the national trend, the drift is the deviation's walk scale and the contrast is against Ituri.

Compute the per-province tables and figure
julia
# The tables below and the pair-plot dropdowns further down name the
# provinces in this order.
@assert PROVINCE_LABELS[1:N_PATCHES] ==
    ["Ituri", "Nord-Kivu", "Haut-Uele", "Other provinces"]
province_detail_tables = [
    patch_summary_table(chn_joint, N_PATCHES; patch = p) for p in 1:N_PATCHES
];
province_detail_fig = plot_patch_summary(chn_joint, N_PATCHES);

Ituri ​

7×7 DataFrame
RowQuantityLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Cumulative infections8736.010542.011716.014297.015933.019280.0
2Reproduction number0.550.690.780.941.051.26
3Daily infections at cut-off31.044.055.074.088.0127.0
4log-Rt deviation from trend-0.32-0.2-0.14-0.030.040.17
5log-Rt vs primary patch0.00.00.00.00.00.0
6Rt deviation innovation sd0.050.0670.0790.1040.1210.158
7Relative case ascertainment0.920.991.021.11.171.31

Nord-Kivu ​

7×7 DataFrame
RowQuantityLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Cumulative infections2758.03401.03856.04811.05592.06771.0
2Reproduction number0.480.640.740.931.061.31
3Daily infections at cut-off18.028.037.055.068.099.0
4log-Rt deviation from trend-0.53-0.3-0.18-0.020.070.24
5log-Rt vs primary patch-0.58-0.27-0.120.090.20.42
6Rt deviation innovation sd0.0880.1110.1270.1550.1760.214
7Relative case ascertainment0.730.820.870.971.01.07

Haut-Uele ​

7×7 DataFrame
RowQuantityLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Cumulative infections514.0630.0715.0898.01031.01283.0
2Reproduction number0.510.690.81.021.181.57
3Daily infections at cut-off3.05.06.09.012.020.0
4log-Rt deviation from trend-0.41-0.23-0.120.070.170.39
5log-Rt vs primary patch-0.44-0.2-0.070.180.320.59
6Rt deviation innovation sd0.1030.1230.1390.1660.1850.221
7Relative case ascertainment0.890.971.01.081.141.25

Other provinces ​

7×7 DataFrame
RowQuantityLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Cumulative infections98.0129.0148.0192.0227.0306.0
2Reproduction number0.660.861.021.311.511.96
3Daily infections at cut-off1.02.03.05.07.012.0
4log-Rt deviation from trend-0.150.030.130.30.40.59
5log-Rt vs primary patch-0.190.070.190.410.540.77
6Rt deviation innovation sd0.0740.0930.1090.1350.1540.189
7Relative case ascertainment0.790.90.961.011.061.2

Isolation beds ​

Beds, bed demand, occupied beds, utilisation and demand above the beds by province at the cut-off, from the per-province occupancy and bed figures the situation reports print. Each province's beds are its modelled capacity floored at its last recorded effective beds, which cover the patients each report counts. Its occupied beds are its demand share of the national modelled occupancy, capped at its beds, and the rest is the demand above the beds. The provinces sum to the national figures, and no province's utilisation exceeds 100%.

Province bed table
julia
province_bed_overview = province_bed_table(chn_joint, N_PATCHES);
4×6 DataFrame
RowProvinceBedsBed demandOccupied bedsUtilisation (%)Demand above beds
StringStringStringStringStringString
1Ituri1015 (1015–1059)511 (456–566)453 (413–492)44.3 (40.4–48.2)0 (0–0)
2Nord-Kivu354 (354–354)318 (282–355)281 (254–310)79.5 (71.8–87.7)0 (0–0)
3Haut-Uele138 (138–144)58 (49–68)51 (43–60)36.8 (31.3–43.0)0 (0–0)
4Other provinces28 (28–28)37 (30–45)28 (26–28)100.0 (94.0–100.0)5 (0–12)

Size and infections ​

The national outbreak size in the joint model estimates is the sum of the four patches' renewal equations. The figure below shows the modelled infections behind those totals, daily on the top row and cumulative on the bottom.

Modelled infections by province
julia
province_infections_fig = plot_infections_patches(
    chn_joint;
    n = obs.n, seeding = obs.seeding, n_patches = N_PATCHES
);

The provinces are coupled by a gravity kernel weighted by destination population, described in the mixing and importation Methods section, with its intensity estimated. Every arrival is debited from its origin the same day, so the figure reads as where infection occurred rather than as extra infection. The distances between the patches' population centres are 324 km from Ituri to Nord-Kivu, 216 km from Ituri to Haut-Uele and 390 km from Nord-Kivu to the pooled patch, so most of what leaves Nord-Kivu lands in the pooled patch.

Importation intensity and imports by province
julia
importation_table = summary_table(
    chn_joint, [:importation_epsilon];
    digits = 4,
    labels = Dict(:importation_epsilon => "Importation intensity")
);

province_imports_fig = plot_imports_patches(
    chn_joint;
    n = obs.n, seeding = obs.seeding, n_patches = N_PATCHES
);
1×7 DataFrame
RowQuantityLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Importation intensity0.00050.00110.00160.00270.00370.0066

Reproduction number by province ​

The national reproduction number is in reproduction number over time. Below it is split by province, one panel per province with the national trajectory in grey behind it. The deviations sum to zero, so the grey band is the incidence-weighted middle of the panels rather than any one province. A panel tracking grey says that province moves with the national trajectory. The pooled patch holds almost no confirmed cases, so its panel is carried by the deviation prior and its width is not a measurement.

Reproduction number by province
julia
province_rt_fig = plot_rt_patches(
    chn_joint;
    n = obs.n, breakpoint = _BREAKPOINT,
    n_patches = N_PATCHES,
    rt_start = _rt_start_plot,
    rt_walk_start = clamp(_BREAKPOINT - RT_WALK_LEAD, _rt_start_plot, obs.n),
    display_start = _rt_start_plot,
    as_of_date = string(obs.cutoff), seeding = obs.seeding,
    ramp = RT_INTERVENTION_RAMP
);

The spread of the provinces' log-Rt deviations is the spatial diagnostic. The prior admits real divergence, with a 26% prior probability that the Ituri to Nord-Kivu ratio moves by more than 25% over the window. With four patches and the pooled one carrying almost no signal the cross-province correlation is not identified, and it tracks its prior. It is the correlation of the provinces' deviation innovations, which sum to zero, so it leans negative. With equal spreads each province's correlations with the other three average −1/3.

Spatial hyperparameter summary table
julia
# Labels for the spatial hyperparameters, shared by this table and the pair
# plot against their prior below.
spatial_labels = Dict(
    :region_sd => "Rt deviation spread",
    :region_halflife => "Rt deviation half-life (days)",
    :region_corr_primary_secondary => "Ituri-N.Kivu Rt correlation",
    :province_ascertainment_sd => "Ascertainment spread",
    :importation_epsilon => "Importation intensity",
    :province_cfr_sd => "Lethality spread",
    :province_death_ascertainment_sd => "Death-confirmation spread"
)
spatial_hyper_table = summary_table(
    chn_joint,
    [
        :region_sd, :region_halflife, :region_corr_primary_secondary,
        :province_ascertainment_sd,
    ];
    digits = 3, labels = spatial_labels
);
4×7 DataFrame
RowQuantityLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Rt deviation spread0.0720.1540.2060.2890.3360.423
2Rt deviation half-life (days)23.56332.15940.18758.05374.852110.368
3Ituri-N.Kivu Rt correlation-0.738-0.536-0.385-0.0870.1290.489
4Ascertainment spread0.0160.0720.1110.1940.2550.405

Case-fatality ratio by province ​

Whether the case-fatality ratio varies by province, set against the national ratios in the confirmed case-fatality ratio. Only the product of a province's lethality and death confirmation is identified, and the province compositions Methods section says how the priors split it.

Province case-fatality spread
julia
province_cfr_spread = summary_table(
    chn_joint,
    [:province_cfr_sd, :province_death_ascertainment_sd];
    digits = 3, labels = spatial_labels
);
2×7 DataFrame
RowQuantityLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Lethality spread0.0120.0390.0620.1110.1410.2
2Death-confirmation spread0.0090.0380.0630.110.1410.205
Province case-fatality table
julia
confirmed_cfr = delay_corrected_confirmed_cfr(
    chn_joint;
    obs_confirmed = obs.confirmed_cases,
    obs_confirmed_deaths = obs.confirmed_deaths
);
province_cfr = province_cfr_table(
    chn_joint, confirmed_cfr;
    province_cases = vec(sum(province_cases.increments; dims = 2)),
    province_deaths = vec(sum(province_deaths.increments; dims = 2)),
    n_patches = N_PATCHES
);
4×4 DataFrame
RowProvinceNaive observed confirmed ratioDelay-corrected confirmed CFRStructural (infection-based) CFR
StringStringStringString
1Ituri46.0%41.7% (35.6–49.0%)46.0% (34.4–59.9%)
2Nord-Kivu59.5%59.1% (49.9–70.2%)50.0% (37.5–67.2%)
3Haut-Uele42.1%41.9% (34.7–50.7%)45.4% (34.6–58.8%)
4Other provinces37.9%47.0% (36.0–61.3%)46.6% (34.4–59.8%)

Province parameters against their priors ​

The pair plots below set the posterior of the province parameters against their prior. The prior is the four-patch model's, as the shared prior draws carry no province parameters.

Draw from the patch model's prior
julia
prior_patch_chn = patch_prior_draws(obs);

The first pair plot covers the spatial hyperparameters: the spread, half-life and correlation of the Rt deviations, the spread of case ascertainment, the importation intensity, and the spreads of lethality and death confirmation.

Spatial hyperparameter pair plot (prior overlaid)
julia
spatial_pair_fig = plot_pair(
    chn_joint,
    [
        :region_sd, :region_halflife, :region_corr_primary_secondary,
        :province_ascertainment_sd, :importation_epsilon, :province_cfr_sd,
        :province_death_ascertainment_sd,
    ];
    prior = prior_patch_chn, labels = spatial_labels
);

The pair plots below take one province at a time. Each sets the reproduction number at the cut-off against the relative case ascertainment, the case-fatality ratio against the relative death confirmation, and the province's importation intensity against its export weight relative to Ituri. Each pair is identified only as a product, so a ridge between the two is expected and its position along the ridge is set by the prior.

Compute the per-province pair plots
julia
province_pair_labels = Dict(
    :R_T_patch => "Reproduction number",
    :province_ascertainment => "Case ascertainment",
    :CFR_patch => "Case-fatality ratio",
    :province_death_ascertainment => "Death confirmation",
    :importation_epsilon_patch => "Importation intensity",
    :export_weight => "Export weight"
)
# Ituri is the export reference, its weight fixed at one, so its pair plot
# leaves the weight out.
province_pair_figs = [
    plot_pair(
        chn_joint,
        [
            :R_T_patch, :province_ascertainment,
            :CFR_patch, :province_death_ascertainment,
            :importation_epsilon_patch,
            (p == 1 ? () : (:export_weight,))...,
        ];
        patch = p, prior = prior_patch_chn, labels = province_pair_labels
    )
        for p in 1:N_PATCHES
];
Ituri pair plot (prior overlaid)

Nord-Kivu pair plot (prior overlaid)

Haut-Uele pair plot (prior overlaid)

Other provinces pair plot (prior overlaid)

Composition checks ​

Whether the model reproduces each province's observed share of the national total is on the in-sample checks page.

Data currency ​

The province blocks are the situation reports' provincial tables: confirmed cases, confirmed deaths and the 24h laboratory volumes. Each is read here against the cut-off, so a block that stops updating shows as a date before it rather than as a flat series.

Province block currency
julia
# Every province block whose last vintage falls before the cut-off, from
# the shared stream registry rather than a per-page list of dates. The
# grace the national table allows is not applied here: one vintage behind
# is already worth reading.
province_currency = let
    status = stream_report_status(obs; stratum = :province)
    behind = status[[ismissing(d) || d > 0 for d in status.days_since], :]
    isempty(behind) ?
        Markdown.parse(
            "Every province block reports to the cut-off, $(obs.cutoff)."
        ) :
        MarkdownTable(
            DataFrame(
                "Block" => behind.label,
                "Last reported" => [
                    ismissing(d) ? "never" : string(d)
                    for d in behind.last_date
                ],
                "Days before cut-off" => [
                    ismissing(d) ? "-" : string(d)
                    for d in behind.days_since
                ]
            )
        )
end;

Every province block reports to the cut-off, 2026-09-26.

Saving province assets ​

The summary dashboard shows the province comparison and the reproduction number by province, so they are written here rather than on the National page.

Write the province dashboard assets
julia
dashboard_dir = joinpath(
    pkgdir(BVDOutbreakSize), "docs", "src", "summary_assets"
)
mkpath(dashboard_dir)
CairoMakie.save(joinpath(dashboard_dir, "rt_provinces.png"), province_rt_fig)
open(joinpath(dashboard_dir, "provinces.md"), "w") do io
    print(io, province_headline_md)
end

The full analysis code, data and model definitions are in the epiforecasts/BVDOutbreakSize repository.