Skip to content

Province in-sample checks ​

Whether the fitted joint model reproduces the per-province data it was fitted to. It also re-fits the joint as a single population to check the spatial structure. The national checks are on the national in-sample checks 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")
# `sens_no_patches` is the headline with `n_patches = 1`, the check on the
# spatial structure.
chn_no_patches = load_fit("sens_no_patches")
posterior_C_joint = vec(Array(chn_joint[:C_T]))
posterior_C_no_patches = vec(Array(chn_no_patches[:C_T]));

Summary ​

Whether each province's cases and deaths are reproduced, from the checks further down this page. Bias runs from −1 to 1 and is negative when the model under-predicts, and coverage is the fraction of vintages inside the 90% predictive interval.

  • Least well reproduced: Analysed specimens, Nord-Kivu (bias -0.3, 90% coverage 1.0); Analysed specimens, Ituri (bias 0.22, 90% coverage 1.0); Confirmed cases, Haut-Uele (bias 0.11, 90% coverage 0.94).

  • Coverage: 12 of 12 province streams have 90% coverage of at least 0.8; the lowest is Confirmed deaths, Ituri at 0.93.

  • Count-scale coverage: 8 of 8 province streams have 90% coverage of at least 0.8; the lowest is Confirmed deaths, Ituri at 0.92.

Ituri

  • Confirmed cases: bias -0.04 and 90% coverage 0.95 over 99 vintages.

  • Confirmed deaths: bias 0.01 and 90% coverage 0.93 over 99 vintages.

  • Confirmed cases as counts: bias 0.06 and 90% coverage 0.96 over 99 vintages.

  • Confirmed deaths as counts: bias -0.06 and 90% coverage 0.92 over 99 vintages.

Nord-Kivu

  • Confirmed cases: bias 0.02 and 90% coverage 0.95 over 99 vintages.

  • Confirmed deaths: bias -0.03 and 90% coverage 0.94 over 99 vintages.

  • Confirmed cases as counts: bias 0.07 and 90% coverage 0.98 over 99 vintages.

  • Confirmed deaths as counts: bias -0.08 and 90% coverage 0.97 over 99 vintages.

Haut-Uele

  • Confirmed cases: bias 0.11 and 90% coverage 0.94 over 99 vintages.

  • Confirmed deaths: bias 0.03 and 90% coverage 0.97 over 99 vintages.

  • Confirmed cases as counts: bias 0.13 and 90% coverage 0.95 over 99 vintages.

  • Confirmed deaths as counts: bias -0.01 and 90% coverage 0.97 over 99 vintages.

Other provinces

  • Confirmed cases: bias 0.03 and 90% coverage 0.98 over 99 vintages.

  • Confirmed deaths: bias -0.01 and 90% coverage 0.98 over 99 vintages.

  • Confirmed cases as counts: bias 0.05 and 90% coverage 0.98 over 99 vintages.

  • Confirmed deaths as counts: bias -0.01 and 90% coverage 0.98 over 99 vintages.

Province prior predictive check ​

Whether the four-patch prior, before any data are fitted, brackets each province's observed share of the confirmed cases and deaths.

Draw from the four-patch prior
julia
prior_patch_chn = patch_prior_draws(obs);

prior_province_table = patch_overview_table(prior_patch_chn, N_PATCHES);
Show prior province summary table
ProvinceReproduction numberCumulative infectionsShare of infections (%)Relative ascertainment
Ituri1.01 (0.11–1.84)324436 (1630–3882486)88.1 (15.0–99.4)1.0 (0.65–1.51)
Nord-Kivu1.35 (0.17–2.44)13100 (15–7118721)4.2 (0.2–29.9)1.0 (0.65–1.49)
Haut-Uele1.34 (0.15–2.39)5261 (6–1931361)1.8 (0.1–10.1)1.0 (0.67–1.48)
Other provinces1.39 (0.19–2.47)14067 (13–12309122)4.7 (0.2–49.1)1.0 (0.65–1.51)
Province composition prior predictive checks
julia
prior_case_ppc_fig = plot_province_composition_ppc(
    prior_patch_chn;
    share_key = :province_shares,
    obs_increments = province_cases.increments,
    days = province_cases.days, seeding = obs.seeding, n_patches = N_PATCHES,
    title = "Confirmed case share by province, prior"
);

prior_death_ppc_fig = plot_province_composition_ppc(
    prior_patch_chn;
    share_key = :province_death_shares,
    obs_increments = province_deaths.increments,
    days = province_deaths.days, seeding = obs.seeding, n_patches = N_PATCHES,
    title = "Confirmed death share by province, prior"
);

Province prior and posterior pair plot
julia
# Per-province entries of a vector deterministic, one draw vector each,
# keyed `<name>_<patch>` so they can sit side by side in one table.
function _patch_columns(chn, key::Symbol, name::AbstractString; f = identity)
    draws = [collect(v) for v in vec(collect(chn[key]))]
    return [
        Symbol(name, "_", p) => Float64[f(d[p]) for d in draws]
            for p in 1:N_PATCHES
    ]
end
_province_pair_draws(chn) = NamedTuple(
    vcat(
        _patch_columns(chn, :R_T_patch, "R_T"),
        _patch_columns(chn, :C_T_patch, "log10_C_T"; f = x -> log10(x + 1))
    )
)
_province_pair_labels = Dict(
    Symbol(name, "_", p) => string(label, ", ", PROVINCE_LABELS[p])
        for p in 1:N_PATCHES
        for (name, label) in (("R_T", "R_T"), ("log10_C_T", "log10 C_T"))
)
province_pair_fig = plot_pair(
    _province_pair_draws(chn_joint);
    prior = _province_pair_draws(prior_patch_chn),
    labels = _province_pair_labels
);

Province compositions ​

Whether each province's modelled share of the national confirmed cases and deaths reproduces the observed share at every spatial vintage (see the province compositions Methods section).

Province composition posterior predictive checks
julia
province_case_ppc_fig = plot_province_composition_ppc(
    chn_joint;
    share_key = :province_shares,
    obs_increments = province_cases.increments,
    days = province_cases.days, seeding = obs.seeding, n_patches = N_PATCHES,
    title = "Confirmed case share by province"
);

province_death_ppc_fig = plot_province_composition_ppc(
    chn_joint;
    share_key = :province_death_shares,
    obs_increments = province_deaths.increments,
    days = province_deaths.days, seeding = obs.seeding, n_patches = N_PATCHES,
    title = "Confirmed death share by province"
);

The three province terms for the isolation and laboratory data are checked the same way. The laboratory panel is the split of each calendar week's analysed specimens, which identifies the background split. The occupancy panel is the split of the patients in isolation among the provinces printed that day, on the weekly days the fit scores, over the per-patch bed demand. The bed panel is the split of the beds among the provinces printed that day, on the days a count changed, over each patch's modelled share of the national capacity. A gap in a panel is a day on which that province printed nothing.

Province laboratory, occupancy and bed split posterior predictive checks
julia
province_lab_bin_days = [
    maximum(province_lab.days[province_lab.bins .== b])
        for b in 1:maximum(province_lab.bins)
];

province_lab_ppc_fig = plot_province_composition_ppc(
    chn_joint;
    share_key = :province_lab_shares,
    obs_increments = province_lab.increments,
    days = province_lab_bin_days, seeding = obs.seeding, n_patches = N_PATCHES,
    title = "Analysed specimen share by province, by week"
);

province_occupancy_ppc_fig = plot_province_split_ppc(
    chn_joint;
    share_key = :province_occupancy_share, rows = province_isolation,
    seeding = obs.seeding, n_patches = N_PATCHES,
    title = "Isolation occupancy share by province"
);

province_beds_ppc_fig = plot_province_split_ppc(
    chn_joint;
    share_key = :province_capacity_share, rows = province_capacity,
    seeding = obs.seeding, n_patches = N_PATCHES,
    title = "Isolation bed share by province"
);

Province stream calibration ​

Whether each province's share predictions are calibrated at the observed national total, scored as in the national stream calibration. The analysed-specimen rows score the weekly laboratory split the same way, at each week's observed national analysed total.

Build the province calibration panels
julia
province_case_panels = province_composition_panels(
    chn_joint;
    share_key = :province_shares,
    obs_increments = province_cases.increments,
    stream = "Confirmed cases", n_patches = N_PATCHES
);
province_death_panels = province_composition_panels(
    chn_joint;
    share_key = :province_death_shares,
    obs_increments = province_deaths.increments,
    stream = "Confirmed deaths", n_patches = N_PATCHES
);
province_lab_panels = province_composition_panels(
    chn_joint;
    share_key = :province_lab_shares,
    obs_increments = province_lab.increments,
    stream = "Analysed specimens", n_patches = N_PATCHES
);
province_panels = vcat(
    province_case_panels, province_death_panels, province_lab_panels
);
province_calibration_table = stream_calibration(province_panels);
province_calibration_fig = plot_stream_calibration(
    province_calibration_table
);

Province calibration table
StreamVintagesBias50% coverage90% coverage
Confirmed cases, Ituri99-0.040.620.95
Confirmed cases, Nord-Kivu990.020.570.95
Confirmed cases, Haut-Uele990.110.740.94
Confirmed cases, Other provinces990.030.90.98
Confirmed deaths, Ituri990.010.670.93
Confirmed deaths, Nord-Kivu99-0.030.630.94
Confirmed deaths, Haut-Uele990.030.790.97
Confirmed deaths, Other provinces99-0.010.910.98
Analysed specimens, Ituri160.220.881
Analysed specimens, Nord-Kivu16-0.30.751
Analysed specimens, Haut-Uele160.020.311
Analysed specimens, Other provinces160.10.811

Province counts ​

Whether each province's cases and deaths are reproduced as counts once the national total is predicted as well.

Build the province count panels
julia
# The joint posterior predictive, shared with the national page (see
# `joint_posterior_predictive` in `docs/pages/_setup.jl`). Its draws pair
# one to one with `chn_joint`, whose shares split them.
pp_joint = joint_posterior_predictive();
_pp_draws(vn) = collect(pp_joint[BVDOutbreakSize.FlexiChains.Prefixed(vn)]);

# The national confirmed cases are replicated per laboratory window, oldest
# first, as on the national page. The first confirmed vintage is an
# unreplicated baseline.
_conf_windows = BVDOutbreakSize.confirmed_positivity_windows(
    obs.confirmed_history, obs.lab_history, obs.lab_daily_history
);
_conf_obs = collect(
    first(
        pp_joint[k] for k in keys(pp_joint)
            if occursin(
                "confirmed_state.confirmed_positives.positives", string(k)
            )
    )
);
_conf_replicates = [
    vcat(collect(e), collect(p), collect(l))
        for (e, p, l) in zip(
            vec(_pp_draws(@varname(early_increments.increments))),
            vec(_conf_obs),
            vec(_pp_draws(@varname(late_increments.increments)))
        )
];
province_case_count_panels = province_count_panels(
    chn_joint;
    share_key = :province_shares,
    obs_increments = province_cases.increments,
    province_days = province_cases.days,
    national_days = vcat(
        _conf_windows.early_days, _conf_windows.obs_days,
        _conf_windows.late_days
    ),
    national_replicates = _conf_replicates,
    baseline = Int(first(obs.confirmed_history.counts)),
    stream = "Confirmed cases", n_patches = N_PATCHES
);
# The national confirmed deaths are replicated from zero at every vintage.
province_death_count_panels = province_count_panels(
    chn_joint;
    share_key = :province_death_shares,
    obs_increments = province_deaths.increments,
    province_days = province_deaths.days,
    national_days = obs.confirmed_deaths_history.days,
    national_replicates = _pp_draws(@varname(cdeath_increments.increments)),
    stream = "Confirmed deaths", n_patches = N_PATCHES
);
province_count_calibration_table = stream_calibration(
    vcat(province_case_count_panels, province_death_count_panels)
);
province_count_calibration_fig = plot_stream_calibration(
    province_count_calibration_table
);
Province count calibration plot

Province count calibration table
StreamVintagesBias50% coverage90% coverage
Confirmed cases, Ituri990.060.70.96
Confirmed cases, Nord-Kivu990.070.770.98
Confirmed cases, Haut-Uele990.130.780.95
Confirmed cases, Other provinces990.050.870.98
Confirmed deaths, Ituri99-0.060.660.92
Confirmed deaths, Nord-Kivu99-0.080.630.97
Confirmed deaths, Haut-Uele99-0.010.740.97
Confirmed deaths, Other provinces99-0.010.920.98

Province correlations and totals ​

Which province quantities trade off against each other, and whether each province's case and death totals agree with the observed ones.

Province posterior correlation heatmap
julia
# LaTeX label for one province's entry of a quantity. Spaces in a province
# name need escaping inside `\mathrm`.
_province_tex(sym, p) = string(
    sym, raw"\ (\mathrm{", replace(PROVINCE_LABELS[p], " " => raw"\ "), "})"
)
province_correlation_draws = NamedTuple(
    vcat(
        [:C_T => vec(Array(chn_joint[:C_T]))],
        _patch_columns(chn_joint, :R_T_patch, "R_T"),
        _patch_columns(chn_joint, :province_ascertainment, "asc"),
        _patch_columns(chn_joint, :CFR_patch, "CFR")
    )
);
province_correlation_labels = merge(
    Dict(:C_T => raw"C_T"),
    Dict(
        Symbol(name, "_", p) => _province_tex(tex, p)
            for p in 1:N_PATCHES
            for (name, tex) in (
                ("R_T", raw"R_T"), ("asc", raw"p_\mathrm{asc}"),
                ("CFR", raw"\mathrm{CFR}"),
            )
    )
);
province_correlation_fig = plot_correlation_heatmap(
    province_correlation_draws; labels = province_correlation_labels
);

Province totals against observed
julia
# Keyed by panel title, which names the stream and the province. A total
# that is the same in every draw (a patch with no deaths at any vintage) has
# no spread to plot and gives the corner plot a zero-width axis, so it is
# left out.
_varying_panels = filter(
    p -> length(unique(sum(r) for r in p.replicates)) > 1, province_panels
);
province_totals = NamedTuple(
    Symbol(p.title) => [sum(Float64.(r)) for r in p.replicates]
        for p in _varying_panels
);
province_observed = NamedTuple(
    Symbol(p.title) => Float64(sum(p.observed)) for p in _varying_panels
);
province_pairs_fig = plot_stream_pairs(province_totals, province_observed);

Province sensitivity to assumptions ​

Spatial structure sensitivity ​

The headline runs the model over four patches, one each for Ituri, Nord-Kivu and Haut-Uele and a fourth pooling the other affected provinces. Reducing it to a single patch collapses it onto one well-mixed population, which is the model the earlier releases used. The model overview on the methods page describes what the provinces add.

Splitting the country into provinces adds no national data, so the national outbreak size should not move far either way. The two are not identical by construction: the provinces run free and the country grows at the force-weighted mean of their reproduction numbers, which sits above the central trend they pool toward. That gap is first order in the deviation scale. A large gap between the two posteriors below would therefore point at the deviation priors rather than at the data.

julia
spatial_sensitivity_table = streams_table(
    "Meta-population (headline)" => posterior_C_joint,
    "Single population (n_patches = 1)" => posterior_C_no_patches
);
spatial_sensitivity_table
2×7 DataFrame
RowStreamLower 90%Lower 60%Lower 30%Upper 30%Upper 60%Upper 90%
StringFloat64Float64Float64Float64Float64Float64
1Meta-population (headline)12196.014801.016297.019667.021712.025514.0
2Single population (n_patches = 1)12404.014490.016064.019173.021460.026598.0
Spatial-structure density overlay
julia
spatial_sensitivity_fig = plot_density_overlay(
    "Meta-population (headline)" => posterior_C_joint,
    "Single population" => posterior_C_no_patches;
    xlabel = "Cumulative infections"
);

The size is the gate, but it is not the only quantity the spatial structure could move. The three figures below set the national reproduction number, the case-fatality ratio and the reproduction number at the cut-off from the two fits against each other. Each is a national quantity that both models estimate, so the two posteriors should sit on top of each other. In the trajectory figure they do, closely enough that the grey reference is hidden behind the coloured band for most of the window. The table after them gives the same quantities as numbers, which is where the agreement is read rather than eyeballed.

National quantities under both structures
julia
spatial_rt_fig = plot_rt_streams(
    [
        (;
            label = "Single population (n_patches = 1)",
            chn = chn_no_patches, rt_start = _rt_start_plot,
            rt_walk_start = clamp(
                _BREAKPOINT - RT_WALK_LEAD, _rt_start_plot,
                obs.n
            ), colour = :steelblue,
        ),
    ];
    joint = (;
        chn = chn_joint, rt_start = _rt_start_plot,
        rt_walk_start = clamp(
            _BREAKPOINT - RT_WALK_LEAD, _rt_start_plot,
            obs.n
        ),
    ),
    n = obs.n, breakpoint = _BREAKPOINT,
    as_of_date = string(obs.cutoff), seeding = obs.seeding,
    display_start = _rt_start_plot, ncols = 1,
    title = "National reproduction number under both structures",
    reference_label = "the meta-population headline",
    panel_label = "the single-population fit"
);

spatial_cfr_fig = plot_density_overlay(
    "Meta-population (headline)" => vec(Array(chn_joint[:CFR])),
    "Single population" => vec(Array(chn_no_patches[:CFR]));
    xlabel = "Case-fatality ratio"
);

spatial_rt_density_fig = plot_density_overlay(
    "Meta-population (headline)" => vec(Array(chn_joint[:R_T])),
    "Single population" => vec(Array(chn_no_patches[:R_T]));
    xlabel = "Reproduction number at the cut-off"
);

The table gathers the same three quantities as credible intervals, alongside the outbreak start date the two fits imply.

National quantities under both structures, as a table
julia
# One row per national quantity, one column per structure, each cell a
# median with a 90% credible interval. Built here rather than by stacking
# two `summary_table` calls, so the two structures sit side by side and the
# reader compares along a row.
spatial_quantities_table = let
    # A count rounded to zero decimals still prints a trailing ".0", so
    # whole-number quantities go through `Int`.
    fmt(x, d) = d <= 0 ? string(round(Int, x)) : string(round(x; digits = d))
    cell(v, d) = string(
        fmt(quantile(v, 0.5), d), " (",
        fmt(quantile(v, 0.05), d), "–", fmt(quantile(v, 0.95), d), ")"
    )
    rows = [
        ("Cumulative infections", :C_T, 0),
        ("Reproduction number at the cut-off", :R_T, 2),
        ("Case-fatality ratio", :CFR, 2),
        ("Outbreak age (days)", :T, 0),
        ("Latest growth rate (per day)", :r, 3),
    ]
    DataFrame(
        "Quantity" => [r[1] for r in rows],
        "Meta-population (headline)" => [
            cell(
                vec(Array(chn_joint[r[2]])), r[3]
            )
                for r in rows
        ],
        "Single population" => [
            cell(vec(Array(chn_no_patches[r[2]])), r[3])
                for r in rows
        ]
    )
end;
5×3 DataFrame
RowQuantityMeta-population (headline)Single population
StringStringString
1Cumulative infections17916 (12196–25514)17481 (12404–26598)
2Reproduction number at the cut-off0.8 (0.56–1.05)0.78 (0.55–1.02)
3Case-fatality ratio0.49 (0.37–0.6)0.5 (0.38–0.6)
4Outbreak age (days)197 (186–213)196 (186–213)
5Latest growth rate (per day)-0.016 (-0.04–0.004)-0.017 (-0.038–0.001)

Province parameter recovery ​

Whether the model recovers each province's values when fitted to data it simulated itself, from the same runs as the national parameter recovery. The top panel shows each seed's posterior median with its 50% and 90% intervals divided by that seed's true value. Below, each quantity by province is on its own scale with the prior in grey, each seed's posterior in its colour and its true value as a dashed line.

Recovery figure
julia
province_recovery_results = recovery_results()
_recovery_short = [
    "C_T_patch" => "C_T", "R_T_patch" => "R_T", "CFR_patch" => "CFR",
    "province_ascertainment" => "ascertainment",
    "region_drift_sd" => "drift SD",
]
province_recovery_quantities = [
    "$(k)[$(p)]" for (k, _) in _recovery_short for p in PROVINCE_LABELS
]
province_recovery_fig = isempty(province_recovery_results.params) ? nothing :
    plot_recovery(
        province_recovery_results.params, province_recovery_results.draws,
        province_recovery_results.prior;
        quantities = province_recovery_quantities,
        labels = Dict(
            "$(k)[$(p)]" => "$(v), $(p)" for (k, v) in _recovery_short
            for p in PROVINCE_LABELS
        ),
        panel_labels = Dict(
            "$(k)[$(p)]" => p for (k, _) in _recovery_short for p in PROVINCE_LABELS
        ),
        row_labels = last.(_recovery_short),
        log_x = ["C_T_patch[$(p)]" for p in PROVINCE_LABELS]
    );

The error of each seed's posterior median relative to the truth, and the z-score of the truth, summarised across seeds.

Summary table across seeds
julia
province_recovery_summary = isempty(province_recovery_results.params) ?
    DataFrame() :
    recovery_summary_table(province_recovery_results.params; province = true);

province_recovery_summary_display = isempty(province_recovery_summary) ?
    Markdown.parse("No parameter-recovery run is available for this build.") :
    MarkdownTable(province_recovery_summary);
QuantityTruth, lowestTruth, highestRelative error, medianRelative error, rangez, medianTruth in 90%Outside 99%
CFR_patch[Haut-Uele]0.1730.2550.07-0.18 to 0.230.273/30
CFR_patch[Ituri]0.1530.288-0.06-0.14 to 0.17-0.283/30
CFR_patch[Nord-Kivu]0.1580.270-0.16 to 0.17-0.13/30
CFR_patch[Other provinces]0.1460.2420.12-0.16 to 0.240.523/30
C_T_patch[Haut-Uele]0.45846.40.12-0.04 to 0.140.433/30
C_T_patch[Ituri]3280122000.08-0.34 to 0.230.492/30
C_T_patch[Nord-Kivu]0.907117-0.02-0.11 to 0.020.093/30
C_T_patch[Other provinces]0.848119-0.04-0.1 to 0.050.253/30
R_T_patch[Haut-Uele]0.6961.380.03-0.2 to 0.270.343/30
R_T_patch[Ituri]0.7651.420.02-0.2 to 0.10.292/30
R_T_patch[Nord-Kivu]0.7681.430.03-0.19 to 0.060.33/30
R_T_patch[Other provinces]0.8131.31-0.01-0.15 to 0.040.033/30
province_ascertainment[Haut-Uele]0.8441.05-0.02-0.2 to -0.02-0.033/30
province_ascertainment[Ituri]0.9441.110.09-0.1 to 0.240.473/30
province_ascertainment[Nord-Kivu]0.8551.04-0.02-0.1 to -0.00.063/30
province_ascertainment[Other provinces]0.9851.250-0.03 to 0.350.173/30
region_drift_sd[Haut-Uele]0.008190.09670.36-0.44 to 0.470.513/30
region_drift_sd[Ituri]0.004140.06190.490.19 to 1.560.593/30
region_drift_sd[Nord-Kivu]0.004320.04290-0.1 to 1.470.263/30
region_drift_sd[Other provinces]0.009880.02360.160.12 to 1.70.383/30
Each seed's recovered province values
julia
province_recovery = let r = province_recovery_results.params
    isempty(r) ? DataFrame() :
        r[
            occursin.("[", r.quantity), [
                :seed, :quantity, :truth, :median, :lower_90, :upper_90,
                :covered_90,
            ],
        ]
end;

province_recovery_display = isempty(province_recovery) ?
    Markdown.parse("No parameter-recovery run is available for this build.") : MarkdownTable(province_recovery);
seedquantitytruthmedianlower_90upper_90covered_90
1CFR_patch[Haut-Uele]0.2550.2720.1880.352true
1CFR_patch[Ituri]0.2880.2690.1740.369true
1CFR_patch[Nord-Kivu]0.270.2710.1650.348true
1CFR_patch[Other provinces]0.2420.2710.180.35true
1C_T_patch[Haut-Uele]0.4580.4380.08431.634true
1C_T_patch[Ituri]3283.1563538.2782600.945462.002true
1C_T_patch[Nord-Kivu]0.9070.930.1993.237true
1C_T_patch[Other provinces]0.8480.8110.172.86true
1R_T_patch[Haut-Uele]0.6960.8810.5321.411true
1R_T_patch[Ituri]0.7650.8430.5781.098true
1R_T_patch[Nord-Kivu]0.7680.8150.4921.238true
1R_T_patch[Other provinces]0.8130.8420.4511.216true
1province_ascertainment[Haut-Uele]1.0471.0280.771.743true
1province_ascertainment[Ituri]0.9441.0290.7952.095true
1province_ascertainment[Nord-Kivu]1.0271.0020.6981.504true
1province_ascertainment[Other provinces]0.9850.9530.3451.184true
1region_drift_sd[Haut-Uele]0.09670.05440.01250.119true
1region_drift_sd[Ituri]0.06190.07350.01410.146true
1region_drift_sd[Nord-Kivu]0.04290.04290.01460.115true
1region_drift_sd[Other provinces]0.01990.05370.01480.136true
2CFR_patch[Haut-Uele]0.1730.2130.1480.318true
2CFR_patch[Ituri]0.1530.1790.1220.242true
2CFR_patch[Nord-Kivu]0.1580.1840.130.249true
2CFR_patch[Other provinces]0.1460.1810.1270.241true
2C_T_patch[Haut-Uele]46.4452.14631.53789.111true
2C_T_patch[Ituri]7207.6714792.5224085.5866359.346false
2C_T_patch[Nord-Kivu]116.645113.92477.485175.711true
2C_T_patch[Other provinces]119.014107.10673.163162.585true
2R_T_patch[Haut-Uele]1.2481.291.0751.595true
2R_T_patch[Ituri]1.2541.2831.0881.547true
2R_T_patch[Nord-Kivu]1.2511.2831.0681.574true
2R_T_patch[Other provinces]1.281.2731.0551.56true
2province_ascertainment[Haut-Uele]0.880.7040.4570.99true
2province_ascertainment[Ituri]1.0481.2990.9811.812true
2province_ascertainment[Nord-Kivu]1.0451.0410.8611.299true
2province_ascertainment[Other provinces]1.0371.040.8651.322true
2region_drift_sd[Haut-Uele]0.008190.0120.0009930.045true
2region_drift_sd[Ituri]0.004140.01060.0009860.0389true
2region_drift_sd[Nord-Kivu]0.004320.01070.000960.0416true
2region_drift_sd[Other provinces]0.009880.01110.0008940.0414true
3CFR_patch[Haut-Uele]0.1920.1570.09650.24true
3CFR_patch[Ituri]0.1890.1630.09730.248true
3CFR_patch[Nord-Kivu]0.1970.1660.1030.252true
3CFR_patch[Other provinces]0.1880.1570.09750.24true
3C_T_patch[Haut-Uele]11.87813.5696.09729.445true
3C_T_patch[Ituri]12213.64915079.0689646.97625885.321true
3C_T_patch[Nord-Kivu]48.3442.91221.41187.432true
3C_T_patch[Other provinces]38.74640.53320.29480.54true
3R_T_patch[Haut-Uele]1.3831.110.7781.397true
3R_T_patch[Ituri]1.4161.140.9081.366false
3R_T_patch[Nord-Kivu]1.4331.1560.8791.461true
3R_T_patch[Other provinces]1.3131.120.8181.381true
3province_ascertainment[Haut-Uele]0.8440.830.4591.245true
3province_ascertainment[Ituri]1.1060.990.6661.509true
3province_ascertainment[Nord-Kivu]0.8550.770.4511.088true
3province_ascertainment[Other provinces]1.2531.6881.0192.804true
3region_drift_sd[Haut-Uele]0.02180.02960.002210.0948true
3region_drift_sd[Ituri]0.01920.02870.00230.081true
3region_drift_sd[Nord-Kivu]0.02880.02610.002470.0839true
3region_drift_sd[Other provinces]0.02360.02740.002450.0821true

Saving province in-sample outputs ​

Write the summary bullets
julia
# The bullets under the summary heading at the top of the page. They read
# tables built further down, so they are written here and read back when
# the site is assembled.
evaluation_insample_province_summary = let
    fmt(x) = ismissing(x) || !isfinite(x) ? "n/a" :
        string(round(x; digits = 2))
    fitted = filter(r -> isfinite(r["Bias"]), province_calibration_table)
    worst = first(
        sort(fitted, "Bias"; by = abs, rev = true), min(3, size(fitted, 1))
    )
    function coverage_bullet(heading, tbl, scale)
        low = first(sort(tbl, "90% coverage"), min(1, size(tbl, 1)))
        return string(
            "- **", heading, ":** ", count(>=(0.8), tbl[!, "90% coverage"]),
            " of ", size(tbl, 1), " province streams have 90% coverage of ",
            "at least 0.8", scale,
            size(low, 1) == 0 ? "." :
                string(
                    "; the lowest is ", low[1, "Stream"], " at ",
                    fmt(low[1, "90% coverage"]), "."
                )
        )
    end
    overall = [
        string(
            "- **Least well reproduced:** ",
            join(
                [
                    string(
                        r["Stream"], " (bias ", fmt(r["Bias"]),
                        ", 90% coverage ", fmt(r["90% coverage"]), ")"
                    )
                        for r in eachrow(worst)
                ], "; "
            ), "."
        ),
        coverage_bullet("Coverage", fitted, ""),
        coverage_bullet(
            "Count-scale coverage",
            filter(
                r -> isfinite(r["Bias"]), province_count_calibration_table
            ),
            ""
        ),
    ]
    rows(tbl) = Dict(r["Stream"] => r for r in eachrow(tbl))
    cal = rows(province_calibration_table)
    count_cal = rows(province_count_calibration_table)
    function calibration(tbl, kind, p, label)
        r = get(tbl, string(kind, ", ", PROVINCE_LABELS[p]), nothing)
        r === nothing && return string("- ", label, ": not scored.")
        return string(
            "- ", label, ": bias ", fmt(r["Bias"]), " and 90% coverage ",
            fmt(r["90% coverage"]), " over ", r["Vintages"], " vintages."
        )
    end
    detail = [
        join(
            [
                string("**", PROVINCE_LABELS[p], "**"), "",
                calibration(cal, "Confirmed cases", p, "Confirmed cases"),
                calibration(cal, "Confirmed deaths", p, "Confirmed deaths"),
                calibration(
                    count_cal, "Confirmed cases", p,
                    "Confirmed cases as counts"
                ),
                calibration(
                    count_cal, "Confirmed deaths", p,
                    "Confirmed deaths as counts"
                ),
            ], "\n"
        )
            for p in 1:N_PATCHES
    ]
    join(vcat([join(overall, "\n")], detail), "\n\n")
end
dashboard_dir = joinpath(
    pkgdir(BVDOutbreakSize), "docs", "src", "summary_assets"
)
mkpath(dashboard_dir)
write(
    joinpath(dashboard_dir, "evaluation_insample_province.md"),
    evaluation_insample_province_summary
);