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.31, 90% coverage 1.0); Analysed specimens, Ituri (bias 0.23, 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.94 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.03 and 90% coverage 0.95 over 99 vintages.

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

  • Confirmed cases as counts: bias 0.07 and 90% coverage 0.99 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.98 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.96 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.610.94
Confirmed cases, Nord-Kivu990.030.560.95
Confirmed cases, Haut-Uele990.110.750.94
Confirmed cases, Other provinces990.030.90.98
Confirmed deaths, Ituri990.010.670.93
Confirmed deaths, Nord-Kivu99-0.030.620.93
Confirmed deaths, Haut-Uele990.030.80.98
Confirmed deaths, Other provinces99-0.010.910.96
Analysed specimens, Ituri160.230.881
Analysed specimens, Nord-Kivu16-0.310.751
Analysed specimens, Haut-Uele160.010.251
Analysed specimens, Other provinces160.080.751

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.760.99
Confirmed cases, Haut-Uele990.130.760.95
Confirmed cases, Other provinces990.050.870.98
Confirmed deaths, Ituri99-0.060.680.92
Confirmed deaths, Nord-Kivu99-0.080.610.97
Confirmed deaths, Haut-Uele99-0.010.760.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)12371.014885.016574.019815.021957.026103.0
2Single population (n_patches = 1)11973.014504.016190.019412.021419.025866.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 infections18127 (12371–26103)17719 (11973–25866)
2Reproduction number at the cut-off0.8 (0.55–1.06)0.78 (0.56–1.02)
3Case-fatality ratio0.48 (0.37–0.59)0.5 (0.39–0.62)
4Outbreak age (days)195 (184–211)197 (186–213)
5Latest growth rate (per day)-0.016 (-0.039–0.004)-0.017 (-0.037–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.2120.23-0.16 to 0.340.883/30
CFR_patch[Ituri]0.1530.276-0.01-0.12 to 0.15-0.023/30
CFR_patch[Nord-Kivu]0.1580.288-0.01-0.14 to 0.17-0.023/30
CFR_patch[Other provinces]0.1460.285-0.02-0.15 to 0.230.013/30
C_T_patch[Haut-Uele]0.45846.40.08-0.2 to 0.120.383/30
C_T_patch[Ituri]3280122000.03-0.34 to 0.20.272/30
C_T_patch[Nord-Kivu]0.907117-0.03-0.14 to 0.120.013/30
C_T_patch[Other provinces]0.8481190.02-0.11 to 0.140.253/30
R_T_patch[Haut-Uele]0.6961.380.03-0.19 to 0.180.333/30
R_T_patch[Ituri]0.7651.420.02-0.19 to 0.120.292/30
R_T_patch[Nord-Kivu]0.7681.430.02-0.19 to 0.10.293/30
R_T_patch[Other provinces]0.8131.31-0.01-0.15 to 0.040.053/30
province_ascertainment[Haut-Uele]0.8441.05-0.02-0.17 to 0.0-0.063/30
province_ascertainment[Ituri]0.9441.110.06-0.1 to 0.190.313/30
province_ascertainment[Nord-Kivu]0.8551.04-0.05-0.11 to -0.01-0.433/30
province_ascertainment[Other provinces]0.9851.250.01-0.01 to 0.340.13/30
region_drift_sd[Haut-Uele]0.008190.09670.35-0.67 to 0.40.492/30
region_drift_sd[Ituri]0.004140.06190.6-0.48 to 1.550.613/30
region_drift_sd[Nord-Kivu]0.004320.0429-0.07-0.29 to 1.330.173/30
region_drift_sd[Other provinces]0.009880.02360.180.07 to 0.690.43/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.2120.2830.2040.361true
1CFR_patch[Ituri]0.2760.2740.2040.359true
1CFR_patch[Nord-Kivu]0.2880.2840.2140.373true
1CFR_patch[Other provinces]0.2850.280.2040.383true
1C_T_patch[Haut-Uele]0.4580.3670.09271.294true
1C_T_patch[Ituri]3283.1563374.9942515.8634688.509true
1C_T_patch[Nord-Kivu]0.9071.0140.2722.635true
1C_T_patch[Other provinces]0.8480.970.2283.333true
1R_T_patch[Haut-Uele]0.6960.820.5761.029true
1R_T_patch[Ituri]0.7650.8550.6491.038true
1R_T_patch[Nord-Kivu]0.7680.8470.6091.057true
1R_T_patch[Other provinces]0.8130.8490.5871.075true
1province_ascertainment[Haut-Uele]1.0471.0480.9081.744true
1province_ascertainment[Ituri]0.9440.9990.7141.536true
1province_ascertainment[Nord-Kivu]1.0270.9740.5851.121true
1province_ascertainment[Other provinces]0.9850.990.6971.284true
1region_drift_sd[Haut-Uele]0.09670.03150.007480.0697false
1region_drift_sd[Ituri]0.06190.03240.00850.0661true
1region_drift_sd[Nord-Kivu]0.04290.03050.006520.0708true
1region_drift_sd[Other provinces]0.01990.03360.006410.0786true
2CFR_patch[Haut-Uele]0.1730.2130.1480.323true
2CFR_patch[Ituri]0.1530.1760.1190.239true
2CFR_patch[Nord-Kivu]0.1580.1840.130.247true
2CFR_patch[Other provinces]0.1460.180.1230.241true
2C_T_patch[Haut-Uele]46.4450.3530.06288.576true
2C_T_patch[Ituri]7207.6714788.464127.4826326.404false
2C_T_patch[Nord-Kivu]116.645112.78674.589172.218true
2C_T_patch[Other provinces]119.014105.34271.234166.288true
2R_T_patch[Haut-Uele]1.2481.2811.0741.583true
2R_T_patch[Ituri]1.2541.281.0871.549true
2R_T_patch[Nord-Kivu]1.2511.281.0741.572true
2R_T_patch[Other provinces]1.281.2711.0631.561true
2province_ascertainment[Haut-Uele]0.880.7310.471.004true
2province_ascertainment[Ituri]1.0481.250.9581.817true
2province_ascertainment[Nord-Kivu]1.0451.0350.8561.293true
2province_ascertainment[Other provinces]1.0371.0320.8411.316true
2region_drift_sd[Haut-Uele]0.008190.01110.0009970.0448true
2region_drift_sd[Ituri]0.004140.01060.0009070.0399true
2region_drift_sd[Nord-Kivu]0.004320.01010.0008880.0424true
2region_drift_sd[Other provinces]0.009880.01060.001040.0429true
3CFR_patch[Haut-Uele]0.1920.1620.09820.247true
3CFR_patch[Ituri]0.1890.1660.1020.257true
3CFR_patch[Nord-Kivu]0.1970.1690.1020.259true
3CFR_patch[Other provinces]0.1880.160.09720.245true
3C_T_patch[Haut-Uele]11.87813.3316.20328.892true
3C_T_patch[Ituri]12213.64914663.3269624.37725333.56true
3C_T_patch[Nord-Kivu]48.3441.52821.84487.393true
3C_T_patch[Other provinces]38.74639.40519.9683.386true
3R_T_patch[Haut-Uele]1.3831.1220.7951.418true
3R_T_patch[Ituri]1.4161.1470.9261.371false
3R_T_patch[Nord-Kivu]1.4331.160.8881.475true
3R_T_patch[Other provinces]1.3131.120.8131.379true
3province_ascertainment[Haut-Uele]0.8440.830.4511.216true
3province_ascertainment[Ituri]1.1060.9940.6651.531true
3province_ascertainment[Nord-Kivu]0.8550.7630.4481.095true
3province_ascertainment[Other provinces]1.2531.6751.0432.763true
3region_drift_sd[Haut-Uele]0.02180.03060.003570.09true
3region_drift_sd[Ituri]0.01920.03080.002920.0845true
3region_drift_sd[Nord-Kivu]0.02880.02670.002890.084true
3region_drift_sd[Other provinces]0.02360.02780.002680.0893true

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