Skip to content

Health-zone estimates ​

This page splits each patch of the joint fit across its health zones, giving each zone's reproduction number, share of the patch, one-week forecast and probability of at least a few cases. The health-zone model on the methods page gives the maths, and the interactive map is on the dashboard. The one-week zone forecast is on the health-zone forecasts page and its scores in the health-zone forecast evaluation.

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: the headline joint,
# the zone fit melded from it, and the frozen zone fit the sampler
# diagnostics read.
chn_joint = load_fit("joint");
chn_local = load_fit("local");
frozen_local = load_fit("local_frozen_validation");

# The zone stage's fixed inputs with the joint's forecast, from the shared
# setup, so the health-zone forecast page draws the same forecast from the
# same inputs.
zone_inputs = zone_stage_inputs(; forecast = true);
zone_patch = zone_inputs.patch_of_zone;

Last updated: 23 October 2026.

Data as of: 20 October 2026.

Estimates by zone ​

The interactive map on the dashboard adds the probability of at least cases at a chosen and a filter by recent cases.

Health-zone post-processing
julia
# The geojson keys a zone without the province prefix the manifest carries.
zone_map_keys = [
    String(last(split(k, "."; limit = 2)))
        for k in zone_inputs.zone_keys
];
# Daily zone reproduction numbers over the zone grid, each zone draw on
# its own draw of the patch trajectory so the patch uncertainty is
# carried, and the cut-off values the map and table read. A zone's
# reproduction number is reported from the day its cumulative infections
# reach the floor in the median draw.
zone_rt_traj = reconstruct_zone_rt(chn_local, zone_inputs);
zone_RT_finite = [filter(isfinite, m[:, obs.n]) for m in zone_rt_traj];
zone_RT_reported = findall(!isempty, zone_RT_finite);
zone_share_T = let vs = vec(collect(chn_local[:share_T_zone]))
    [Float64[v[z] for v in vs] for z in eachindex(zone_map_keys)]
end;
_zq(v, p) = quantile(v, p)
# The one-week zone forecast drawn from the zone model, and the
# probability of at least K cases per zone from the same draws.
zone_fc = zone_forecast(chn_local, zone_inputs);
zone_fc_draws = zone_forecast_draws(zone_fc, zone_inputs);
ZONE_THRESHOLDS = (1, 5, 10, 20)
zone_fc_probs = zone_forecast_probabilities(
    zone_fc, zone_inputs; thresholds = ZONE_THRESHOLDS, draws = zone_fc_draws
);
zone_overview = zone_overview_table(chn_local, zone_inputs);
# Cases allocated to each zone over the past one, two and four weeks,
# and the last vintage on which each zone's count rose.
zone_recent = Dict(
    w => zone_recent_cases(zone_inputs; window = w) for w in (7, 14, 28)
);
zone_last_case = zone_last_case_dates(zone_inputs);

zone_map_fig = plot_zone_map_panels(
    [
        (;
            values = [median(zone_RT_finite[z]) for z in zone_RT_reported],
            zones = zone_map_keys[zone_RT_reported],
            lower = [_zq(zone_RT_finite[z], 0.05) for z in zone_RT_reported],
            upper = [_zq(zone_RT_finite[z], 0.95) for z in zone_RT_reported],
            diverging_at = 1.0, scale = log10,
            title = "Reproduction number at the cut-off",
            colorbar_label = "R",
        ),
        (;
            values = [_zq(v, 0.05) for v in zone_fc_draws.zones],
            zones = zone_map_keys, scale = CairoMakie.Makie.pseudolog10,
            title = "Confirmed cases over the coming week (lower 90%)",
            colorbar_label = "cases",
        ),
        (;
            values = [_zq(v, 0.95) for v in zone_fc_draws.zones],
            zones = zone_map_keys, scale = CairoMakie.Makie.pseudolog10,
            title = "Confirmed cases over the coming week (upper 90%)",
            colorbar_label = "cases",
        ),
        (;
            values = Float64.(zone_inputs.cumulative), zones = zone_map_keys,
            scale = CairoMakie.Makie.pseudolog10,
            title = "Confirmed cases to date", colorbar_label = "cases",
        ),
    ];
    ncols = 2, title = "Health zones at the cut-off"
);

The reproduction number at the cut-off, the bounds of the 90% interval on next week's confirmed cases, and the confirmed cases to date, by health zone. A zone whose reproduction number interval straddles one is washed towards white, and zones with no confirmed case or too few infections for a reproduction number are grey.
Compute the weekly cases and reproduction number by zone
julia
zone_cases_rt = cases_rt_table(
    zone_rt_traj, zone_inputs.days, zone_inputs.counts;
    cutoff = obs.cutoff, n = obs.n, areas = zone_inputs.zone_labels,
    patch = zone_patch
);
zone_cases_rt_fig = plot_cases_rt(
    zone_cases_rt; patch_labels = zone_inputs.patch_labels, top = 12,
    unit = "health zone"
);

The twelve zones with the most confirmed cases over the past two weeks, each against its reproduction number, with the week before. Faint bars span the 90% credible interval of the reproduction number.
Zone overview table
julia
# The overview as displayed: the interval strings, without the numeric
# columns.
zone_overview_display = let d = zone_overview[
        :,
        [:zone, :patch, :cases, :share, :R_T, :p_R_above_1, :delta_T],
    ]
    d[!, "R modelled separately"] = [
        w ? "yes" : "no" for w in zone_overview.walking
    ]
    d
end;
zonepatchcasesshareR_Tp_R_above_1delta_TR modelled separately
BeniNord-Kivu55348.0 (30.1–65.2)1.11 (0.75–1.55)0.670.11 (-0.39–0.66)yes
KalungutaNord-Kivu815.2 (2.8–9.9)1.12 (0.7–1.75)0.66-0.01 (-0.46–0.44)yes
OichaNord-Kivu545.8 (2.5–13.5)1.13 (0.65–1.91)0.660.09 (-0.52–0.72)yes
Boma MangbetuHaut-Uele4510.7 (5.5–18.7)1.12 (0.69–1.78)0.64-0.05 (-0.42–0.27)yes
MutwangaNord-Kivu312.5 (0.9–7.4)1.14 (0.62–1.94)0.630.11 (-0.44–0.68)yes
MabalakoNord-Kivu838.1 (3.3–19.5)1.11 (0.59–1.97)0.610.14 (-0.43–0.71)yes
NyankundeIturi1340.9 (0.3–2.8)1.09 (0.55–2.25)0.59-0.03 (-0.88–0.83)yes
LolwaIturi360.4 (0.1–1.6)1.08 (0.48–2.27)0.58-0.12 (-1.15–0.84)yes
WambaHaut-Uele15240.4 (21.8–63.4)1.07 (0.62–1.69)0.580.07 (-0.35–0.48)yes
TchomiaIturi820.5 (0.1–2.2)1.08 (0.49–2.27)0.570.04 (-0.87–0.92)yes
KyondoNord-Kivu441.2 (0.5–3.0)1.07 (0.59–1.93)0.57-0.07 (-0.6–0.41)yes
MambasaIturi766.7 (1.8–24.3)1.08 (0.41–2.46)0.560.22 (-0.87–1.33)yes
KiloIturi380.2 (0.1–0.6)1.05 (0.52–2.0)0.55-0.08 (-0.88–0.62)yes
VuhoviNord-Kivu552.1 (1.0–4.8)1.04 (0.6–1.72)0.55-0.03 (-0.44–0.38)yes
ButemboNord-Kivu3196.0 (2.7–11.7)1.04 (0.58–1.7)0.54-0.09 (-0.53–0.3)yes
DamasIturi410.2 (0.0–1.0)1.03 (0.44–2.41)0.52-0.05 (-1.05–0.9)yes
KomandaIturi1913.2 (1.0–10.0)1.01 (0.43–2.18)0.52-0.13 (-1.12–0.85)yes
RwamparaIturi11738.4 (2.7–20.6)1.01 (0.41–1.92)0.510.07 (-0.74–0.85)yes
KatwaNord-Kivu6488.9 (4.2–17.2)1.01 (0.57–1.68)0.51-0.12 (-0.55–0.27)yes
IsiroHaut-Uele1089.1 (3.5–25.3)1.02 (0.53–1.85)0.51-0.01 (-0.5–0.49)yes
The twenty highest-ranked zones, with every zone in the fold below. The share is of the patch's infections at the cut-off, in percent, and the share, reproduction number and deviation are medians with 90% intervals. A zone whose reproduction number is not modelled separately takes it from its province.
All health zones
zonepatchcasesshareR_Tp_R_above_1delta_TR modelled separately
BeniNord-Kivu55348.0 (30.1–65.2)1.11 (0.75–1.55)0.670.11 (-0.39–0.66)yes
KalungutaNord-Kivu815.2 (2.8–9.9)1.12 (0.7–1.75)0.66-0.01 (-0.46–0.44)yes
OichaNord-Kivu545.8 (2.5–13.5)1.13 (0.65–1.91)0.660.09 (-0.52–0.72)yes
Boma MangbetuHaut-Uele4510.7 (5.5–18.7)1.12 (0.69–1.78)0.64-0.05 (-0.42–0.27)yes
MutwangaNord-Kivu312.5 (0.9–7.4)1.14 (0.62–1.94)0.630.11 (-0.44–0.68)yes
MabalakoNord-Kivu838.1 (3.3–19.5)1.11 (0.59–1.97)0.610.14 (-0.43–0.71)yes
NyankundeIturi1340.9 (0.3–2.8)1.09 (0.55–2.25)0.59-0.03 (-0.88–0.83)yes
LolwaIturi360.4 (0.1–1.6)1.08 (0.48–2.27)0.58-0.12 (-1.15–0.84)yes
WambaHaut-Uele15240.4 (21.8–63.4)1.07 (0.62–1.69)0.580.07 (-0.35–0.48)yes
TchomiaIturi820.5 (0.1–2.2)1.08 (0.49–2.27)0.570.04 (-0.87–0.92)yes
KyondoNord-Kivu441.2 (0.5–3.0)1.07 (0.59–1.93)0.57-0.07 (-0.6–0.41)yes
MambasaIturi766.7 (1.8–24.3)1.08 (0.41–2.46)0.560.22 (-0.87–1.33)yes
KiloIturi380.2 (0.1–0.6)1.05 (0.52–2.0)0.55-0.08 (-0.88–0.62)yes
VuhoviNord-Kivu552.1 (1.0–4.8)1.04 (0.6–1.72)0.55-0.03 (-0.44–0.38)yes
ButemboNord-Kivu3196.0 (2.7–11.7)1.04 (0.58–1.7)0.54-0.09 (-0.53–0.3)yes
DamasIturi410.2 (0.0–1.0)1.03 (0.44–2.41)0.52-0.05 (-1.05–0.9)yes
KomandaIturi1913.2 (1.0–10.0)1.01 (0.43–2.18)0.52-0.13 (-1.12–0.85)yes
RwamparaIturi11738.4 (2.7–20.6)1.01 (0.41–1.92)0.510.07 (-0.74–0.85)yes
KatwaNord-Kivu6488.9 (4.2–17.2)1.01 (0.57–1.68)0.51-0.12 (-0.55–0.27)yes
IsiroHaut-Uele1089.1 (3.5–25.3)1.02 (0.53–1.85)0.51-0.01 (-0.5–0.49)yes
MandimaIturi36231.3 (8.3–65.0)0.98 (0.34–1.79)0.490.28 (-0.83–1.51)yes
LuberoNord-Kivu361.4 (0.4–4.9)0.98 (0.42–2.12)0.480.03 (-0.59–0.63)yes
MusieneneNord-Kivu1542.3 (0.8–6.0)0.99 (0.49–1.87)0.48-0.08 (-0.61–0.43)yes
BuniaIturi191418.2 (5.7–38.5)0.97 (0.4–1.72)0.470.12 (-0.59–0.85)yes
LitaIturi3153.3 (0.8–10.5)0.97 (0.34–2.0)0.470.11 (-0.7–0.92)yes
MaserekaNord-Kivu391.0 (0.4–2.7)0.97 (0.48–1.84)0.47-0.08 (-0.61–0.42)yes
PawaHaut-Uele10633.8 (16.6–55.4)0.98 (0.54–1.62)0.470.0 (-0.39–0.37)yes
FatakiIturi840.2 (0.1–0.9)0.94 (0.38–2.31)0.44-0.11 (-0.97–0.74)yes
BambuIturi2160.9 (0.3–2.7)0.92 (0.41–1.92)0.43-0.14 (-0.88–0.58)yes
NiziIturi8324.1 (1.2–12.0)0.87 (0.31–1.84)0.380.05 (-0.61–0.75)yes
MangalaIturi3821.7 (0.5–6.7)0.83 (0.32–2.06)0.37-0.07 (-0.87–0.76)yes
MongbwaluIturi7171.8 (0.4–8.2)0.8 (0.27–2.12)0.36-0.05 (-1.05–0.93)yes
Nia-NiaIturi3342.6 (0.7–10.9)0.72 (0.26–1.97)0.31-0.12 (-1.27–1.02)yes
WapindaNord-Ubangi3100.0 (100.0–100.0)1.48 (1.17–1.79)0.990.0 (0.0–0.0)no
MangoboTshopo1021.2 (13.4–29.2)1.27 (0.92–1.72)0.890.0 (-0.0–0.0)no
Makiso-KisanganiTshopo3345.8 (37.2–54.9)1.24 (0.9–1.67)0.870.0 (-0.0–0.0)no
KabondoTshopo1011.3 (6.4–17.3)1.22 (0.89–1.66)0.860.0 (-0.0–0.0)no
BafwasendeTshopo1813.2 (7.8–20.4)1.19 (0.86–1.58)0.83-0.0 (-0.0–0.0)no
GangaBas-Uele542.7 (28.4–59.1)1.11 (0.88–1.35)0.79-0.0 (-0.0–0.0)no
ViadanaBas-Uele638.3 (21.2–56.1)1.08 (0.87–1.31)0.75-0.0 (-0.0–0.0)no
GetyIturi110.3 (0.1–0.6)1.09 (0.7–1.57)0.63-0.0 (-0.0–0.0)no
ManguredjipaNord-Kivu160.6 (0.3–1.1)1.07 (0.71–1.52)0.62-0.0 (-0.0–0.0)no
BogaIturi20.1 (0.0–0.4)1.07 (0.67–1.6)0.610.0 (-0.0–0.0)no
BienaNord-Kivu240.8 (0.4–1.4)1.06 (0.69–1.52)0.6-0.0 (-0.0–0.0)no
AruIturi150.2 (0.1–0.5)1.03 (0.63–1.53)0.55-0.0 (-0.0–0.0)no
AriwaraIturi80.1 (0.0–0.2)1.02 (0.63–1.52)0.54-0.0 (-0.0–0.0)no
LogoIturi140.2 (0.1–0.5)1.03 (0.64–1.52)0.54-0.0 (-0.0–0.0)no
RimbaIturi190.2 (0.1–0.4)1.02 (0.63–1.52)0.54-0.0 (-0.0–0.0)no
MahagiIturi40.1 (0.0–0.2)1.02 (0.61–1.52)0.53-0.0 (-0.0–0.0)no
AdjaIturi110.0 (0.0–0.1)1.01 (0.62–1.53)0.52-0.0 (-0.0–0.0)no
RunguHaut-Uele61.1 (0.5–2.0)0.99 (0.63–1.51)0.49-0.0 (-0.0–0.0)no
AungbaIturi120.1 (0.0–0.2)0.99 (0.6–1.5)0.48-0.0 (-0.0–0.0)no
KambalaIturi20.0 (0.0–0.1)0.99 (0.59–1.49)0.48-0.0 (-0.0–0.0)no
DrodroIturi150.2 (0.1–0.4)0.97 (0.56–1.47)0.470.0 (-0.0–0.0)no
AlimbongoNord-Kivu50.2 (0.1–0.4)NaN-0.0 (-0.0–0.0)no
GomaNord-Kivu10.0 (0.0–0.1)NaN-0.0 (-0.0–0.0)no
KaynaNord-Kivu30.1 (0.0–0.3)NaN-0.0 (-0.0–0.0)no
DunguHaut-Uele10.2 (0.1–0.6)NaN-0.0 (-0.0–0.0)no
GombariHaut-Uele30.4 (0.1–0.9)NaN-0.0 (-0.0–0.0)no
Miti-MurhesaSud-Kivu3100.0 (100.0–100.0)NaN0.0 (0.0–0.0)no
LubungaTshopo12.2 (0.7–4.9)NaN0.0 (-0.0–0.0)no
TshopoTshopo32.7 (1.0–5.9)NaN-0.0 (-0.0–0.0)no
Wanie-RukulaTshopo12.0 (0.6–4.7)NaN-0.0 (-0.0–0.0)no
ButaBas-Uele117.3 (6.6–33.3)NaN-0.0 (-0.0–0.0)no
BuluSud-Ubangi2100.0 (100.0–100.0)NaN0.0 (0.0–0.0)no

Point prevalence ​

The zone estimates split the infections in the community at the cut-off in the same way as the province estimates. The point prevalence section of the Methods gives the calculation. The figure and table show the twenty zones with the highest rate in the community per 100,000 residents. Every zone is listed in the fold below the table.

Compute point prevalence by zone
julia
zone_population = Dict(
    string(z.province, ".", z.zone) => z.population for z in load_health_zones()
);
zone_prevalence_overview = prevalence_overview(
    zone_prevalence(chn_local, zone_inputs, chn_joint),
    [zone_population[k] for k in zone_inputs.zone_keys];
    labels = zone_inputs.zone_labels, patch = zone_patch
);
zone_prevalence_fig = plot_prevalence_ranking(
    zone_prevalence_overview;
    patch_labels = zone_inputs.patch_labels, max_rows = 20,
    title = "Point prevalence by health zone"
);

julia
MarkdownTable(
    prevalence_table(zone_prevalence_overview; max_rows = 20, area = "Zone")
ZonePopulationInfected but not yet symptomatic per 100,000Symptomatic, undetected per 100,000In the community per 100,000Number in the community
Mandima28663756.9 (18.7–144.6)64.2 (29.5–128.6)121.9 (52.3–268.3)350 (150–769)
Beni42885439.9 (20.0–79.1)43.1 (21.6–78.7)83.9 (43.3–156.9)360 (186–673)
Mambasa15080722.5 (6.9–80.5)23.4 (9.4–60.5)47.1 (17.7–137.1)71 (27–207)
Nia-Nia9175615.4 (4.5–55.7)23.1 (9.3–54.6)38.9 (15.1–107.2)36 (14–98)
Bunia50906017.8 (6.6–43.0)20.4 (9.5–42.2)38.2 (17.9–81.4)194 (91–414)
Nizi12969716.3 (5.4–48.0)19.4 (7.9–45.4)36.3 (14.3–90.2)47 (19–117)
Rwampara25232416.6 (6.2–45.1)17.6 (7.0–41.5)34.4 (14.9–83.8)87 (38–211)
Lita11336414.8 (4.5–48.6)17.0 (7.1–43.0)32.2 (13.0–87.1)37 (15–99)
Mabalako19713615.0 (5.6–41.7)15.4 (6.1–34.1)30.3 (12.3–74.0)60 (24–146)
Mangala897139.8 (3.0–34.4)12.9 (5.1–32.3)23.1 (8.9–65.2)21 (8–59)
Kalunguta1917329.6 (4.1–23.3)9.9 (4.1–22.1)19.6 (8.6–44.0)38 (17–84)
Wamba2247579.4 (3.6–23.1)9.4 (4.0–20.8)18.9 (8.1–43.9)43 (18–99)
Komanda2199487.2 (2.3–24.2)8.7 (3.4–21.1)16.1 (6.3–44.1)35 (14–97)
Oicha2749987.5 (2.9–21.1)7.7 (3.2–17.5)15.3 (6.6–37.3)42 (18–102)
Butembo2893787.0 (2.9–18.2)7.9 (3.5–17.3)14.9 (7.0–34.4)43 (20–100)
Mongbwalu1577145.9 (1.6–25.8)8.6 (3.3–23.1)14.7 (5.3–47.2)23 (8–74)
Pawa3017685.8 (2.1–15.1)6.8 (2.9–14.6)12.7 (5.4–29.0)38 (16–87)
Katwa6122425.0 (2.1–12.8)5.9 (2.8–12.2)11.0 (5.2–23.8)67 (32–146)
Lolwa413145.3 (1.3–19.3)5.5 (1.7–16.9)10.9 (3.3–35.5)5 (1–15)
Vuhovi1519534.9 (2.0–13.4)5.5 (2.1–12.5)10.5 (4.3–25.3)16 (6–38)
Point prevalence in every health zone
ZonePopulationInfected but not yet symptomatic per 100,000Symptomatic, undetected per 100,000In the community per 100,000Number in the community
Mandima28663756.9 (18.7–144.6)64.2 (29.5–128.6)121.9 (52.3–268.3)350 (150–769)
Beni42885439.9 (20.0–79.1)43.1 (21.6–78.7)83.9 (43.3–156.9)360 (186–673)
Mambasa15080722.5 (6.9–80.5)23.4 (9.4–60.5)47.1 (17.7–137.1)71 (27–207)
Nia-Nia9175615.4 (4.5–55.7)23.1 (9.3–54.6)38.9 (15.1–107.2)36 (14–98)
Bunia50906017.8 (6.6–43.0)20.4 (9.5–42.2)38.2 (17.9–81.4)194 (91–414)
Nizi12969716.3 (5.4–48.0)19.4 (7.9–45.4)36.3 (14.3–90.2)47 (19–117)
Rwampara25232416.6 (6.2–45.1)17.6 (7.0–41.5)34.4 (14.9–83.8)87 (38–211)
Lita11336414.8 (4.5–48.6)17.0 (7.1–43.0)32.2 (13.0–87.1)37 (15–99)
Mabalako19713615.0 (5.6–41.7)15.4 (6.1–34.1)30.3 (12.3–74.0)60 (24–146)
Mangala897139.8 (3.0–34.4)12.9 (5.1–32.3)23.1 (8.9–65.2)21 (8–59)
Kalunguta1917329.6 (4.1–23.3)9.9 (4.1–22.1)19.6 (8.6–44.0)38 (17–84)
Wamba2247579.4 (3.6–23.1)9.4 (4.0–20.8)18.9 (8.1–43.9)43 (18–99)
Komanda2199487.2 (2.3–24.2)8.7 (3.4–21.1)16.1 (6.3–44.1)35 (14–97)
Oicha2749987.5 (2.9–21.1)7.7 (3.2–17.5)15.3 (6.6–37.3)42 (18–102)
Butembo2893787.0 (2.9–18.2)7.9 (3.5–17.3)14.9 (7.0–34.4)43 (20–100)
Mongbwalu1577145.9 (1.6–25.8)8.6 (3.3–23.1)14.7 (5.3–47.2)23 (8–74)
Pawa3017685.8 (2.1–15.1)6.8 (2.9–14.6)12.7 (5.4–29.0)38 (16–87)
Katwa6122425.0 (2.1–12.8)5.9 (2.8–12.2)11.0 (5.2–23.8)67 (32–146)
Lolwa413145.3 (1.3–19.3)5.5 (1.7–16.9)10.9 (3.3–35.5)5 (1–15)
Vuhovi1519534.9 (2.0–13.4)5.5 (2.1–12.5)10.5 (4.3–25.3)16 (6–38)
Nyankunde940044.6 (1.4–14.8)4.8 (1.7–13.2)9.5 (3.4–27.3)9 (3–26)
Bambu1361513.3 (1.1–10.0)4.1 (1.6–9.8)7.5 (3.1–19.1)10 (4–26)
Mutwanga2552783.6 (1.2–12.5)3.5 (1.3–9.7)7.2 (2.7–21.9)18 (7–56)
Manguredjipa680143.1 (1.3–7.4)3.3 (1.4–7.4)6.5 (2.9–14.3)4 (2–10)
Tchomia839173.1 (0.8–13.3)3.0 (0.9–10.8)6.2 (1.8–24.2)5 (2–20)
Musienene3483922.3 (0.8–6.7)2.6 (1.0–6.4)5.0 (2.0–12.5)17 (7–43)
Mangobo1886522.5 (1.0–6.6)2.3 (0.9–5.5)4.9 (2.0–11.9)9 (4–22)
Boma Mangbetu2301042.3 (0.9–6.1)2.5 (1.0–5.7)4.8 (2.0–11.6)11 (5–27)
Biena1361592.1 (0.9–4.9)2.3 (1.0–5.1)4.5 (2.0–9.6)6 (3–13)
Kilo431392.1 (0.6–7.4)2.3 (0.7–6.6)4.4 (1.5–13.9)2 (1–6)
Bafwasende1322642.3 (0.9–6.1)2.2 (0.9–5.3)4.4 (1.8–11.1)6 (2–15)
Wapinda977122.3 (0.4–12.2)1.9 (0.4–8.5)4.2 (0.8–21.3)4 (1–21)
Makiso-Kisangani5357981.9 (0.8–4.5)1.8 (0.8–3.9)3.8 (1.7–8.2)20 (9–44)
Kyondo2703441.5 (0.5–4.5)1.6 (0.6–4.1)3.2 (1.3–8.4)9 (3–23)
Lubero3775061.4 (0.4–5.2)1.6 (0.5–4.4)3.0 (1.0–9.2)11 (4–35)
Masereka2986361.2 (0.4–3.7)1.3 (0.5–3.5)2.6 (1.0–7.2)8 (3–22)
Kabondo2046291.2 (0.5–3.3)1.2 (0.4–2.9)2.4 (0.9–6.2)5 (2–13)
Damas948201.1 (0.3–4.8)1.1 (0.3–4.2)2.2 (0.6–8.7)2 (1–8)
Gety1434421.0 (0.4–2.5)1.1 (0.4–2.6)2.1 (0.8–5.0)3 (1–7)
Boga720281.0 (0.3–2.9)1.0 (0.3–3.0)2.0 (0.6–5.8)1 (0–4)
Isiro5124010.9 (0.3–3.4)1.0 (0.4–2.9)2.0 (0.7–6.1)10 (4–31)
Fataki1580320.6 (0.2–2.8)0.8 (0.2–2.5)1.4 (0.4–5.2)2 (1–8)
Logo2169440.5 (0.2–1.4)0.6 (0.2–1.5)1.1 (0.4–2.8)2 (1–6)
Drodro1999960.4 (0.2–1.1)0.5 (0.2–1.2)1.0 (0.4–2.2)2 (1–4)
Viadana1878730.4 (0.2–1.4)0.5 (0.2–1.4)0.9 (0.3–2.8)2 (1–5)
Ganga2354010.4 (0.1–1.3)0.4 (0.1–1.2)0.8 (0.3–2.5)2 (1–6)
Rimba2222820.4 (0.1–1.0)0.4 (0.2–1.0)0.8 (0.3–1.9)2 (1–4)
Tshopo1728850.3 (0.1–1.1)0.3 (0.1–1.0)0.7 (0.2–2.2)1 (0–4)
Bulu1674810.3 (0.3–0.3)0.3 (0.2–0.4)0.6 (0.5–0.7)1 (1–1)
Rungu1948040.3 (0.1–0.9)0.3 (0.1–0.9)0.6 (0.2–1.7)1 (0–3)
Lubunga1547120.3 (0.1–1.0)0.3 (0.1–0.9)0.6 (0.2–1.9)1 (0–3)
Aru3296670.3 (0.1–0.8)0.3 (0.1–0.8)0.6 (0.2–1.6)2 (1–5)
Wanie-Rukula1552970.3 (0.1–0.9)0.3 (0.1–0.9)0.6 (0.1–1.8)1 (0–3)
Alimbongo2968210.2 (0.1–0.6)0.2 (0.1–0.6)0.5 (0.2–1.2)1 (0–4)
Adja1269330.2 (0.1–0.6)0.2 (0.1–0.6)0.4 (0.1–1.2)1 (0–2)
Aungba2167870.2 (0.1–0.5)0.2 (0.1–0.5)0.4 (0.1–1.0)1 (0–2)
Gombari1229010.2 (0.0–0.5)0.2 (0.1–0.6)0.4 (0.1–1.1)0 (0–1)
Kambala1409560.1 (0.0–0.5)0.2 (0.0–0.5)0.3 (0.1–1.0)0 (0–1)
Miti-Murhesa3039830.1 (0.1–0.2)0.1 (0.1–0.2)0.3 (0.2–0.3)1 (1–1)
Buta2749640.1 (0.0–0.5)0.1 (0.0–0.5)0.3 (0.1–1.0)1 (0–3)
Kayna3226280.1 (0.0–0.4)0.1 (0.0–0.4)0.3 (0.1–0.8)1 (0–2)
Mahagi2478930.1 (0.0–0.4)0.1 (0.0–0.4)0.3 (0.1–0.7)1 (0–2)
Ariwara3147300.1 (0.0–0.4)0.1 (0.0–0.4)0.3 (0.1–0.8)1 (0–2)
Goma2828380.0 (0.0–0.2)0.1 (0.0–0.2)0.1 (0.0–0.3)0 (0–1)
Dungu3077630.0 (0.0–0.1)0.0 (0.0–0.2)0.1 (0.0–0.3)0 (0–1)

Composition checks ​

Whether the model reproduces each zone's observed share of its patch's confirmed cases and deaths is on the in-sample checks page.

Data currency ​

The zone blocks are the per-zone confirmed-case and confirmed-death tables of the situation reports. A block that stops being picked up shows its last date before the cut-off rather than a flat series.

Zone block currency
julia
# Every zone block whose last vintage falls before the cut-off. The grace
# the national table allows is not applied here: one vintage behind is
# already worth reading.
zone_currency = let
    status = stream_report_status(obs; stratum = :zone)
    behind = status[[ismissing(d) || d > 0 for d in status.days_since], :]
    isempty(behind) ?
        Markdown.parse(
            "Every zone 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 zone block reports to the cut-off, 2026-10-20.

Health-zone fit diagnostics ​

Zone fit diagnostics
julia
_zone_per_chain(v) = join(string.(round.(v; sigdigits = 3)), " / ")
function _zone_sampler_row(label, chn)
    d = zone_sampler_diagnostics(chn)
    return (
        fit = label, max_rhat = round(d.max_rhat; digits = 3),
        min_ess_bulk = round(d.min_ess_bulk; digits = 0),
        min_ess_tail = round(d.min_ess_tail; digits = 0),
        divergences = d.n_divergent,
        depth_cap = _zone_per_chain(d.depth_cap_fraction),
        ebfmi = _zone_per_chain(d.ebfmi),
        step_size = _zone_per_chain(d.step_size),
    )
end
zone_sampler_table = DataFrame(
    [
        _zone_sampler_row("health zones", chn_local),
        _zone_sampler_row("health zones (frozen)", frozen_local.chn),
    ]
);
zone_diagnostics = let d = zone_diagnostics_table(chn_local, zone_inputs)
    for c in names(d)[3:end]
        d[!, c] = round.(d[!, c]; digits = startswith(c, "rhat") ? 3 : 0)
    end
    DataFrame(
        [
            n == "walking" ?
                "R modelled separately" => [w ? "yes" : "no" for w in d[!, n]] :
                n => d[!, n]
                for n in names(d)
        ]
    )
end;
fitmax_rhatmin_ess_bulkmin_ess_taildivergencesdepth_capebfmistep_size
health zones1.01727135910.001 / 0.00.98 / 0.8720.00824 / 0.00848
health zones (frozen)1.0514517100.001 / 0.0010.879 / 0.8610.00701 / 0.00706
Sampler diagnostics of the two zone fits. The worst R-hat and smallest effective sample sizes cover every stored quantity except the zone reproduction number. Per chain, the table also gives the fraction of iterations at the tree-depth cap, the energy fraction of missing information and the adapted step size.
Per-zone R-hat and effective sample sizes
zoneR modelled separatelyrhat_R_Tess_bulk_R_Tess_tail_R_Trhat_share_Tess_bulk_share_Tess_tail_share_Trhat_delta_Tess_bulk_delta_Tess_tail_delta_T
AdjanoNaNNaNNaN1200416941.0028941047
AriwaranoNaNNaNNaN1.001161117441.00216461168
Aruno1181613641.001104114511.001591957
AungbanoNaNNaNNaN1.002180017821642989
Bambuyes1274615341225114531.00332371548
BoganoNaNNaNNaN1.002143814241.00322171319
Buniayes1.002266812240.99931691578126541527
Damasyes125941564126431587135221395
Drodrono1223712731.001162817941.00114991026
Fatakiyes1308515761.001293817540.99940511603
GetynoNaNNaNNaN1.001178816031.00221871460
KambalanoNaNNaNNaN118911495110581111
Kiloyes1.001209611671.001166913151.00233361379
Komandayes1210714850.999207218170.99924591577
Litayes1.00125251407128161602130191526
Logono1197414011134614281.0016791045
Lolwayes1249315211.00423941796130401664
MahaginoNaNNaNNaN1.0021859169417031092
Mambasayes0.999229915121.001258214731.00132361316
Mandimayes1295912901316913751.00133671344
Mangalayes1.002277415951.00127451564134741450
Mongbwaluyes1.00129181494126611823130271190
Nia-Niayes1265813361274516130.99935461464
Niziyes1.001294314501.00228281608128711649
Nyankundeyes0.99923791401121091571127091274
RimbanoNaNNaNNaN1155315181.001525946
Rwamparayes1.002302814841261516551.00129141362
Tchomiayes1.001250914121.001248715900.99932891185
AlimbongonoNaNNaNNaN1262816621.001877997
Beniyes1374716471.00124361631128411204
BienanoNaNNaNNaN1.001232219101.00420761213
Butemboyes1.004295414761310017491.00133001290
GomanoNaNNaNNaN1.001231714681.00310451317
Kalungutayes1330913971.004237513181.00436531167
Katwayes1.001264613061.001290512641.00632311309
KaynanoNaNNaNNaN1.001232017461.003706991
Kyondoyes1.002312114471.001244815541.00234041357
Luberoyes136011284136361668139001442
Mabalakoyes137331381129851497137571329
ManguredjipanoNaNNaNNaN11800173811362803
Maserekayes1315014720.999308016111.00137741494
Musieneneyes1.00232891637128271648133131454
Mutwangayes1367513281287917211.00235321165
Oichayes1340813061378014831.00139041335
Vuhoviyes1.001272815261.00328611923134891572
Boma Mangbetuyes1.004286914251.001231516290.99935551542
DungunoNaNNaNNaN1.001215119651.0018181173
GombarinoNaNNaNNaN1.001163416921.0018141209
Isiroyes1313115590.999253017341.00132541513
Pawayes133091416128411702134301641
RungunoNaNNaNNaN11890193511316967
Wambayes1.002340114061270614940.99933041446
Miti-MurhesanoNaNNaNNaN12107NaNNaNNaNNaN
BafwasendenoNaNNaNNaN1.001199918601.0021681965
KabondonoNaNNaNNaN1.002165619091.00120481220
LubunganoNaNNaNNaN125081718122651179
Makiso-Kisanganino1.002296914181166517941.00115691096
MangobonoNaNNaNNaN1.001183218751.00216871242
TshoponoNaNNaNNaN1.001358217381.00127031283
Wanie-RukulanoNaNNaNNaN1.002209911821.00321871201
ButanoNaNNaNNaN1320416591.00125591257
GanganoNaNNaNNaN1328316951.00226931304
ViadananoNaNNaNNaN1.001312317921.00422931034
BulunoNaNNaNNaN1.0032091NaNNaNNaNNaN
WapindanoNaNNaNNaN1.00217671672NaNNaNNaN
Per-zone R-hat and effective sample sizes of the cut-off reproduction number, share and deviation. The rows whose reproduction number is modelled separately are the ones to read.
Reproduction number from the zone stage and the joint fit
julia
# Each patch's implied reproduction number from the joint draws with the
# joint posterior mean generation interval, so the patch line is close to
# the quantity the zone values average to.
zone_grid = zone_inputs.t0:obs.n
_patch_infection_draws = vec(collect(chn_joint[:infections_patch]));
patch_implied_rt = [
    let m = Matrix{Float64}(
            undef,
            length(_patch_infection_draws), obs.n
        )
        for (i, v) in enumerate(_patch_infection_draws)
            I = reshape(Float64.(v), N_PATCHES, obs.n)
            m[i, :] .= implied_national_Rt(I[p, :], zone_inputs.g)
        end
        m
    end
        for p in 1:N_PATCHES
];
# The implied reproduction number of each row of `draws` (draws × days).
function _implied_rt_matrix(draws::AbstractMatrix)
    m = similar(draws, Float64)
    for i in axes(draws, 1)
        m[i, :] .= implied_national_Rt(view(draws, i, :), zone_inputs.g)
    end
    return m
end
# The zone stage's deformed patch trajectories on one side and the joint's
# draws on the other, nationally then per patch.
zone_stage_rt = let I = zone_patch_infections(chn_local, zone_inputs)
    vcat([_implied_rt_matrix(sum(I))], _implied_rt_matrix.(I))
end;
joint_stage_rt = let draws = _patch_infection_draws
    national = Float64[
        sum(reshape(Float64.(v), N_PATCHES, obs.n)[:, t])
            for v in draws, t in 1:obs.n
    ]
    vcat([_implied_rt_matrix(national)], patch_implied_rt)
end;
zone_meld_rt_fig = plot_rt_zones(
    [m[:, zone_grid] for m in zone_stage_rt],
    vcat(["National"], zone_inputs.patch_labels),
    vcat([N_PATCHES + 1], 1:N_PATCHES);
    patch_labels = vcat(zone_inputs.patch_labels, ["National"]),
    patch_colours = vcat(BVDOutbreakSize._ZONE_PATCH_COLOURS, [:black]),
    dates = grid_date.(zone_grid), as_of_date = obs.cutoff,
    top = N_PATCHES + 1, ncols = 3, unit = "patch",
    reference_rt = [m[:, zone_grid] for m in joint_stage_rt],
    reference_label = "Headline joint fit",
    title = "Reproduction number from the zone stage and the joint fit"
);

The reproduction number implied by the zone stage's patch trajectories against that implied by the joint fit, nationally and for each patch, both using the joint posterior mean generation interval. Agreement means the melding has kept the joint's patch trajectories rather than moved them to fit the zone data.

Health-zone parameters against their priors ​

Draw from the zone model's prior
julia
prior_chn_zone = zone_prior_draws(zone_inputs);
Compute the zone prior and posterior table
julia
# Each scalar zone hyperparameter with its table label and pair-plot axis
# label, kept where both chains carry it: the mixing and correlation
# blocks are sampled only when their inputs are on.
zone_hyper = [
    h for h in (
            (:region_sd_zone, "Level spread σ_level", "σ_level"),
            (:region_halflife_zone, "Deviation half-life (days)", "half-life (days)"),
            (:correlation_reference_zone, "Correlation ρ_corr", "ρ_corr"),
            (
                :zone_ascertainment_sd, "Ascertainment spread σ_ascertainment",
                "σ_ascertainment",
            ),
            (
                :zone_severity_sd, "Severity spread σ_severity (logit)",
                "σ_severity",
            ),
            (:mixing_within_zone, "Within-patch mixing ε_within", "ε_within"),
            (:mixing_departure_zone, "Mixing departure τ_mix", "τ_mix"),
            (:composition_rho_zone, "Case composition ρ", "ρ"),
            (:composition_rho_death_zone, "Death composition ρ_death", "ρ_death"),
        )
        if BVDOutbreakSize._has_key(chn_local, h[1]) &&
        BVDOutbreakSize._has_key(prior_chn_zone, h[1])
]
zone_hyper_keys = first.(zone_hyper)
_hyper_draws(chn, k) = Float64.(vec(collect(chn[k])))
_drift_draws(chn, p) = Float64[
    v[p] for v in vec(collect(chn[:region_drift_sd_zone]))
]
function _prior_posterior_row(label, post, prior)
    f(x) = string(round(x; sigdigits = 3))
    ci(v) = string(
        f(median(v)), " (", f(quantile(v, 0.05)), "–",
        f(quantile(v, 0.95)), ")"
    )
    return (
        parameter = label, posterior = ci(post), prior = ci(prior),
        sd_ratio = round(std(post) / std(prior); digits = 2),
    )
end
zone_prior_table = DataFrame(
    vcat(
        [
            _prior_posterior_row(
                label, _hyper_draws(chn_local, k),
                _hyper_draws(prior_chn_zone, k)
            )
                for (k, label, _) in zone_hyper
        ],
        [
            _prior_posterior_row(
                "Drift scale σ_δ ($(zone_inputs.patch_labels[p]))",
                _drift_draws(chn_local, p), _drift_draws(prior_chn_zone, p)
            )
                for p in eachindex(zone_inputs.patch_labels)
        ]
    )
);
# The pair plot's axes take the symbols alone.
zone_hyper_pair_fig = plot_pair(
    chn_local, zone_hyper_keys;
    prior = prior_chn_zone,
    labels = Dict(k => sym for (k, _, sym) in zone_hyper)
);
parameterposteriorpriorsd_ratio
Level spread σ_level0.463 (0.0732–0.856)0.198 (0.0214–0.597)1.33
Deviation half-life (days)16.3 (10.0–26.3)43.8 (16.2–127.0)0.14
Correlation ρ_corr1.7e-5 (1.73e-7–0.000586)0.365 (0.0416–0.849)0
Ascertainment spread σ_ascertainment0.176 (0.0415–0.28)0.101 (0.0181–0.422)0.36
Severity spread σ_severity (logit)0.182 (0.017–0.411)0.0951 (0.00718–0.273)1.45
Within-patch mixing ε_within0.079 (0.0427–0.135)0.0366 (0.00306–0.13)0.67
Mixing departure τ_mix0.465 (0.0544–0.941)0.348 (0.0327–0.98)0.88
Case composition ρ0.0287 (0.0257–0.0319)0.0328 (0.0243–0.0426)0.33
Death composition ρ_death0.0402 (0.0344–0.0465)0.0516 (0.0381–0.0701)0.37
Drift scale σ_δ (Ituri)0.47 (0.363–0.631)0.11 (0.0594–0.203)1.76
Drift scale σ_δ (Nord-Kivu)0.304 (0.209–0.439)0.107 (0.0598–0.199)1.55
Drift scale σ_δ (Haut-Uele)0.28 (0.178–0.446)0.111 (0.0587–0.199)1.85
Drift scale σ_δ (Sud-Kivu)0.109 (0.0573–0.205)0.113 (0.0584–0.191)1.12
Drift scale σ_δ (Tshopo)0.109 (0.0595–0.196)0.109 (0.0616–0.203)1.01
Drift scale σ_δ (Bas-Uele)0.109 (0.057–0.201)0.106 (0.0579–0.202)1.02
Drift scale σ_δ (Sud-Ubangi)0.109 (0.0613–0.192)0.107 (0.0585–0.203)0.96
Drift scale σ_δ (Nord-Ubangi)0.109 (0.0591–0.199)0.11 (0.0591–0.207)0.93
Posterior against prior for each zone hyperparameter, with the ratio of their standard deviations, and the drift scale given per patch. A ratio near one means the zone data add little to the prior, and a ratio above one means the data move the parameter into the prior's wider tail.
Zone hyperparameter pair plot (prior overlaid)

Joint posterior of the scalar zone hyperparameters, with the prior overlaid.

Change over the past week ​

How each zone's reproduction number has moved between the fit a week earlier and the current one is on the in-sample checks page.

Saving zone assets ​

The dashboard map reads the per-zone estimates and time series written here, and the summary dashboard's health-zone table is built from them. The zone forecast assets are written by the health-zone forecasts and health-zone forecast evaluation pages.

Write the zone dashboard and release assets
julia
dashboard_dir = joinpath(
    pkgdir(BVDOutbreakSize), "docs", "src", "summary_assets"
)
mkpath(dashboard_dir)
# The health-zone maps, the weekly cases against R and the one-week zone forecast for the summary
# dashboard, and the per-zone estimates the interactive map reads: one row per zone keyed as the
# geojson keys it, with the cases and deaths to date, the reproduction
# number and the chance it exceeds one, the one-week forecast and the
# share of the patch's infections, whether the zone's reproduction number
# is modelled separately (`walking`) and the data cut-off. A zone below the
# reporting floor carries no reproduction number.
CairoMakie.save(joinpath(dashboard_dir, "zone_rt_map.png"), zone_map_fig)
CairoMakie.save(
    joinpath(dashboard_dir, "cases_rt_zones.png"),
    plot_cases_rt(
        zone_cases_rt; patch_labels = zone_inputs.patch_labels, top = 12,
        unit = "health zone", size = (680, 560), fontsize = 12
    )
)
_zone_deaths = [
    let h = obs.zone_death_history
        haskey(h, prov) && haskey(h[prov], z) &&
            !isempty(h[prov][z].counts) ? Int(h[prov][z].counts[end]) :
            0
    end
        for (prov, z) in zip(
            zone_inputs.zone_province,
            zone_inputs.zone_names
        )
]
_zone_rt_stat(f) = [isempty(r) ? missing : f(r) for r in zone_RT_finite]
zone_estimates = DataFrame(
    zone = zone_map_keys,
    label = zone_inputs.zone_labels, province = zone_inputs.zone_province,
    patch = zone_inputs.patch_labels[zone_patch],
    cases = zone_inputs.cumulative, deaths = _zone_deaths,
    R_T_median = _zone_rt_stat(median),
    p_rt_above_one = [
        isempty(r) ? missing : mean(r .> 1) for r in zone_RT_finite
    ],
    R_T_lower = _zone_rt_stat(r -> quantile(r, 0.05)),
    R_T_upper = _zone_rt_stat(r -> quantile(r, 0.95)),
    forecast_median = [median(v) for v in zone_fc_draws.zones],
    forecast_lower = [quantile(v, 0.05) for v in zone_fc_draws.zones],
    forecast_upper = [quantile(v, 0.95) for v in zone_fc_draws.zones],
    share_median = [median(v) for v in zone_share_T],
    share_lower = [quantile(v, 0.05) for v in zone_share_T],
    share_upper = [quantile(v, 0.95) for v in zone_share_T],
    p_ge_1 = zone_fc_probs[:, 1], p_ge_5 = zone_fc_probs[:, 2],
    p_ge_10 = zone_fc_probs[:, 3], p_ge_20 = zone_fc_probs[:, 4],
    cases_last_7 = zone_recent[7], cases_last_14 = zone_recent[14],
    cases_last_28 = zone_recent[28],
    last_case_date = [ismissing(d) ? "" : string(d) for d in zone_last_case],
    walking = Int.(zone_inputs.walking),
    as_of = fill(string(obs.cutoff), length(zone_map_keys))
)
CSV.write(joinpath(dashboard_dir, "zone_estimates.csv"), zone_estimates)
# Each zone's daily reproduction number, weekly allocated confirmed cases
# and weekly confirmed deaths, for the dashboard map's detail column. A
# zone's deaths are its own cumulative series, so each zone is binned on
# its own vintages.
_with_series(t, s) = (t[!, :series] .= s; t)
_zone_weekly_deaths = let h = obs.zone_death_history
    out = DataFrame(area = String[], date = Date[], count = Int[])
    for (prov, z, key) in zip(
            zone_inputs.zone_province, zone_inputs.zone_names, zone_map_keys
        )
        haskey(h, prov) && haskey(h[prov], z) &&
            !isempty(h[prov][z].counts) || continue
        c = h[prov][z].counts
        append!(
            out,
            weekly_count_table(
                h[prov][z].days, reshape([c[1]; max.(diff(c), 0)], 1, :),
                [key]; cutoff = obs.cutoff, n = obs.n
            )
        )
    end
    out
end
CSV.write(
    joinpath(dashboard_dir, "zone_timeseries.csv"),
    vcat(
        _with_series(
            rt_quantile_table(
                zone_rt_traj, zone_map_keys; cutoff = obs.cutoff,
                n = obs.n, from = zone_inputs.t0
            ), "rt"
        ),
        _with_series(
            weekly_count_table(
                zone_inputs.days, zone_inputs.counts, zone_map_keys;
                cutoff = obs.cutoff, n = zone_inputs.n
            ), "cases"
        ),
        _with_series(_zone_weekly_deaths, "deaths");
        cols = :union
    )
)
open(joinpath(dashboard_dir, "zone_headline.md"), "w") do io
    print(io, zone_headline(zone_estimates; top = 10))
end
# The zone estimates and time series into the release outputs, so each
# results release carries the dashboard map's zone inputs at its cut-off.
output_dir = get(
    ENV, "BVD_OUTPUT_DIR",
    joinpath(pkgdir(BVDOutbreakSize), "output")
)
mkpath(output_dir)
for f in ("zone_estimates.csv", "zone_timeseries.csv")
    cp(joinpath(dashboard_dir, f), joinpath(output_dir, f); force = true)
end

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