Quickstart Guide Part 7: Transmission¶
In the previous lesson, you explored a model that had agents interact with each other using the ask
and tell
functions. These functions enable direct interaction between agents, where one agent can determine and change the value of a variable for another agent and thus influence the course of the simulation.
The FRED simulation engine also includes infrastructure for indirect interaction between agents via a mechanism called transmission. In a transmissible condition, one agent can cause another agent to move to a certain state within the condition (with a certain probability) simply by being present in the same place or network. This interaction is indirect in the sense that you do not have to write any specific actions for agents to carry it out - the transmission is handled internally within the FRED simulation engine, as long as the condition is set up properly.
In this lesson, you'll explore the transmission feature of the FRED modeling language by modeling an outbreak of an influenza-like disease in Jefferson County, PA. Don't worry if you don't have a background in epidemiology - the key details that you need to know will be explained below.
By the end of this lesson, you will be able to:
1. Define a transmissible condition within a FRED model using the transmission_mode
keyword.
2. Describe how agents' transmissible and susceptible properties facilitate the spread of a transmissible condition in a FRED simulation.
3. Specify an exposed state that agents are moved to upon successful transmission within a transmissible condition.
4. Use the set_state
command to move an agent to a specific state in a different condition.
5. Create plots of the number of agents in each state to visualize the spread of the transmissible condition.
Note that the idea of transmission is not limited to infectious disease. Transmission is useful for many models of social interaction where something - an idea, a belief, an advertisement, or some other piece of information - is spread between people.
Start by importing the packages required to execute simulations and create the visualizations:
# Epistemix package for managing simulations and simulation results in Python
from epx import Job, ModelConfig, SynthPop
import time
import pandas as pd
import folium
import map_disease
import plotly.express as px
import plotly.graph_objects as go
import plotly.io as pio
import requests
# Use the Epistemix default plotly template
r = requests.get("https://gist.githubusercontent.com/daniel-epistemix/8009ad31ebfa96ac97b7be038c014c0d/raw/320c3b0ca3dfbf7946e49c97254fa65d4753aeac/epx_plotly_theme.json")
if r.status_code == 200:
pio.templates["epistemix"] = go.layout.Template(r.json())
pio.templates.default = "epistemix"
7.2 Setting a Condition to be Transmissible¶
Start by opening the simpleflu.fred
file in the left panel.
The model defines a condition named INFLUENZA
. At the beginning of the block that defines this condition (before any states are defined), you'll notice some new lines of code:
These keywords are used to set the transmission properties of the condition. The first, transmission_mode
, indicates that the condition will be transmissible and specifies how transmission should occur. The value proximity
means that agents can transmit the condition by being in the same place - e.g., their household, workplace, or school - at the same time. The default value is none
, meaning that the condition is not transmissible.
The transmissibility
keyword sets the "contagiousness" of the condition - i.e., the ease with which it can spread between agents. The higher this number, the higher the expected number of contacts (i.e., potential transmission events) when agents are mixing at a given place and time.
The exposed_state
indicates the state that agents are moved to upon an exposure, i.e., a "successful" transmission event. We'll discuss this in more detail below.
7.3 Transmissible and Susceptible Agents¶
Transmissible conditions must also set two new properties for agents - transmissibility and susceptibility. These two properties are linked: if an agent is transmissible for some condition, then they can expose a susceptible agent to whatever is being spread in that condition. In the case of the model you are exploring in this lesson, the thing being spread is the influenza virus.
In a condition named CONDITION
, the transmissible and susceptible properties are set by the following keywords:
These properties exist for each condition separately - in other words, simulations can model more than one transmissible condition, if desired.
In the influenza model here, the virus is assumed to be novel and, thus, no one in the population has any immunity to it. This means that everyone is susceptible to infection at the start of the simulation. We achieve this in the agent_startup
block with the following code:
INFLUENZA
condition, meaning that, if they encounter a transmissible agent during the simulation, they may be moved to the Exposed
state (upon "successful" transmission).
Agents can be made to be transmissible in a given condition by setting the trans
keyword as follows:
Exposed
) from which they subsequently become infectious. More detail on that in the next section.
To summarize, in a transmissible condition, a transmissible agent can expose a susceptible agent to whatever is being spread in the condition. An exposure results in the transition of the susceptible agent to the Exposed
state defined for the condition.
7.4 The Exposed State and Introducing Exposures into the Simulation¶
Every transmissible condition must define an "exposed" state. This is the state that a susceptible agent transitions to upon a successful transmission event from a transmissible agent. The "exposed" state is set as follows at the start of the condition
block:
The event that introduces the virus into this simulation happens in the Import
state. Only the meta agent is sent to the Import
state, as specified by the following line of code:
Import
state at the start of the simulation, it selects 10 agents at random, and moves them to the state Exposed
.
state Import {
# Action rules
import_exposures(INFLUENZA, 10)
# Wait rules
wait()
# Transition rules
default()
}
In this model, an agent remains in the Exposed
state for roughly two days, before moving to one of two infectious states. At that point, the exposed agent's INFLUENZA.trans
property is set to be greater than 0 (the precise value depends on whether they are symptomatic), and they can go on to expose other agents to the virus.
Note that there is no transition rule that specifies that any agent should transition from Susceptible
to Exposed
- only an exposure, either from the meta agent or from contact with an already infectious agent, can cause an agent to enter the Exposed
state. Similarly, agents will not enter the Exposed
state of the RECORD_EXPOSURES
condition except as a result of the set_state
action rule in the INFLUENZA.Exposed
condition.
7.5 Run the Model and Visualize the State Count Data¶
Ok, thats a lot of preamble! Let's run the model and inspect the outputs to learn more about what is going on. Execute the cell below to run the model:
# create a ModelConfig object
flu_config = ModelConfig(
synth_pop=SynthPop("US_2010.v5", ["Jefferson_County_PA"]),
start_date="2023-01-01",
end_date="2023-06-01",
)
# create a Job object using the ModelConfig
flu_job = Job(
"trans_demo.fred",
config=[flu_config],
key="flu_job",
fred_version="11.0.1",
results_dir="/home/epx/qsg-results"
)
# call the `Job.execute()` method
flu_job.execute()
# the following loop idles while we wait for the simulation job to finish
start = time.time()
timeout = 300 # timeout in seconds
idle_time = 3 # time to wait (in seconds) before checking status again
while str(flu_job.status) != 'DONE':
if time.time() > start + timeout:
msg = f"Job did not finish within {timeout / 60} minutes."
raise RuntimeError(msg)
time.sleep(idle_time)
str(flu_job.status)
You just ran your first epidemic simulation! Here's a rundown of what just happened:
- At the start of the simulation, the meta agent exposed 10 agents to the influenza virus, moving them to the
Exposed
state. - Over the course of the next week or so, those infected agents carried out their daily schedule of activities, spending time at home, at work or school, and interacting with other agents in their census block group.
- At each place they visited, they mixed with other agents and potentially exposed some of them to influenza.
- Those new exposed agents reported information about themselves and their exposure to a file at the moment of their exposure.
- Those newly infected agents then went on to expose other agents in the population as they went about their daily schedules.
- The virus spread through the population, until it eventually ran out of people to infect, resulting in the end of the outbreak.
Model outputs¶
One important output that the FRED simulation engine produces is a record of the number of agents in each state on each day. You have encountered this output in earlier parts of the Quickstart Guide - for example, in Part 4, where agents slept and watched television.
Tracking the number of agents in each of the disease states is the foundation of epidemiological studies. It conveys information about the progression of the disease in the community, and it informs public health officials about which interventions may be required to bring an outbreak under control.
The code in the next cell retrieves the state count information for each state as a pandas DataFrame:
susceptible = flu_job.results.state(
condition = "INFLUENZA",
state = "Susceptible",
count_type = "new"
)
exposed = flu_job.results.state(
condition ="INFLUENZA",
state ="Exposed",
count_type ="new",
)
infectious_s = flu_job.results.state(
condition ="INFLUENZA",
state ="InfectiousSymptomatic",
count_type ="new",
)
infectious_a = flu_job.results.state(
condition ="INFLUENZA",
state ="InfectiousAsymptomatic",
count_type ="new",
)
recovered = flu_job.results.state(
condition ="INFLUENZA",
state ="Recovered",
count_type ="new",
)
Take a quick look at one of these DataFrames to see what data is included:
In the DataFrame above, you have the run_id number (equal to 0 in all cases, since we only ran a single simulation), the sim_day, which starts with the value 0 on the first day of the simulation, and a column named new. This gives the number of agents that entered that state on that simulation day.
The next cell does some merging and tidying up of these individual DataFrames to create a single DataFrame that contains the data for all of the states:
# rename the 'new' counts column to the name of the state
susceptible.rename(columns={'new':'Susceptible'},inplace=True)
exposed.rename(columns={'new':'Exposed'},inplace=True)
infectious_s.rename(columns={'new':'InfectiousS'},inplace=True)
infectious_a.rename(columns={'new':'InfectiousA'},inplace=True)
recovered.rename(columns={'new':'Recovered'},inplace=True)
# build up the merged DataFrame with counts for all states
all_states = pd.merge(susceptible,exposed)
all_states = pd.merge(all_states,infectious_s)
all_states = pd.merge(all_states,infectious_a)
all_states = pd.merge(all_states,recovered)
#retrieve the sim_date from the column and add to the all_states frame.
all_states['sim_date']=flu_job.results.dates().sim_date
all_states
As you can see, the all_states
DataFrame now contains the number of agents entering each state on each day of the simulation. We've also added the calendar date as a column, so we can plot real dates.
We can now use the plotly express package to create a figure with the so-called "epi-curves" for this outbreak:
fig = px.line(
all_states,
x='sim_date',
# pass a list to plot all states
y=[
'Exposed',
'InfectiousS',
'InfectiousA',
'Recovered',
],
title="The Evolution of an Influenza Epidemic"
)
# customize font and legend orientation & position
fig.update_layout(
font_family="Epistemix Label",
yaxis_title="Number of agents entering the state",
xaxis_title="Date",
legend_title="State",
title_font_size=30
)
fig.show()
In this simple influenza model, agents recover from the disease and then have complete immunity, so they are not able to be reinfected if they encounter a transmissible agent later on. That results in this epidemic having a single peak, because the number of susceptible agents is constantly decreasing.
The blue curve represents the exposed agents. Red and green show the infectious agents who are either symptomatic or asymptomatic. Note that the relative scaling of these curves is indicative of the one-third/two-thirds split in the probability that exposed agents become asymptomatic or symptomatic: there are roughly twice as many symptomatic agents every day.
The purple curve shows the recovered population. Notice the lag in the timing of the peak, which occurs several days later than the peak of the exposed curve - it takes some time (but not forever) for an infected agent to recover.
7.5 Including Multiple Conditions via Separate Model Files¶
You may have noticed that when we ran the model above, we did not specify the simpleflu.fred
file when we created our Job object, above. Instead, we passed the trans_demo.fred
file as the first argument. Open that file now to take a look.
Instead of containing the blocks that you have become accustomed to seeing - startup
, variables
, condition
, etc. - it contains two standalone lines of code that use a new command - include
:
In all the multi-condition models you have run up to this point of the Quickstart Guide, the different conditions have all resided in a single model file. Here, we have broken them out into two different files, simpleflu.fred
, which defines the disease condition for this simulation, and record_exposures.fred
, which instructs agents to record the location where they were exposed to influenza, along with some demographic information, in a file.
The include
statements in trans_demo.fred
tell FRED to load the conditions described in these two files and include them in the simulation. It can be useful to break conditions out into individual files like this. First, it helps keep model files from getting too long. Second, it makes it easy to control whether a condition should be used. Simply remove or comment out the include
statement for a condition to leave it out.
Cross-talk between conditions¶
A key component of the FRED modeling language is the ability for an action in one condition to transition an agent to a new state in another condition. You can see an example of this happening on line 45 of the simpleflu.fred
file:
Exposed
state in the INFLUENZA
condition into the Exposed
state in the RECORD_EXPOSURES
condition. Once in that new state, the agent then makes use of some additional functions related to transmission to determine where they were exposed.
Each of these functions queries records of the agent's exposure in the INFLUENZA
condition, representing another case of cross-talk between conditions. The was_exposed_externally
function determines whether or not the agent was exposed by the meta agent at the start of the simulation. If this function returns "false", then the agent uses the was_exposed_in
function to determine if they were exposed in one of the places that they regularly attend, like their household or workplace.
Once the agent determines where they were exposed, they obtain the latitude and longitude of that location (as we did in Part 5 of the Quickstart Guide) and write it to a CSV file, along with some demographic information.
7.6 Epi-Curves with Demographic Information¶
Let's start by reading the contents of the CSV file produced by the RECORD_EXPOSURES
condition. The next cell executes some code to create a nicely formatted DataFrame from the CSV file. Then, it plots the number of agents that entered the Exposed
state each day:
explocs = map_disease.reformat_expcsv(flu_job,"exposure_locs.csv")
fig = px.histogram(
explocs,
x="expdate",
labels={"expdate": "Date"},
title="Exposures per Day"
)
fig.update_layout( # customize font and legend orientation & position
yaxis_title="Number of exposures",
title_font_size=30,
bargap=0.1
)
fig.show()
This graph can be compared directly to the Exposed
state counts we graphed earlier in this lesson, drawn from the all_states
DataFrame that we created:
fig = px.bar(
all_states,
x='sim_date',
y='Exposed',
# changing date range to match previous plot
range_x=['2023-01-01','2023-04-05'],
title="Exposures per Day",
)
fig.update_layout(
yaxis_title="Number of exposures",
xaxis_title="Date",
legend_title="State",
bargap=0.1,
title_font_size=30,
)
fig.show()
As you can see, the curves look the exactly same over the date range of interest. However, we aren't making use of all the data available to us when making the first chart. Remember, every agent also recorded their demographic information when recording their exposure information. Thus, we can modify the code that made the first plot to disaggregate the exposures according to the race of the exposed agent:
fig = px.histogram(
explocs,
x="expdate",
# Added color keyword to group by agent race
color='race',
labels={"expdate": "Date"},
title="Exposures per Day by Race",
)
# customize font and legend orientation & position
fig.update_layout(
yaxis_title="Number of exposures",
title_font_size=30,
bargap=0.1,
)
fig.show()
Now, we have a stacked histogram, where the number of exposures for each race is indicated by a unique color. This information is not included in the base state counts output by the FRED simulation engine. But, by defining an additional condition in which agents record information in a file when they are exposed, we can access additional dimensions of data to use in subsequent analyses.
7.7 Mapping Disease Spread¶
The last thing we'll do with the data set from this simulation is make a map to show how influenza spread in Jefferson County during this outbreak. This type of information is uniquely accessible in agent-based modeling - other strategies for modeling disease spread tend not to capture this geographic detail.
Execute the code below to see the visualization. The first line of code downsamples the exposure location data to create a manageable sub-sample for this web-based map visualization. The second and third lines transform the latitude, longitude, and date into a format required to make the animated map. The last line runs some code that actually makes the map. You can examine these functions in the map_disease.py
file in this directory, if you are interested in the details of how they work.
sample_df=map_disease.resample_dataframe(explocs,8000)
exp_geojson = map_disease.create_geojson(
map_disease.create_geojson_features_basic(sample_df)
)
map_disease.fred_map_movie(exp_geojson)
Each time step of this animation respresents a day in the simulation, and each red dot represents the location of an agent exposed to influenza. These maps will look familiar if you've ever watched a movie with an epidemic, like Outbreak or Contagion. You can zoom in and out to see what is happening at different scales, from a single neighborhood to the whole county.
The spatial information captured here is incredibly useful for epidemiologists and public health policy makers. Maps like these can help indicate where clusters of disease might form, which can in turn help decision makers determine the best locations to roll out interventions like vaccines or stay-at-home orders.
7.8 Lesson Recap¶
You've covered a lot in this tutorial! The model you explored here gives a sense of the kind of complex social interactions that can be studied with FRED models. By running this model of influenza, you have encountered some important features of the FRED modeling language and strategies for creating rich models with useful data outputs.
- You saw how a condition can be designated transmissible, so that transmissible agents can expose susceptible agents to the property of interest being tracked in the simulation - in this case, the influenza virus.
- You encountered the idea of an "exposed" state, where susceptible agents are moved if a successful transmission takes place during the simulation.
- You plotted "epi-curves" to trace the progression of a flu epidemic. These plots display the number of agents entering each state of the flu model on each day.
- You learned how to use
include
statements to allow conditions for the same model to be written in separate files. - You used several functions that allow for cross-talk between states.
- You made use of the
print_csv
function in a separate state to record additional information about exposed agents in a file. Then, you used the resulting data to produce more complex visualizations of the evolution of this flu epidemic.
In the next lesson, you'll encounter another complex model that demonstrates how agents can interact with their environments. In doing so, you'll see another example of so-called emergent behavior, which agent-based modeling (with Epistemix) is uniquely suited to studying. Let's keep going!
7.9 Additional Exercises¶
-
Try adjusting the wait times for different stages of the SEIR influenza model.
How do these changes affect the epi-curves produces by the model? -
Now try adjusting the
INFLUENZA.trans
values for the symptomatic and asymptomatic states.
How do the effects compare to changing the infection period? -
Adapt the model so that, after a certain period of time, agents lose their immunity and return to the
Susceptible
state. Is there a maximum time scale for immunity loss that shows up in the epi-curves?