← All writing
Note · Computational epidemiology

Solving the SIR model from scratch

Three coupled equations explain why an outbreak peaks and then burns out. Below they are derived, integrated and put on sliders.

Apr 20222 min readNumerical methodsSciPy

The model

Split a population into the susceptible SS, the infectious II and the recovered RR. Infections happen when susceptible and infectious people meet, at a rate β\beta per day, and the infectious recover at a rate γ\gamma. Working in shares of the population (so S+I+R=1S + I + R = 1) gives

dSdt=−βSIdIdt=βSI−γIdRdt=γI\begin{aligned} \frac{dS}{dt} &= -\beta S I \\[1.2ex] \frac{dI}{dt} &= \beta S I - \gamma I \\[1.2ex] \frac{dR}{dt} &= \gamma I \end{aligned}

The second equation already holds the main result. II grows only while βS>γ\beta S > \gamma, that is while R0S>1R_0 S > 1 with the basic reproduction number R0=β/γR_0 = \beta / \gamma. Every infection uses up susceptibles, so SS falls until the condition fails and the curve turns over.

Try it: an outbreak in 160 daysSIR
R₀ = β / γ–
Peak infected–
Peak day–
Ever infected–
Computed in your browser with RK4, step 0.25 days, starting from 0.1% infected.

Integrating it

There is no closed-form solution for I(t)I(t), so we integrate numerically. SciPy’s solve_ivp does it in a few lines. The figure above uses a hand-written fourth-order Runge–Kutta step instead, which is the same idea in about fifteen lines of JavaScript.

sir.py
import numpy as np
from scipy.integrate import solve_ivp
def sir(t, y, beta, gamma):
s, i, r = y # shares of the population
return [-beta * s * i, beta * s * i - gamma * i, gamma * i]
beta, gamma = 0.32, 0.10 # per day
y0 = [0.999, 0.001, 0.0]
t = np.linspace(0, 160, 641)
sol = solve_ivp(sir, (0, 160), y0, args=(beta, gamma), t_eval=t)
s, i, r = sol.y
print(f"R0 = {beta / gamma:.2f}, peak on day {t[i.argmax()]:.0f}")

How big does it get?

Divide the first equation by the third and integrate. With almost everyone susceptible at the start, the share that is still susceptible at the end, s∞s_\infty, solves

s∞=exp⁡ ⁣(−R0 (1−s∞))s_\infty = \exp\!\big(-R_0\,(1 - s_\infty)\big)

For R0=3.2R_0 = 3.2 that leaves about 5% untouched, so roughly 95% are infected at some point. Drag γ\gamma up until R0R_0 drops below 1 and the outbreak never takes off.