Building a compartmented model
So far we’ve used one of epydemic
’s built-in disease
processes. However, there are a lot of possible models that we might
want to build from scratch, so we have to understand the mechanisms
epydemic
provides. To make this explanation simple, we’ll dig into
how a slightly simplified version of epydemic
’s SIR process is
built from scratch.
For anyone who isn’t acquainted with SIR: the Susceptible-Infected-Removed (SIR) model represents an epidemic as a process where nodes in a network represent individuals and edges represent interactions between them. Each individual is labelled with one of three states:
S: the individual is susceptible to the disease;
I: the individual is infected with the disease, and can infect any susceptible neighbour; and
R: the individual has recovered (or died) and takes no further part in the simulation.
This structure is called a compartmented model of disease: the states of individuals are referred to conventionally as compartments. Questions about the epidemic are often phrased in terms of the asymptotic behaviour of the process: in the limit, when the process reaches equilibrium, what proportion of nodes are in which compartment? For an epidemic to be an epidemic, this usually means getting a large number of nodes in the R compartment, meaning they’ve been infected and have been removed.
A simulation of SIR consists of:
taking a network;
seeding it with infected individuals;
setting the probability with which infected nodes infect neighbouring susceptibles;
setting the probability with which infected noides recover spontaneously; and
running the simulation until equilibrium, for example when there are no infected nodes left or a long time has passed.
A moment’s thought will show that there are lots of possible
variations to this basic process, for example where the infection
probability changes with time, or the network is re-wired as the
epidemic progresses. For this reason it’s worth understanding SIR ab
initio. What follows is a walk-through of the code in epydemic
’s
standard SIR
model.
Getting started
Let’s start defining a class to represent the model. This will be a
sub-class of CompartmentedModel
, which is itself a sub-class
of Process
, the class of network processes.
from epydemic import CompartmentedModel
We then need to populate this class with the elements needed to define a compartmented model.
Note
The rest of the code on this page is part of this SIR class, and
so needs to be indented within the scope of the class
definition in proper Python style. We’ve broken it out so we can
insert explanations as we go along.
class SIR(epydemic.CompartmentedModel):
The compartments
The SIR model has three compartments, so the first part of the model is to define these. We could simply name them using their initial letters, but it’s better practice to define constants in the class to represent them.
# the possible dynamics states of a node for SIR dynamics
SUSCEPTIBLE = 'S'
INFECTED = 'I'
REMOVED = 'R'
The transition probabilities
Next we need to be able to specify the transition probabilities that form the dynamics. There are two such parameters: the probability of the infection passing across a link between a susceptible and an infected node, the the probability of an infected node being removed.
We also need to specify the probability of a node being initially
infected. Essentially this is the probability that a node starts out
in the SIR.INFECTED
compartment rather than in the
SIR.SUSCEPTIBLE
compartment, and will be used to seed the network
randomly at the start of the simulation.
# the model parameters
P_INFECTED = 'pInfected'
P_INFECT = 'pInfect'
P_REMOVE = 'pRemove'
Note that these aren’t the parameters themselves, they’re the name of parameters that will be supplied when the model is run.
Loci
Now we come to a slightly more complicated topic. Where does the process occur? The description above suggests that changes happen in two places:
on the edges beteeen susceptible and infected noides (where the infection may pass from the latter to the former); and
on the infected nodes themselves (which may become removed).
In the latter case, the process occurs to nodes in a specific
compartment; in the former, it occurs on edges between nodes in
specific compartments. We need to keep track of where these changes
can happen, which is the function of loci: we’ll return to these in a
minute. For the time being, note that we conventionally refer to the
edges where the infection process can occur as SI edges – edges
between a node in the S compartment and a node in the I compartment –
and so we’ll define two loci: one can just be called SIR.INFECTED
,
but the other needs to be given a name.
# the edges at which dynamics can occur
SI = 'SI'
Building the model
We can now build the model ready for simulation. There are four elements to this:
Collect the parameters we need, which are passed to us in a dict.
Create the compartments.
Create the loci that track the nodes and edges we need to perform actions on.
Define the events we want to happen to nodes or edges at these loci.
The code to do this is:
def build( self, params ):
pInfected = params[self.P_INFECTED]
pInfect = params[self.P_INFECT]
pRemove = params[self.P_REMOVE]
self.addCompartment(self.INFECTED, pInfected)
self.addCompartment(self.REMOVED, 0.0)
self.addCompartment(self.SUSCEPTIBLE, 1 - pInfected)
self.trackNodesInCompartment(self.INFECTED)
self.trackEdgesBetweenCompartments(self.SUSCEPTIBLE, self.INFECTED, name=self.SI)
self.addEventPerElement(self.SI, pInfect, self.infect)
self.addEventPerElement(self.INFECTED, pRemove, self.remove)
We first grab the parameters by name from the parameters dict. We then
declare the three compartments by calling
CompartmentedModel.addCompartment()
, which takes a compartment
name and the initial probability that a node will randomly be placed
in that compartment. The parameter pInfected
(named
SIR.P_INFECTED
) gives the probability of a node initially being
infected (in the SIR.INFECTED
compartment, and since no nodes
start off in the SIR.REMOVED
compartment (probability 0.0
),
the probability of nodes being SIR.SUSCEPTIBLE
is 1 -
pInfected
.
We then declare that we want to track the I nodes and the SI edges,
and create two loci to do this. The first gets named SIR.INFECTED
by default; the second is given an explicit name. SIR.SI
(a name
will be created if none is given). The methods
CompartmentedModel.trackNodesInCompartment()
and
CompartmentedModel.trackEdgesBetweenCompartment()
each create an
return a Locus
, essentially a set of nodes of edges (with
some supporting methods that we’ll ignore for now).
Finally, we add events to the loci. These are per-element events,
which occur with the gibven probability to each event in the locus. We
provide to Process.addPerElementEvent()
the locus at which the
event occurs, the probability with which it occurs, which we
retreieved from the experimental parameters), and the event
function that is called when the event happens. The event functions
are methods that we’ll now define.
Events
Finally, we define the events that happen as part of the process.
def infect( self, t, e ):
(n, m) = e
self.changeCompartment(n, self.INFECTED)
self.markOccupied(e, t)
def remove( self, t, n ):
self.changeCompartment(n, self.REMOVED)
Both event functions are passed the current simulation time and the element on which the event should occur, drawn from the locus to which the event is attached.
For the infect
event, the element is an edge (because the event
occurs on SI edges). We extract the endpoints of the edge by
pattern-matching. Since the locus is defined as holding edges between
a node in the SIR.SUSCEPTIBLE
compartment and a node in the
SIR.INFECTED
compartment, we will be passed edges with this
orientation: we can assume that n
above is a susceptible node. We
use CompartmentedModel.changeCompartment()
to change the
compartment of n
to SIR.INFECTED
: the compartment of m
doesn’t change (it stays infected). We also mark the edge as one that
the infection travsersed using CompartmentedModel.markOccupied()
(the “occupied” terminology is slightly uninformative but is standard
in the literature, coming from percolation theory).
For the remove
event, which happens at infected nodes, the element
will be a node, and we simply change its compartment to
SIR.REMOVED
.
Note
You can see the code for epydemic
’s actual built-in SIR process
here.