import matplotlib.pyplot as plt
import numpy as np
from epymorph.kit import *
from epymorph.adrio import acs5
Multi-node Simulations with Movement Models
SCENARIO:
Imagine a new respiratory pathogen emerges in Maricopa County, AZ where 5 people are initially infected. In an effort to control the outbreak, local public health officials across the state have asked us for guidance on how the pathogen might spread to different counties. We therefore need to consider human movement patterns in our model to understand spatial spread. We will build a County-level spatial model with epymorph
, such that each node of our model represents the population in a single county. The model then assumes that the populations within the county interact homogeneously. Each node is interconnected through human movement, which affects the regional spread of the pathogen.
The exercises in this vignette show you how to add nodes and movement between nodes to your disease models in epymorph
and will explore how varying how we model movement (i.e., the assumptions that govern movement flows) might impact disease dynamics.
Exercise 1
= CountyScope.in_states(['AZ'], year=2020)
my_scope
my_scope.labels# You can also see their FIPS code:
# my_scope.node_ids
array(['Apache, AZ', 'Cochise, AZ', 'Coconino, AZ', 'Gila, AZ',
'Graham, AZ', 'Greenlee, AZ', 'La Paz, AZ', 'Maricopa, AZ',
'Mohave, AZ', 'Navajo, AZ', 'Pima, AZ', 'Pinal, AZ',
'Santa Cruz, AZ', 'Yavapai, AZ', 'Yuma, AZ'], dtype='<U14')
from epymorph.data.ipm.sirs import SIRS
= SIRS() sirs_ipm
sirs_ipm.diagram()
from epymorph.data.mm.flat import Flat
= Flat() flat_mm
# Maricopa is the 8th node in the scope (i.e., index 7)
= init.SingleLocation(location=7, seed_size=5) my_init
= TimeFrame.of("2020-01-01", duration_days=190) time
= 0.15 comm_prop
= 0.4
beta = 1 / 4
gam = 1 / 200 xi
= SingleStrataRUME.build(
rume =my_scope,
scope=sirs_ipm,
ipm=flat_mm,
mm=my_init,
init=time,
time_frame={
params'beta': beta,
'gamma': gam,
'xi': xi,
'population': acs5.Population(),
'commuter_proportion': comm_prop,
}
)# Print a description of params
print(rume.params_description())
ipm::beta (type: float, shape: TxN)
infectivity
ipm::gamma (type: float, shape: TxN)
progression from infected to recovered
ipm::xi (type: float, shape: TxN)
progression from recovered to susceptible
mm::population (type: int, shape: N)
The total population at each node.
mm::commuter_proportion (type: float, shape: S, default: 0.1)
The proportion of the total population which commutes.
init::population (type: int, shape: N)
The population at each geo node.
- Run your simulation
= BasicSimulator(rume)
sim with sim_messaging(live = False):
= sim.run(
out =default_rng(5),
rng_factory )
Loading gpm:all::mm::population (epymorph.adrio.acs5.Population):
|####################| 100% (1.055s)
Running simulation (BasicSimulator):
• 2020-01-01 to 2020-07-08 (190 days)
• 15 geo nodes
|####################| 100%
Runtime: 0.903s
out.plot.line(=out.rume.scope.select.all(),
geo=out.rume.time_frame.select.all().group("day").agg(),
time=out.rume.ipm.select.compartments("I"),
quantity="Infectious in AZ Counties",
title="outside",
legend )
out.table.quantiles(=[1.0],
quantiles=out.rume.scope.select.all(),
geo=out.rume.time_frame.select.all().group("day").agg(),
time=out.rume.ipm.select.compartments("I"),
quantity )
geo | quantity | 1.0 | |
---|---|---|---|
0 | Apache, AZ | I | 5990.0 |
1 | Cochise, AZ | I | 10481.0 |
2 | Coconino, AZ | I | 11939.0 |
3 | Gila, AZ | I | 4234.0 |
4 | Graham, AZ | I | 3259.0 |
5 | Greenlee, AZ | I | 767.0 |
6 | La Paz, AZ | I | 1855.0 |
7 | Maricopa, AZ | I | 379413.0 |
8 | Mohave, AZ | I | 17673.0 |
9 | Navajo, AZ | I | 9060.0 |
10 | Pima, AZ | I | 90031.0 |
11 | Pinal, AZ | I | 34140.0 |
12 | Santa Cruz, AZ | I | 3875.0 |
13 | Yavapai, AZ | I | 20063.0 |
14 | Yuma, AZ | I | 18073.0 |
def log10_transform(data_df):
# we have to be careful to avoid log(0)!
= data_df["data"].apply(lambda x: np.log10(x) if x > 0 else nan)
log_value return data_df.assign(data=log_value)
map.choropleth(
out.=rume.scope.select.all(),
geo=rume.time_frame.select.rangex("2020-04-01", "2020-05-01").agg(compartments="max"),
time=rume.ipm.select.compartments("I"),
quantity=log10_transform,
transform= 6.0,
vmax = 2.5,
vmin )
Exercise 2
In Exercise 1, we used a “flat” movement model, in which individuals could move equally randomly to any other node. This led to more or less syncronous outbreaks across space, which is of course not very realistic. In this Exercise, we’ll use more realistic movement models.
Note that this new MM requires a calculation of centroids, but this can be done using the us_tiger
ADRIO. Additionally, we need a parameter ‘phi’ which modulates the effect of distance in the probability kernel. On your own, you can test how ‘phi’ influences the dynamics.
from epymorph.data.mm.centroids import Centroids
from epymorph.adrio import us_tiger
= SingleStrataRUME.build(
rume =my_scope,
scope=sirs_ipm,
ipm=Centroids(),
mm=my_init,
init=time,
time_frame={
params#For IPM
'beta': beta,
'gamma': gam,
'xi': xi,
#For MM
'population': acs5.Population(),
'commuter_proportion': comm_prop,
'centroid': us_tiger.InternalPoint(),
'phi': 20.0,
}
)
print(rume.params_description())
ipm::beta (type: float, shape: TxN)
infectivity
ipm::gamma (type: float, shape: TxN)
progression from infected to recovered
ipm::xi (type: float, shape: TxN)
progression from recovered to susceptible
mm::population (type: int, shape: N)
The total population at each node.
mm::centroid (type: [(longitude, float), (latitude, float)], shape: N)
The centroids for each node as (longitude, latitude) tuples.
mm::phi (type: float, shape: S, default: 40.0)
Influences the distance that movers tend to travel.
mm::commuter_proportion (type: float, shape: S, default: 0.1)
The proportion of the total population which commutes.
init::population (type: int, shape: N)
The population at each geo node.
= BasicSimulator(rume)
sim with sim_messaging(live = False):
= sim.run(
out =default_rng(5),
rng_factory )
Loading gpm:all::mm::population (epymorph.adrio.acs5.Population):
|####################| 100% (0.579s)
Loading gpm:all::mm::centroid (epymorph.adrio.us_tiger.InternalPoint):
|####################| 100% (0.704s)
Running simulation (BasicSimulator):
• 2020-01-01 to 2020-07-08 (190 days)
• 15 geo nodes
|####################| 100%
Runtime: 0.807s
out.plot.line(=out.rume.scope.select.all(),
geo=out.rume.time_frame.select.all().group("day").agg(),
time=out.rume.ipm.select.compartments("I"),
quantity="Infectious in AZ Counties",
title="outside",
legend )
map.choropleth(
out.=rume.scope.select.all(),
geo=rume.time_frame.select.rangex("2020-04-01", "2020-05-01").agg(compartments="max"),
time=rume.ipm.select.compartments("I"),
quantity=log10_transform,
transform= 6.0,
vmax = 2.5,
vmin )
Exercise 3
Finally, use a movement model that is based on real data on human behavior. Specifically, we will use a commuter-style movement model created from a model published by Pei et al. This model requires a commuter matrix, and we will use mobility data from the ACS5 commuting flows, which are at the County level. Notice that for the MM, we will use some control parameters with their defaults.
from epymorph.data.mm.pei import Pei as CommuterMM
from epymorph.adrio import commuting_flows
= SingleStrataRUME.build(
rume =my_scope,
scope=sirs_ipm,
ipm=CommuterMM(),
mm=my_init,
init=time,
time_frame={
params#For IPM
'beta': beta,
'gamma': gam,
'xi': xi,
#For MM
'commuters': commuting_flows.Commuters(),
# General:
'population': acs5.Population(),
}
)
print(rume.params_description())
ipm::beta (type: float, shape: TxN)
infectivity
ipm::gamma (type: float, shape: TxN)
progression from infected to recovered
ipm::xi (type: float, shape: TxN)
progression from recovered to susceptible
mm::commuters (type: int, shape: NxN)
A node-to-node commuters marix.
mm::move_control (type: float, shape: S, default: 0.9)
A factor which modulates the number of commuters by conducting a
binomial draw with this probability and the expected commuters
from the commuters matrix.
mm::theta (type: float, shape: S, default: 0.1)
A factor which allows for randomized movement by conducting a
poisson draw with this factor times the average number of
commuters between two nodes from the commuters matrix.
init::population (type: int, shape: N)
The population at each geo node.
= BasicSimulator(rume)
sim with sim_messaging(live = False):
= sim.run(
out =default_rng(5),
rng_factory )
Loading gpm:all::mm::commuters (epymorph.adrio.commuting_flows.Commuters):
|####################| 100% (10.932s)
Loading gpm:all::init::population (epymorph.adrio.acs5.Population):
|####################| 100% (0.628s)
Running simulation (BasicSimulator):
• 2020-01-01 to 2020-07-08 (190 days)
• 15 geo nodes
|####################| 100%
Runtime: 0.983s
out.plot.line(=out.rume.scope.select.all(),
geo=out.rume.time_frame.select.all().group("day").agg(),
time=out.rume.ipm.select.compartments("I"),
quantity="Infectious in AZ Counties",
title="outside",
legend )