2014 · Problem B — The Next Plague?
Compartmental ODE Epidemiology Resource allocation Risk communicationRead the official problem page →
The prompt, restated
A remote village of about 300 people has roughly half its inhabitants reporting similar symptoms — fever, fatigue, and a rash, plus a handful of severe cases. The team is asked to play the role of a public-health analyst dispatched to the scene: (1) Propose a disease model that captures the observed dynamics and classify the outbreak's severity (mild, moderate, severe, pandemic-prone). (2) Decide whether the outbreak is contained inside the village or is likely to spread to neighbouring villages. (3) With a fixed budget of medical staff, vaccines, anti-virals, and isolation beds, allocate resources to minimize total morbidity and mortality. (4) Respond to staged information drops — a second information packet reveals more cases in a nearby town, a third reveals genomic data suggesting a respiratory pathogen — and update both the model and the recommendation. (5) Write a one-page non-technical news brief the local health authority could release without causing panic.
The judges explicitly reward teams who keep their model in step with the information release: a paper that classifies the disease as "mild flu" on day 1 and never revisits the call after the day-3 genomic data shows up will be marked down heavily.
Key modeling idea
Treat the village as a closed SIR (or SEIR) compartmental system with a small population $N \approx 300$, then ask whether the basic reproduction number $R_0$ implied by the observed attack rate is above or below 1 under realistic intervention scenarios. Severity is a function of $R_0$, the case fatality rate (CFR), and the doubling time; containment is a network question (does the village mix with neighbours?). Resource allocation becomes an optimization over interventions — isolate, treat, vaccinate — each with its own cost, efficacy, and supply cap.
Suggested approach
- Step 1 — Fit a baseline SEIR. Use a four-compartment model $S \to E \to I \to R$ with $\beta$ (contact rate), $\sigma$ (1/incubation period), and $\gamma$ (1/infectious period). Calibrate $\beta$ from the observed attack rate (~150/300 in a few days suggests $R_0 \approx 3\text{–}5$ [illustrative]). See technique 6 for the ODE setup.
- Step 2 — Classify severity. Use a small decision table on $(R_0,\,\text{CFR},\,\text{doubling time})$: mild (<1.5, <0.1%, >14 d), moderate (1.5–2.5, 0.1–1%, 7–14 d), severe (2.5–5, 1–5%, 3–7 d), pandemic-prone (>5, >5%, <3 d). Anchor the bands to WHO pandemic-phase guidance.
- Step 3 — Test containment. Add a coupled second compartmental model for a neighbouring village, with a small inter-village mixing rate $\epsilon$ representing travel. Sweep $\epsilon$; a containment failure is when the second village's $I_2(t)$ peak exceeds a threshold within 30 days.
- Step 4 — Allocate resources by linear program. Variables: vaccine doses to villages, antivirals to symptomatic cases, isolation beds for severe cases. Objective: minimize total deaths $= \sum_i \text{CFR}_i \cdot \text{Infected}_i$. Constraints: supply caps, staff-hours, isolation-bed count. Solve with `scipy.optimize.linprog` (technique 11).
- Step 5 — Roll forward with each info drop. Maintain the model as a living artifact: on each new information release, refit $\beta$ and CFR with the latest data, recompute the resource allocation, and report what changed. Judges reward this discipline more than they reward a complex model.
Data sources to consider
| Source | What you get |
|---|---|
| WHO Disease Outbreak News archives | Real attack-rate / CFR templates for SARS, MERS, Ebola, COVID-19 |
| CDC EPI-X / MMWR (Morbidity & Mortality Weekly) | Calibrated incubation and infectious-period distributions |
| Anderson & May, Infectious Diseases of Humans (1991) | Canonical SEIR derivations and $R_0$ estimation methods |
| Ferguson et al., Imperial COVID-19 Report 9 | Worked example of intervention modelling and resource scaling |
| WHO & Gavi vaccine supply data | Realistic dose / cold-chain / staffing constraints for the LP |
Common pitfalls
- Picking SIR without justifying. A measles-like rash with the observed attack rate is closer to SEIR with a 7–14 day incubation. Defend the compartment choice against at least one alternative (SIRS, SEIRD).
- Treating $R_0$ as the disease. $R_0$ depends on the social network as much as on the pathogen. Distinguish the basic reproduction number from the effective $R_t$ under interventions.
- Allocating before optimizing. "Vaccinate everyone" is not a model. Write the LP, even if you solve it by hand for $n=3$ villages.
- Updating the model but not the recommendation. When the day-3 genomic drop indicates respiratory transmission, the allocation should shift toward masks and isolation, not just toward antivirals. Show the recommendation change.
- Panicking the news brief. The non-technical summary should communicate uncertainty without alarmism — a single chart of $I(t)$ with a 90% band, plus three plain bullets, is usually enough.
Python sketch
SEIR baseline with a sweep over a single intervention parameter (contact reduction from isolation), plus a tiny LP for vaccine allocation across two villages.
import numpy as np
from scipy.integrate import odeint
from scipy.optimize import linprog
# --- SEIR model -------------------------------------------------
def seir(y, t, beta, sigma, gamma, N):
S, E, I, R = y
dS = -beta * S * I / N
dE = beta * S * I / N - sigma * E
dI = sigma * E - gamma * I
dR = gamma * I
return [dS, dE, dI, dR]
N, I0, E0 = 300, 5, 0
beta0, sigma, gamma = 0.9, 1/5.2, 1/7.0 # ~ R0 = 6.3
t = np.linspace(0, 60, 600)
# baseline
sol = odeint(seir, [N - I0 - E0, E0, I0, 0], t,
args=(beta0, sigma, gamma, N))
peak_baseline = sol[:, 2].max()
# isolation: cut contacts by fraction phi
phi = 0.5
sol_iso = odeint(seir, [N - I0 - E0, E0, I0, 0], t,
args=(beta0 * (1 - phi), sigma, gamma, N))
peak_iso = sol_iso[:, 2].max()
print(f"peak I: baseline={peak_baseline:.0f}, isolation={peak_iso:.0f}") # [illustrative]
# --- Vaccine allocation LP --------------------------------------
# minimize -(efficacy_A * x_A + efficacy_B * x_B) (max deaths averted)
# s.t. x_A + x_B <= 200 doses, x_A <= 300, x_B <= 500
c = [-0.85, -0.70] # negate for max
A_ub = [[1, 1]]; b_ub = [200]
bounds = [(0, 300), (0, 500)]
res = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method="highs")
print(f"vaccines: village A = {res.x[0]:.0f}, village B = {res.x[1]:.0f}")
Sensitivity & validation checklist
- Vary $R_0$ from 1.5 to 6 — the severity-classification bucket should flip exactly where your decision table says it should.
- Sweep inter-village mixing $\epsilon$ from 0 to 0.05; identify the threshold above which containment fails.
- Vaccine efficacy: at 50% efficacy the LP should prefer the larger village; at 90% efficacy it should prefer the higher-incidence village.
- Compare your peak-infection curve to the analytic SIR peak $1 - (1 + \ln R_0)/R_0$ — should agree to within a few percent for the SIR limit $\sigma \to \infty$.
- Stress the staged-information rollout: feed the day-3 update and confirm the model refits and the recommendation changes accordingly.