Python model of Covid-19 epidemic | Towards Data Science

How long will the epidemic last? How long do we need to keep social distancing? What if it is lifted? What if nothing was done?
One month ago, with contributions from doctors and data scientists, I created a model in Python to forecast the evolution of the Coronavirus epidemic in the metropolitan region of São Paulo and answer the questions above. While some were skeptical about the scenarios forecasted, I am currently shocked by how accurate the predictions have proven to be. The model has since been improved and open-sourced, and that is why I want to share with you how to use it, for any region you want to model.
When predictions were made, we expected there was some delay in confirmation of deaths, but that was not part of the model, yet. By then, we still knew about deaths from hospital reports, before they reached official statistics. So we expected the official counts to follow a curve like projected, shifted to the right by a few days.
The model was saying: I don't know what will happen in the long term, but for the next 2–3 weeks any scenario looks the same. No surprise we got right those 3 weeks if we did model correctly the disease. For the last 10 days, the inclination of the curve seems to be in the middle between the two scenarios considered.
The current model can deal with uncertainties about how the epidemic behaves, and "learns" from observations. Nobody is still sure exactly how contagious the disease is, or how people are behaving. The shadowed areas around projections in the graphs represent that uncertainty. When learning, the model identifies which combinations of hypotheses can explain reality, and which can not.
Still, we can start asking questions.
What if... there was a lockdown as of April 20th? (In São Paulo, stay-at-home is just a recommendation, and some data points to 40% of people not staying at home each day)
What if... nobody was using masks outside hospitals? Assuming they are about 40% effective if the sick wears them, and another 20% effective for the susceptible person, we would expect the spread of the virus to be worse if nobody was using them.
What is... the effect of social distancing? Let's check how the model expects the reproductive number (the number of people infected by each infected person) has behaved in Rio de Janeiro:
As expected, social distancing is very effective in reducing transmission. People who can't afford to stay at home, or don't even have a home to stay, can't isolate as effectively, as shown by the higher reproductive number.
But wait... why does the reproductive number have a higher starting point (R0) for the very low income? While other social classes start at 3.4, very low income begins at 4.0. This non-obvious behavior has a simple explanation: more people living together increases the chances of transmission at home, as the model correctly pointed.
Finally, Why... death counts accelerated so badly in mid-April, to then seem to drop, and then increase again? (This happened in São Paulo, and the explanation explains similar patterns in New York and other places)
Taking into account the delays in diagnosis and death confirmations, and how they change during time can explain non-obvious behaviors seen in official numbers. We expect that real deaths were higher than the official counts for many weeks until mid-April, due to lack of testing capacity. When testing catches up, things seem out of control for a few days.
For simulations about Brasil, you can find detailed historical data here. For world data, this Kaggle dataset can be a good start.
(In case you are not interested in Python modeling, you can jump to the graphs near the end, with projections for New York State)
When modeling São Paulo, one thing was clear for me from the start: social inequality matters. So I knew the model would be more complex than most models built until then. Using a time-series model would be helpless because countries ahead in the curve had very different societies. Building a compartmental model (SIR, SEIR and the likes), predefining states, and specifying differential equations for transitions, would be unfeasible due to the many important aspects to be modeled. An individual-based simulation was the go.
By using Simpy, it was easy to model arbitrary characteristics of the disease or about people's behavior. No need to know in advance what are the state transitions, not even how many possible states, just the state variables and the logic governing them.
In short, we create a population randomly, following characteristics of its subsegments, we designate a few seed infections, and we let individuals interact. All rules about how people interact, what will be the outcome for a sick person, how long each phase of the disease will take, all that can be programmed explicitly. One event can trigger parallel logic, running independently from each other, with independent randomness. It is like writing asynchronous code, that will run synchronously in simulation time.
Python is great, but when modeling a disease that can have effects on small fractions of the population, a lot of individuals need to be simulated. 10.000 individuals and memory consumption and CPU usage sky-rocketed. To solve that, each individual became an object in Cython, with little changes in the code, but enabling the simulation of 1.000.000 individuals with about 1GB RAM consumed and huge performance gains.
Disease evolution and transmission were then modeled in detail. Most parameters came from these two sources:
Estimates of the severity of coronavirus disease 2019: a model-based analysis
Epidemic Calculator — Contains estimates for several disease parameters.
The current source code of the model can be found here:
github.com
First, let's clone the repo, set up a virtual environment, and install the simulation code. (Installation will download requirements, compile Cython code and install everything as a package)
git clone https://github.com/ArroyoAndre/covidsimulation.gitcd covidsimulation/python3 -m venv venv. venv/bin/activate./setup.sh
Then, start Jupyter:
jupyter notebook
In a notebook, let's start with the imports:
Now, we will define a population. To model a region for real, you can define several populations (e.g. city and country-side, rich and poor, general population and essential service workers, ...)
We will simulate New York State. Numbers are provided for didactic purposes. (for example, I invented age distributions and home sizes)
We also need to specify the Interventions. We will use two kinds of interventions. SocialDistanceChange will account for school closures and social gatherings banning on March 10th, and stay-at-home orders on March 22nd. On April 10th and 12th, we will use the DiagnosisDelayChange intervention to reduce the delay on testing results, to account for a spike in deaths reported a few days later.
Wait... Why did I start diagnosis_delay at 10 days, to then reduce it to 5 days? It seems reasonable that the time for exams to be collected, sent to a lab, tested, results to come back, and deaths to be reported, can sum to around 5 days. According to Wikipedia, NY had around 700 deaths each day between April 10th and April 15th. On the 16th and 17th, there were 3500 more deaths than the previous average, to then return to the previous trend. So we can hypothesize there were about 5-days-worth deaths pending confirmation, that finally got confirmed. Therefore the previous delay of 10 days.
Now, we need to wrap all of that into a region's Parameters object.
Because with few infectees the time for the epidemic to gain traction can vary a lot, we align all simulations in D0, defined as the date in which the number of people infected reaches a threshold. We will define D0 as March 10th (because not much happened before that). Since we don't know how many people were infected by then, we set that as a RandomParameter with a uniform distribution between 10k and 200k.
We have added two uncertain parameters to the model. A few others, like the rate of transmission from an infected person, are also not known or might vary between regions, and default to RandomParameter. That's why we need to calibrate our model.
Often, the variable with fewer biases that we can use for calibration is the official death count. There might be some systemic underreporting and systemic delay, but that is better than other biases. We are already assuming the average delay for confirmation of cases begins at 10 days and changes to 5 days in mid-April, so we should be fine.
We will create a score function that will give lower loss values to simulations that better match the official death count at certain dates:
We will then start the calibration process. First, we will try several combinations of values for the model's internal parameters, using small populations of 100k individuals. (I use a virtual instance with 32 CPU cores for this. On a laptop, it might take a while...) We will do 1000 trials and keep the 100 best (p=0.1).
Second, we will try those 100 best on populations of 1 million individuals, to reduce noise when separating the best ones. We will then keep the 16 best.
Those random_states are our posterior of the joint distribution of all uncertain parameters in the model. With appropriate naming, it is possible to use the posterior from one region as a prior for another region, to get a refined posterior.
After all the time spent calibrating you probably want to save the random_states found so far. (How to do it using pickle is beyond the purpose of this post, but you are encouraged to google it)
We are now in shape to do a real simulation, and predict something in the future. We will take the 16 best random_states found, and apply each of them twice, on a population of 2 million individuals. Again, we will keep the 16 simulations (0.5 of them) that best fitted our calibration data.
Let’s plot the confirmed deaths, to do the first check:
And total deaths, without the confirmation delay, in log scale:
We can also satisfy our curiosity and see which values were learned for our RandomParameters.
It is possible to see the implemented metrics:
What about the fatality rates? There is not a metric for that, but we can create one.
Although this was quick modeling (sorry if you are running a laptop) for didactic purposes, the results are impressively realistic.
Good priors are gold. In many cases, avoiding UniformParameters, even if replacing them by a different distribution, can make the difference between a model that can just learn the past, and one that can really forecast non-obvious behaviors about the future. Remember:
sh*t in → sh*t out
Performing many simulations (starting from 10k random_states at least), and performing them with large populations (2 million for presentation purposes) can also substantially increase accuracy.
Adding different types of metrics to the scoring function can make more output variables accurate.
There are many more features that were not explored here, like studying the effect of hygiene measures or of using masks. It is also possible to simulate different social distancing scenarios, including intermittent distancing, and to take into account the health system’s capacity.
I hope you can make good use of the model, either to inform people about forecasts and scenarios in your region, or just to learn about how it was built and how to do cooler stuff in Python.
You are invited to collaborate to improve the model, to model new regions, to add new features, and to share your results.
Note from the editors: Towards Data Science is a Medium publication primarily based on the study of data science and machine learning. We are not health professionals or epidemiologists, and the opinions of this article should not be interpreted as professional advice. To learn more about the coronavirus pandemic, you can click here.
Some rights reserved