Epidemic Modeling for Babies

Jake Underland (GDSC Waseda)

2022-01-08

Acknowledgment: This programming assignment was originally designed by Emma Nechamkin, inspired by the book Networks, Crowds, and Markets. It was furthered developed by the faculty of University of Chicago, whose specific names are cited in the source files. Additionally, modifications were made by Jake Underland.

Introduction

An SIR model is a simple but commonly used model for epidemics. The name derives from the three possible states that a person in the model can take: Susceptible to the disease, Infected with the disease, and Recovered from the disease. Our model uses these three states as well as social factors (the interactions of people in the network) and biological factors (duration of typical infection) to model the spread of disease in a given community.

This assignment can be completed using only basic programming operations outlined here.

Before you start

On your terminal, create a directory to store the files for this assignment, and navigate to that directory. For those of you who have a github and know how to push to your github repository, follow all of the code below. Otherwise, skip the last 3 lines.

$ mkdir sir
$ cd ./sir
$ git init 
$ git remote add upstream https://github.com/jaked0626/sir.git
$ git pull upstream main
$ git remote add origin https://github.com/YOUR_USERNAME/REPOSITORY_NAME.git
$ git branch -M main
$ git push -u origin main

The README.txt file explains what each file is.

Now, open Visual Studio Code (or your preferred code editor) and open the files to edit, as well as an ipython terminal in the same directory. If you have trouble with this part, we are happy to take questions.
Once you have the terminal open in VSC, add the following:

$ ipython  
In [1]: %load_ext autoreload
In [2]: %autoreload 2
In [3]: from sir import *

Once you’ve done this, you can test all of the functions contained in the file sir.py in the ipython terminal. All you have to do is make edits to the original file, save it, and call the function in the terminal.

The Model

To begin building our SIR model, we must specify the model’s details. In particular, we must decide what data we will model as part of our simulation (including the exact Python types and data structures we will use) as well as how to model the behavior of a disease.

In our simulation, the data we will be modeling is the following:

  • Disease states: The three states describing the health of each person in the model
  • Person: How a person is represented
  • Structure of city: How a city is represented, the city being a collection of neighborhoods of people.

A more detailed explanation of each data type is specified below:

Disease states

All people in the simulation can exist in one of three disease states, Susceptible, Infected, or Recovered.

  • Susceptible: The person is not infected but can be infected in the future. This state will be represented with the string 'S'.
  • Infected: The person is currently infected with the disease. This state will be represented with the string 'I'.
  • Recovered: The person has recovered from an infection and will be immune to the infection in the rest of the simulation (beware, this is an unrealistic assumption!). This state will be represented with the string 'R'.

Person

A person is represented in a string. The string will contain that person’s disease state and, if they are infected ('I'), the number of days the person has been in that disease state (e.g., 'I0', 'I1', 'I2', etc.).
For example, a person who has been Infected for 3 days will be represented as I3.

Structure of a City

A city is represented as a list of people. Since a Person is represented as above, the city will take the form ['S', 'R', 'I1'] The above city represents 3 people, one who is Susceptible, one who has Recovered, and one who has been Ill for 1 day.
Each person in our model has one or two neighbors, one to their immediate left and/or one to their immediate right. Beware, the person at the very front of the list only has one neighbor to the right, and the person at the back of the list only has one neighbor to the left.

Rules of the Simulation

As the simulation progresses, the values representing people will be updated. The rules of the updates are as follows:

  • Disease Transmission: A susceptible person with at least one infected neighbor will always get infected the next day. There is no other way for a person in our simulation to become infected.
  • Contagiousness of infected people: The number of days a person is infected and remains contagious is a parameter to the simulation. We will track the number of days a person has been infected as part of their state. People who become infected start out in state 'I0'. For each day a person is infected, we increment the counter by one: 'I0' becomes 'I1', 'I1' becomes 'I2', etc. When the counter reaches the specified number of days contagious, we will declare them to be recovered ('R') and no longer contagious. At that point, they are immune to the disease and cannot become re-infected. For example, if we are simulating an infection in which people are contagious for three days, a newly infected person will start in state 'I0', move to 'I1' after one day, to 'I2' after two days, and to state 'R' after three days.
  • Advancing the simulation: To update the state of the simulation, we process all the people in the city and, if the above two rules cover a person, we update that person as described above.
  • Stopping condition: the simulation should stop when there are no more infected people in the city.

Your tasks

Now we know the fundamentals of the model, we describe the tasks you will need to complete to assemble the whole of the model. Each task corresponds to a function which you will be coding in sir.py. If you need to review functions, take a look at this.

Task 1: Count the number of infected people in a city

def count_infected(city):
    '''
    Count the number of infected people

    Inputs:
      city (list of strings): the state of all people in the
        simulation at the start of the day
    Returns (int): count of the number of people who are
      currently infected
    '''

    # YOUR CODE HERE

    # REPLACE -1 WITH THE APPROPRIATE INTEGER
    return -1

This function takes a city (represented as a list) and counts the number of people who are infected ('I{int}').
For example, given city ['I0', 'I0', 'I2', 'S', 'R'], the function would return 3 (notice how we have to account for the fact that there are multiple infected states). Given a city such as ['S', 'S', 'S', 'S'], the function would return 0.

Testing Task1
You can test your code on the ipython terminal as explained earlier. Try running test cases, and see if your code is outputting correct results.

In [5]: count_infected(['I0', 'I0', 'I2', 'S', 'R'])
Out[5]: 3

When you’re fairly confident with your code, open another terminal at the same directory, but DO NOT open ipython. Here, you can run the code:

$ py.test -xvk count

to run a set of automated tests prepared for you.
If your code passes all the tests, it is working correctly and you can go to the next step.

Task 2: Is a neighbor infected?

def has_an_infected_neighbor(city, position):
    '''
    Determine whether a person has an infected neighbor

    Inputs:
      city (list): the state of all people in the simulation at the
        start of the day
      position (int): the position of the person to check

    Returns:
      True, if the person has an infected neighbor, False otherwise.
    '''

    # This function should only be called when the person at position
    # is susceptible to infection.
    assert city[position] == "S"

    # YOUR CODE HERE

    # REPLACE None WITH THE APPROPRIATE BOOLEAN VALUE
    return None

You will complete function above. Given a city and a position (index) pointing to an individual person in the city, determine if that person has any infected neighbors.
Remember, not all people have two neighbors. You have to control for corner situations where the person that position points to only has 1 neighbor.
Furthermore, the following line

assert city[position] == "S"

ensures that the person we will be calling this function on is of disease state "S". Otherwise, there will be no use in calling this function–if the person is Infected or Recovered, it doesn’t matter if their neighbor is Infected, since they cannot contract the disease from them!

Testing Task 2
As the previous task, start testing your code in ipython.

In [6]: has_an_infected_neighbor(['I1', 'S', 'S'], 1)
Out[6]: True

In [7]: has_an_infected_neighbor(['S', 'I1', 'IO'], 0)
Out[7]: True

In [8]: has_an_infected_neighbor(['S', 'R', 'IO'], 0)
Out[8]: False

In [9]: has_an_infected_neighbor(['S', 'I0', 'S'], 2)
Out[9]: True

In [10]: has_an_infected_neighbor(['S'], 0)
Out[10]: False

When you’re done manually testing your code, you can run the automatic tests via terminal:

$ py.test -xvk has

Task 3: Advance person at position

Your third task is to complete the function advance_person_at_position which takes city, position and days_contagious as inputs. The goal of this function is to advance the disease state of a person from one day to the next. Given a city, a person’s location within that city, and the number of days days_contagious the infection is contagious, your function should determine the next state for the person. Specifically, if the person is:

  1. Susceptible ('S'): you need to determine whether they have an infected neighbor (by using the has_an_infected_neighbor function) and, if so, change them to the first infected state ('I0'). Otherwise, they remain in the Susceptible ('S') state.

  2. Infected ('I', followed by an integer; we will refer to that integer as \(x\)): determine whether the person remains infected (that is, \(x+1 <\) days_contagious ) and moves to the next infected state (e.g. 'I0' becomes 'I1', etc) or switches to the recovered state ('R'). To compute the new state of an infected person, you will need to extract the number of days infected from the state as a string, convert it to an integer, and then compare it to the number days_contagious . If you determined the person will remain infected, you’ll need to construct a new string from 'I' and \(x + 1\).

  3. Recovered ('R'): you should do nothing. Recovered people remain in that state.

Testing Task 3
Begin with manual testing in ipython:

In [20]: advance_person_at_position(['I0', 'I1', 'R'], 0, 2)
Out[20]: "I1"

In [21]: advance_person_at_position(['I0', 'I1', 'R'], 1, 2)
Out[21]: "R"

In [22]: advance_person_at_position(['I0', 'I1', 'R'], 2, 2)
Out[22]: "R"

When finished with manual testing, run

$ py.test -xvk advance

Task 4: Move the simulation forward a single day

Your fourth task is to complete the function simulate_one_day. This function will model one day in a simulation and will act as a helper function to run_simulation. More concretely, simulate_one_day should take the city’s state at the start of the day and the number of days a person is contagious \(c\) and return a new list of disease states (i.e., the state of the city after one day).

Your implementation for this function must use advance_person_at_position to determine the new state of each person in the city.

Testing Task 4
In ipython:

In [24]: simulate_one_day(['S', 'I0', 'S'], 2)
Out[24]: ['I0', 'I1', 'I0']

Notice how the susceptible people at positions 0 and 2 both become infected (they both have an infected neighbor) and the person at position 1 advances to the next state of their infection ('I0' to 'I1').

Automated testing on terminal:

$ py.test -xvk one

Task 5: Run the simulation

Your fifth task is to complete the function run_simulation, which takes the starting state of the city and the number of days a person is contagious, and returns both the final state of the city and the number of days simulated (or the number of days it took to arrive at the final state) as a tuple (a tuple is a pair of variables, similar to a list of length 2). You will notice this function also takes two additional optional parameters (random_seed and vaccine_effectiveness); you can ignore these parameters as we will not use them until the next task.

The function must run one whole simulation, repeatedly calling simulate_one_day until you reach the stopping condition of the simulation: when the city has no infected people in it. As you do this, the function must also count the number of days simulated.

Take into account that, if the stopping condition is true at the start of the simulation, then the number of days simulated will be zero.

Testing Task 5
In ipython:

In [32]: run_simulation(['S', 'S', 'I0'], 3)
Out[32]: (['R', 'R', 'R'], 5)

In [33]: run_simulation(['S', 'R', 'I0'], 3)
Out[33]: (['S', 'R', 'R'], 3)

Here, we see that the first city took 5 days to arrive at its final state, and the second city took 3.

Automated testing:

$ py.test -xvk run

Task 6 and 7

Will update shortly