Network Epidemiology · Agent-Based Modelling
📅 July 2026 ⏱ ≈ 12 min read 🎯 Intermediate

Modelling COVID-19 on Contact Graphs — Building and Simulating Realistic Networks

The homogeneous-mixing assumption behind the classic SIR (Susceptible-Infected-Recovered) equations — everyone equally likely to meet everyone else — is a poor description of how SARS-CoV-2 actually moved through a population: real transmission ran along households, workplaces, schools, and a long tail of occasional contacts, with wildly uneven numbers of contacts per person. This article is about the engineering of the graph itself — how to build a realistic multi-layer contact network and simulate disease spread on it — rather than the general theory of why network topology matters, which our companion article on network epidemiology covers in depth.

1. Why COVID-19 Needed a Graph, Not a Compartment

The classic SIR/SEIR compartmental model assumes every susceptible person has an equal, small chance of contacting every infectious person — mathematically convenient, but wrong in a specific way that mattered enormously for COVID-19: real human contact patterns are extremely heterogeneous. Most people have a handful of close, repeated contacts (household, close colleagues); a few have dozens or hundreds (retail workers, teachers, transit workers). A contact graph — nodes are individuals, edges are contacts capable of transmission — represents this heterogeneity explicitly, rather than averaging it away.

This distinction was not academic. Genomic and contact-tracing studies of SARS-CoV-2 repeatedly found that a small fraction of cases produced a large fraction of subsequent infections (superspreading events in choirs, meatpacking plants, weddings), a pattern that a homogeneous-mixing model structurally cannot reproduce but a graph model reproduces naturally once the degree distribution is realistic (§4).

2. Building a Multi-Layer Contact Graph

Realistic synthetic contact networks (as used by models like Imperial College's CovidSim or the Institute for Disease Modeling's Covasim) are built as a union of several distinct layers, each with its own contact rate and transmission risk:

🏠 Household

Small complete sub-graphs (2-6 nodes), highest transmission probability per contact, always active.

🏢 Workplace

Clustered groups of 5-50, moderate-high transmission risk, active on workdays only.

🏫 School

Class-based clusters plus a school-wide random layer, age-dependent susceptibility.

🎲 Community

Sparse random or small-world edges (shops, transit), lower per-contact risk but very high node count.

A key parameter for each layer is the degree distribution — how many contacts each node has within that layer. Households are naturally small and roughly uniform; workplace and community layers are usually drawn from a heavy-tailed distribution (e.g. negative binomial or power-law) to reproduce the observed extreme variance in real contact counts.

G = G_household ∪ G_workplace ∪ G_school ∪ G_community

Each edge (i, j) in layer L carries: β_L // per-contact, per-day transmission probability, layer-specific

3. Graph-Based SEIR: States and Transition Rules

Each node carries a disease state — Susceptible, Exposed (infected but not yet infectious), Infectious (further split into asymptomatic/mild/severe for COVID-19), Recovered, and optionally Dead. Instead of a rate-equation compartment, transitions happen per edge, per day:

for each infectious node i, for each neighbour j in state S across all layers:
  P(j becomes E on this day) = 1 − (1 − β_L)^(contacts on layer L)

E → I after latent period ~ LogNormal(μ=1.6, σ=0.5) days
I → R (or Dead) after infectious period, duration and outcome drawn per node // individual heterogeneity in severity/duration, not just a population average

Running this stochastically, node by node, day by day, naturally reproduces branching, clustering within households, and stalling out in sparsely-connected parts of the graph — none of which a mean-field ODE can represent, since it has no concept of "this particular household" or "this particular workplace".

4. Superspreading and Overdispersion (k)

Empirically, the number of secondary infections caused by an individual case of COVID-19 is well described by a negative binomial distribution with dispersion parameter k ≈ 0.1-0.5 — far more overdispersed than the Poisson distribution (k → ∞) implied by homogeneous mixing. A small k means most infected people transmit to few or no one, while a small fraction transmit to dozens.

Reading the dispersion parameter k:
k → ∞ (Poisson): everyone equally likely to transmit — homogeneous-mixing regime
k ≈ 1: moderate variability
k ≈ 0.1-0.2 (estimated for SARS-CoV-2): strong overdispersion — the "80/20 rule" where roughly 10-20% of cases cause ~80% of transmissions

On a contact graph, this overdispersion emerges naturally, without any extra parameter, purely from the combination of (a) a heavy-tailed degree distribution (§2) and (b) variation in individual infectiousness/viral load — both of which are explicit, measurable graph and node properties rather than a fitted statistical add-on.

5. Simulating Interventions on the Graph

The graph representation makes it straightforward to simulate real non-pharmaceutical interventions as concrete graph operations, rather than abstract parameter changes:

Because each intervention is a well-defined graph operation, this approach lets you directly compare, e.g., "close schools" vs. "trace and isolate contacts of confirmed cases" on the exact same underlying population structure — a comparison that is difficult to make meaningfully in a compartmental model.

6. JavaScript: Building and Running the Simulation

function buildContactGraph(n = 2000) {
  const nodes = Array.from({ length: n }, (_, i) => ({ id: i, state: 'S', edges: [] }));
  const addEdge = (i, j, layer, beta) => {
    nodes[i].edges.push({ to: j, layer, beta });
    nodes[j].edges.push({ to: i, layer, beta });
  };

  // Household layer: cluster nodes into groups of 2-5
  let idx = 0;
  while (idx < n) {
    const size = 2 + Math.floor(Math.random() * 4);
    const members = [];
    for (let k = 0; k < size && idx < n; k++) members.push(idx++);
    for (let a = 0; a < members.length; a++)
      for (let b = a + 1; b < members.length; b++)
        addEdge(members[a], members[b], 'household', 0.25);
  }

  // Community layer: heavy-tailed random edges (configuration-model style)
  const extraEdges = Math.floor(n * 2.5);
  for (let e = 0; e < extraEdges; e++) {
    const i = Math.floor(Math.random() * n), j = Math.floor(Math.random() * n);
    if (i !== j) addEdge(i, j, 'community', 0.04);
  }
  return nodes;
}

function stepDay(nodes) {
  const newlyExposed = [];
  for (const node of nodes) {
    if (node.state !== 'I') continue;
    for (const { to, beta } of node.edges) {
      if (nodes[to].state === 'S' && Math.random() < beta) newlyExposed.push(to);
    }
  }
  for (const id of newlyExposed) nodes[id].state = 'E';
  // (E→I and I→R transitions with sampled durations omitted for brevity)
  return nodes;
}

// Seed one infectious node in a high-degree hub and run for 120 days
const graph = buildContactGraph(2000);
graph[0].state = 'I';
for (let day = 0; day < 120; day++) stepDay(graph);

Running many stochastic replicates of this same graph reproduces the overdispersed secondary-case distribution of §4 directly from the edge structure — no dispersion parameter k needs to be injected by hand. For the underlying theory of R₀, herd immunity thresholds, and how network topology governs outbreak size in general, see Epidemic Spreading on Networks.