Session 4: networks and contact structure#
Zoo models: Bass diffusion, age-structured SEIR
Goals#
Represent who-influences-whom as a network, and an influence step as one sparse product.
Compare a well-mixed model with the same model on a network.
Summarise a network by a contact matrix between groups and use it in a compartmental model.
Before the session: Networks.
Exercises#
4.1 Run the Bass model well mixed and on random networks of mean degree 4 and 50. When does each reach half adoption?
Solution
from modelly.zoo.bass_diffusion.model import BassDiffusion, BassScenario
for params in (
{"mixing": "well-mixed"},
{"mixing": "network", "degree": 4},
{"mixing": "network", "degree": 50},
):
model = BassDiffusion(BassScenario(rng=1, **params))
model.run()
frame = model.datacollector.get_model_vars_dataframe()
print(params, frame["time"][frame["adoption"] >= 0.5].iloc[0])
The dense network is indistinguishable from mixing; the sparse one is later.
4.2 Why does a sparse network slow imitation down, when every household has the same
p and q?
Solution
Adopters and non-adopters cluster: early adopters’ neighbours adopt by imitation, so the non-adopters left over tend to have non-adopting neighbours, and see a lower share of adopters than the population average. In the well-mixed model everybody sees the average.
4.3 Build a population of 600 people with households (all pairs linked) and workplaces
of 8 with MultilayerNetwork, give everybody an age, and compute its contact matrix for
the groups under 18, 18 to 64, and 65 and over.
Solution
import numpy as np
from modelly.space.networks import MultilayerNetwork, age_groups, contact_matrix
rng = np.random.default_rng(1)
n = 600
ages = rng.integers(0, 90, n)
network = MultilayerNetwork(n, {"household": 1.0, "work": 0.5})
for start in range(0, n, 3): # households of three
members = range(start, min(start + 3, n))
network.add_edges([(u, v) for u in members for v in members if u < v], "household")
workers = rng.permutation(np.flatnonzero((ages >= 18) & (ages < 65)))
for start in range(0, len(workers), 8):
team = workers[start : start + 8].tolist()
network.add_edges([(u, v) for u in team for v in team if u < v], "work")
groups = age_groups(ages, [18, 65])
print(contact_matrix(network, groups))
4.4 Use that matrix as the contacts of SEIRScenario (with matching group_sizes)
and report r0() and the final size by group.
Solution
from modelly.zoo.age_seir.model import AgeSEIR, SEIRScenario
matrix = contact_matrix(network, groups)
sizes = tuple(int(k) for k in np.bincount(groups, minlength=3))
model = AgeSEIR(
SEIRScenario(
rng=1,
group_sizes=sizes,
contacts=tuple(map(tuple, matrix)),
beta=0.1,
initial_infected=(1, 1, 1),
)
)
print(model.r0(), model.final_size())
Adults, who have both household and work contacts, have the largest final size.