Inside the Bundibugyo Outbreak Model
Parameters, Code, and Validation Against the CDC’s Published Figures
This is the technical companion to What Would It Take to Stop This Outbreak?. The main note is about reading an outbreak projection; this one is about building one. It documents the model, lists every parameter, shows the core of the code, and reports how the reimplementation reproduces the CDC’s published figures.
The model is a close reimplementation of the model in Mooring et al., MMWR 2026, built from the parameter box and supplementary figures in that report. It is not the authors’ original code, and it is built for teaching, not response planning. The CDC team’s report is the source for their actual projections and recommendations.
The model is a stochastic branching process run forward in continuous time. A single index case is seeded at an inferred spillover date. Each infected person, after a latent period equal to the incubation period, becomes infectious and draws a number of secondary infections from a negative-binomial offspring distribution; the timing of each potential transmission is a draw from a Weibull generation-interval distribution measured from symptom onset. A transmission only happens if it is scheduled before the infector recovers, dies, or is isolated. The realized reproduction number is not set directly but emerges from the offspring distribution together with the case fatality ratio, the recovery and mortality delays, and the isolation policy. The outbreak is the transmission tree that grows from repeating this rule.
Parameters
Every parameter is drawn once per simulated outbreak (constant within a simulation, varying across simulations), reproducing the prior box in the MMWR report. The values come from that box; the report cites the upstream literature each one draws on.
| Parameter | Prior | Role |
|---|---|---|
| Offspring mean | Log-normal, median 2.87, IQR 2.60–3.16 | Mean of the negative-binomial offspring distribution |
| Dispersion k | Uniform(0.02, 2.20) | Variability in secondary infections (superspreading) |
| Generation-interval offset | Uniform(5.7, 11.3) d | Latent period, taken equal to the incubation period |
| Generation-interval shape | 2 (fixed) | Weibull shape |
| Generation-interval scale | Uniform(5.40, 10.07) | Weibull scale (onset-to-transmission timing) |
| Recovery delay | Uniform(9.9, 10.0) d | Symptom onset to recovery |
| Mortality delay | Uniform(9.0, 11.4) d | Symptom onset to death |
| Case fatality ratio | Beta(13.2, 26.8), mean ≈ 0.33 | Probability an infected person dies |
| Isolation probability | 20 / 50 / 70 / 95% | Probability a case is detected once symptomatic |
| Isolation lag | Exponential, mean 2 d | Symptom onset to isolation |
| Isolation effectiveness | 100% | Reduction in infectiousness once isolated |
A note on the reproduction number: because transmissions scheduled after recovery, death, or isolation do not occur, the basic reproduction number is roughly 87% of the offspring mean at the central parameter values. This means an offspring mean of 2.87 corresponds to an R0 of about 2.5. That coupling is why the case fatality ratio and the infectious-period delays appear in R0 at all. The interactive note exposes R0 directly as its transmissibility control and converts to the offspring mean using this factor; the engine itself is parameterized by the offspring mean, as documented above.
The core of the code
The full engine is a single ES module, bundibugyo-engine.js. This is the same file the interactive note loads in your browser and the validation harness loads in Node. The core is the per-individual step: decide the person’s fate, apply isolation if the intervention is active, then schedule offspring that fall within the infectious window.
const onset = tInf + p.offset;
const dies = rng() < p.cfr;
let infectiousEnd = onset + (dies ? p.mortDelay : p.recDelay);
// Isolation: anyone still infectious during the intervention period can be detected once
// symptomatic. The detection clock starts at onset, or at the intervention day for the
// prevalent pool already symptomatic when the intervention begins.
if (infectiousEnd > interventionDay && rng() < isoProb) {
const isoTime = Math.max(onset, interventionDay) + expo(rng, isoLagMean);
if (isoTime < infectiousEnd) infectiousEnd = isoTime;
}
// Schedule offspring; a transmission only fires if it precedes the end of infectiousness.
const nOff = negbinom(rng, p.M, p.k);
for (let i = 0; i < nOff; i++) {
const childInf = onset + weibull(rng, giShape, p.giScale);
if (childInf <= infectiousEnd) { if (childInf <= horizon) heap.push(childInf); }
}Validation
The harness bundibugyo-validate.mjs loads the engine module and reproduces the three supplementary figures of the MMWR report. Running node bundibugyo-validate.mjs all uses 500 simulations per bar, matching the report. These are Monte-Carlo figures: at 500 simulations a single run wobbles by a couple of points (the report’s own bars carry the same sampling noise), so the model values below are averaged over several larger runs to give a steady estimate; the published targets are read from the supplementary figures and main text.
Effective reproduction number, before and after isolation (Supplementary Figure 1). The pre-intervention reproduction number holds near 2.4 across scenarios; isolation drives it down, crossing the critical value of one near 70% detection.
| Isolation | Model post-isolation R | MMWR figure |
|---|---|---|
| 20% | ~1.9 | ~2.1 |
| 50% | ~1.3 | ~1.4 |
| 70% | ~0.9 | ~0.9 |
| 95% | ~0.4 | ~0.3 |
Outbreak-size distribution, 100-death calibration (Supplementary Figure 2). Share of simulations in each size category by August 22, 2026. Model figures are shown against the published bars.
| Isolation | ≥20,000 cases (model / MMWR) | ≥4,000 deaths (model / MMWR) |
|---|---|---|
| 20% | 86% / ~79% | 89% / ~88% |
| 50% | 40% / ~33% | 48% / ~45% |
| 70% | 8% / ~6% | 12% / ~9% |
| 95% | 0% / ~0% | 0% / ~0% |
Independent calibration check. The model is not told when the outbreak began; it infers the spillover date from the death target. Those inferred dates match the report’s without being fit to them, evidence the calibration is doing the right thing.
| Death target | Inferred spillover (model) | MMWR inferred spillover |
|---|---|---|
| 50 | ~95 days before May 24 | ~94 days (Feb 19) |
| 100 | ~103 days | ~105 days (Feb 8) |
| 200 | ~108 days | ~115 days (Jan 29) |
The headline number reproduces: under the 50-death calibration with 20% isolation, the model puts about 66% of simulations at 20,000 or more cases (and lands on 65% at the report’s 500 simulations per bar, matching its stated 65%).
Where the reimplementation differs
The agreement is close but not perfect:
- Moderate-isolation bars run a few points high. At 50% isolation the model places slightly more mass in the largest size category than the published figure, and slightly less in the middle category. The model’s outbreaks are marginally more “fizzle-or-explode” than the original, a signature of how superspreading concentrates transmission. The effect is within about ten points and within the precision of reading a stacked bar chart.
- The pre-isolation reproduction number is the mean (≈2.3) where the figure likely plots the median (≈2.4). Both sit inside the figure’s interquartile error bars, and reflect a small difference in how the generation interval and infectious period interact.
- Comparison precision is limited by the source. The published bars are read by eye from stacked figures, so agreement closer than a few percentage points is not meaningful.
None of these changes the qualitative story. The reproduction number, the 65% headline, the location of the containment threshold, and the inferred spillover dates all reproduce. The model is close enough to reason with, and transparent enough to argue about.
Reproducing this yourself
The engine and the validation harness are plain JavaScript with no dependencies. Download both files into the same directory and run with Node:
# bundibugyo-engine.js and bundibugyo-validate.mjs in the same folder
node bundibugyo-validate.mjs all # DS3, DS4, DS5
node bundibugyo-validate.mjs compare # 100-death buckets vs the DS4 figure
node bundibugyo-validate.mjs diag # inferred spillover dates and accepted-draw propertiesThe two files are bundibugyo-engine.js and bundibugyo-validate.mjs; the interactive that drives the engine is in the main note.
Built and validated with Claude Opus 4.8.
References
- Mooring EQ, Koval WT, Routledge I, Holmdahl I, España G, Kahn R, Bruce BB. Modeled Scenario Projections for the Ebola Disease Outbreak Caused by Bundibugyo Virus, 2026. MMWR 2026;75(22). https://www.cdc.gov/mmwr/volumes/75/wr/mm7522e1.htm