Simulating Outbreaks to Measure Detection Power
This guide, part of Cluster Detection Power & Benchmarking, focuses on the part of a benchmark that decides whether its results mean anything: the synthetic outbreaks themselves.
Problem Context & Constraints
A power estimate is only as meaningful as the outbreaks it was computed against. The default in most methodological work is a circular cluster with a uniform excess — every area within a radius gets the same relative risk. That shape is convenient, it suits circular scan statistics perfectly, and it resembles few real outbreaks.
Real outbreaks come in recognisable forms. Point-source outbreaks — a contaminated food venue, a cooling tower — produce risk that decays with distance from the source, highest at the centre and fading outward rather than stopping at an edge. Corridor outbreaks follow linear features: a river for vector-borne disease, a highway for a pollutant, a commuter rail line for a respiratory pathogen. Diffuse outbreaks raise risk modestly across a broad region, such as an environmental exposure affecting a whole valley. Each is detected differently, and a benchmark built only on circles says nothing about the other two.
Three constraints shape realistic simulation. Shapes must resemble the hazards the program monitors. Excess risks must be modest, because the outbreaks that are hard to detect — and therefore the ones a benchmark exists to evaluate — have relative risks of 1.3 to 2, not 5. And outbreaks must be placed across the geography, since power at one location says little about power at another.
Prerequisites
python3.11numpy1.26.4,geopandas0.14.4,shapely2.0.4,pandas2.2.2- The analysis geography with population, in a projected metric coordinate reference system
- Linear features for corridor scenarios — rivers, highways, transit lines
- A list of outbreak scenarios with shapes, extents and excess risks, ideally informed by past investigations
Step-by-Step Solution
Build a relative risk vector for each shape, calibrate it to a target number of excess cases rather than only a relative risk, place it at many locations, and simulate counts.
# Realistic outbreak relative-risk surfaces for power benchmarking.
# Pinned: numpy==1.26.4, geopandas==0.14.4, shapely==2.0.4, pandas==2.2.2
import logging
import numpy as np
import pandas as pd
import geopandas as gpd
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("outbreaks")
SEED = 20260918
def point_source(areas: gpd.GeoDataFrame, source, peak_rr: float, decay_m: float) -> np.ndarray:
"""Excess risk decaying exponentially with distance from a source point."""
d = areas.geometry.centroid.distance(source).to_numpy()
return 1.0 + (peak_rr - 1.0) * np.exp(-d / decay_m)
def corridor(areas: gpd.GeoDataFrame, line, peak_rr: float, width_m: float) -> np.ndarray:
"""Excess risk along a linear feature, fading with distance from the line."""
d = areas.geometry.centroid.distance(line).to_numpy()
return 1.0 + (peak_rr - 1.0) * np.exp(-(d / width_m) ** 2)
def diffuse(areas: gpd.GeoDataFrame, region, rr: float) -> np.ndarray:
"""Uniform modest excess across a broad region."""
inside = areas.geometry.centroid.within(region).to_numpy()
return np.where(inside, rr, 1.0)
def expected_excess(pop: np.ndarray, rr: np.ndarray, total: int) -> float:
"""Excess cases implied by rr for a fixed case total."""
base = total * pop / pop.sum()
shifted = total * pop * rr / (pop * rr).sum()
return float((shifted - base)[rr > 1.0].sum())
def calibrate_to_excess(pop, rr_shape_fn, target_excess: float, total: int,
lo: float = 1.01, hi: float = 10.0) -> float:
"""Find the peak relative risk that yields a target number of excess cases."""
for _ in range(60):
mid = (lo + hi) / 2
if expected_excess(pop, rr_shape_fn(mid), total) < target_excess:
lo = mid
else:
hi = mid
return (lo + hi) / 2
def placements(areas: gpd.GeoDataFrame, n: int, stratify_col: str = "urbanicity",
seed: int = SEED) -> pd.DataFrame:
"""Sample outbreak centres stratified by urbanicity so rural power is measured too."""
rng = np.random.default_rng(seed)
per = max(1, n // areas[stratify_col].nunique())
picks = (areas.groupby(stratify_col, group_keys=False)
.apply(lambda g: g.sample(min(per, len(g)), random_state=rng.integers(1e9))))
log.info("placed %d outbreak centres across %s strata", len(picks),
areas[stratify_col].nunique())
return picks[["geoid", stratify_col, "geometry"]]
Calibrating to a target number of excess cases, rather than only a relative risk, is what makes scenarios comparable across locations. A 1.5-fold point-source excess centred in a dense suburb produces many more extra cases than the same excess centred in a farming county, and power follows excess cases far more closely than relative risk. Fixing the excess at, say, fifteen extra cases and solving for the peak relative risk at each location asks a sharper question: can the method find fifteen extra cases here?
Stratifying placements by urbanicity ensures the rural areas — where power is usually lowest and where a program is most likely to be blind — are represented rather than swamped by the many urban candidates.
Validation & Edge Cases
Decay parameters without a basis. A point-source decay of 2 km versus 10 km changes the outbreak’s extent fivefold. Base decay on past investigations — how far from a venue cases lived — or run a range and report power across it.
Corridors that extend beyond the study area. A river corridor running out of the state produces a truncated outbreak whose detectability depends on where it is cut. Clip corridors to the study area and report the fraction of the planted excess retained.
Outbreaks that overlap existing true clusters. If the real data contain a genuine excess, a planted outbreak on top of it is detected for the wrong reason. Simulate on null baselines generated from population, not on the observed cases.
Calibration failures. In very sparse areas no plausible relative risk yields the target excess. Cap the relative risk at a realistic maximum and record the placements that could not reach the target — they are the program’s hard blind spots.
Temporal shape ignored. A spatial benchmark plants an excess in a single period, but real outbreaks build over days or weeks. If the program runs a prospective space-time method, give each scenario an onset curve as well — a sharp rise for a point source, a slow ramp for an environmental exposure — and measure both whether the outbreak is found and how many periods pass first. A method that finds every outbreak three weeks late has high power and little public health value.
Case totals that drift with the plant. When excess cases are added on top of a fixed baseline, the simulated total rises with the outbreak, and methods that condition on the total — most scan statistics do — respond to that shift as well as to the location. Decide which regime matches the program’s data: a conditional design redistributes a fixed total, while an additive design adds cases. Report which one was used, because the two give noticeably different power at small excesses.
Too few placements per stratum. A rural stratum with five placements gives an unstable power estimate. Allocate placements so each stratum has enough for a meaningful estimate, even if urban placements are then fewer than their share.
INFO outbreaks placed 120 outbreak centres across 4 strata
INFO benchmark calibrated point-source excess=15: peak RR ranges 1.08 (urban) to 3.90 (rural, capped at 3.0 for 11 centres)
Compliance Notes
Record every scenario’s shape, parameters, target excess and calibrated peak relative risk, the placement strategy and seed, the placements that hit the relative-risk cap, and the linear features used for corridors with their source.
Simulated outbreaks are synthetic and contain no protected data, so the scenario library can and should be shared with partner agencies running comparable programs. A common scenario library makes power estimates comparable across jurisdictions, which is otherwise impossible because each agency’s benchmark rests on its own idiosyncratic choices.
Related Topics
- Cluster Detection Power & Benchmarking — the parent guide and the benchmark harness.
- Building Synthetic Case Data for Pipeline Tests — the same planted-signal idea used for software testing.
- Running a Prospective Space-Time Scan for Early Warning — a method these scenarios are commonly used to evaluate.