Simulating the Coronavirus Epidemic in Switzerland with a realistic model
By: Thomas Neder
Tags: Coronavirus, Python, Covid 19, Numpy, Data Visualization

Next, I will explain how the analysis works, starting with the model. To arrive at above plot, I had to check the model with different values of its parameters plugged in a vast number of times against the data, keeping only those parameter values and predictions where the predictions of the model lie within the constraints given by daily death count data and daily total cases. There are a few assumptions made to justify the model used and also some shortcomings of it, all of which will be discussed after explaining it.

The model itself can be thought of as a function that when given any values for its parameters returns timelines of the different cases. But, for a complicated system like an epidemic, one can unfortunately not write down this function in a closed form. Instead the model is specified using differential equations, where the variables are fractions of the total population in different types of subpopulations. Any person can be a member of one and only one types of population. The populations appearing in the model are “Never infected”, “Currently infectious”, “Recovered, immune”, “Dead”. Note that these are more specific than just “Healthy” and ”Infected”. The main parameters of the model describe the relations between the populations. These can be summarized in the following diagram, which I’ll explain below. In this discussion we’ll also be able to see one first main shortcoming of the model, namely that one possibly important type of population is still missing, something one could address in a later analysis.

The arrow originating from “Currently Infectious” going left and meeting the arrow coming from “Never infected” means that a currently infectious person has to meet a person that has never been infected to add to the population in “Currently Infectious”. This would subtract the corresponding amount from “Never infected”. The interpretation of the parameter α in the simulation I ran is the amount of people one infected person creates on average per day. The arrows originating from “Currently Infectious” pointing rightward simply mean that “Currently Infectious” people can become members of “Recovered, Immune” or sadly, “Dead”. The parameters shown there correspond exactly to the daily probabilities of one person moving from “Currently Infectious” to one of the other populations, i.e. β is the daily probability of an infected person to recover and become immune, while γ is the daily probability of an infected person to die.

This model has some obvious shortcomings: First of all, spatiality, i.e. the population being spread out in space with differing densities and all the rest of it, is completely ignored. For a small, densely populated country like Switzerland this assumption seems reasonable. For a larger, less densely populated country, e.g. the United States, this assumption might no longer sufficiently hold. Such a system one could attempt to model by having spatially separated populations appear in the model with additional transition probabilities between them. Secondly, many other possibly meaningful subpopulations are being ignored as well. In the model used here, incubation for example is instantaneous: a person coming into contact with an infectious person immediately becomes infectious as well. This means that one infectious person in the model might not really correspond to real-world infectious persons, as one might need to split this group into infected(not yet infectious) and infectious. To inform policy, other stages through which infected persons may progress are important: One might add groups of people that are severely ill, that need to be hospitalised, that need to be taken into intensive care units, and that may need to be put on inhalators. This would of course complicate the model immensely, as additional transition probabilities between these groups and the subpopulations already in the model would need to be introduced where appropriate. Finally, the transition probabilities are assumed to be constant, which if society goes into lockdown is not realistic. However, for most of the time period from which data was used, quarantine was already in place, such that the probabilities calculated reflect spreading characteristics during and despite quarantine measures.

The dynamic relations between the populations in the model can now be expressed via differential equations, which, when iterated numerically, become difference equations. The next paragraph shows the part of the python code that calculates the daily change in the populations:

for i in range(time_steps):



delta_healthy = - alpha*healthy*infected

delta_infected = + alpha*healthy*infected \ -(beta+gamma)*infected

delta_immune = + beta*infected

delta_dead = + gamma*infected



healthy = cut_off(0,1, healthy+delta_healthy)

infected = cut_off(0,1, infected+delta_infected)

immune = cut_off(0,1, immune+delta_immune)

dead = cut_off(0,1, dead+delta_dead)

What I did not mention yet, is that each of the variables and parameters appearing in that bit of code is actually a numpy array of at least 32³ entries to make use of all the vectorization and parallelization built into numpy I could get to help speed up the scan of the parameter space on my 2012 Dell Potato.

The analysis works roughly as follows then. After loading the data, I am smoothing it slightly by sliding a moving average of 3 days over the data to correct for spikes and dips caused by delays in data reporting. The data consists of time series of integrated daily reported deaths and integrated daily total reported confirmed cases, taken directly from the WHO situation reports (https://www.who.int/emergencies/diseases/novel-coronavirus-2019/situation-reports/). I am using the total cases as a lower bound, but for deaths, I am assuming that as rare integer count events, they are Poisson-distributed. Because of this, I assume that if any day had a total number of deaths N that were picked from a Poisson distribution, the true mean of the Poisson distribution lies with the usual probabilities within 1 sigma, 2 sigmas, etc. where the 1 sigma error is given by the square root of N.

Next, I want to perform a scan of the parameter space. This means that I need to generate lots of parameters, which are then plugged into the model, after which the model predictions are checked against the constraints I want to impose from the data. To do this, the function that does one round of scanning, first generates large vectors of randomly chosen values for the three parameters, alpha, beta, gamma. Equally long vectors of the initial populations of the different types of the population are generated as well, after which they’re multiplied together in the way shown in above code snippet.

alpha = 1*np.random.random_sample((n**3,))

beta = np.random.random_sample((n**3,))

gamma = 1*np.random.random_sample((n**3,)) healthy = np.array([1 for i in range(n**3)])

infected = np.array([1/initial_healthy_pop for i in range(n**3)])

immune = np.array([0 for i in range(n**3)])

dead = np.array([0 for i in range(n**3)])

The result of one iteration are four numpy arrays of length n³ containing the predictions of the n³ models differing by their input parameters. These are then appended to larger arrays which at the end of that simulation run contain the n³ simulated time series, which are then returned by the function.

Next we check if the models lie within the bound prescribed by the data, i.e. whether the total number of cases in each model is larger than the total number of cases for each where the data is available so far, and if the number of deaths lies within the upper and lower bounds generated from the daily death counts and their errors. However, to speed up things considerably, I am trying to perform all operations on large numpy arrays solely by using built-in operations and functions that according to the numpy manual are vectorized and already internally use parallelization where possible:

for j in range(max_delay):



dead_timeline_cropped_transposed = \ np.transpose(dead_timeline[j:j+len(dead_data)])



healthy_timeline_cropped_transposed = \

np.transpose(healthy_timeline[j:j+len(dead_data)]) # t_j == 1: model good

t_j_list = (np.prod( \ np.heaviside(dead_timeline_cropped_transposed-dead_lower,1) * \ np.heaviside(dead_upper-dead_timeline_cropped_transposed,1) * \ np.heaviside(total_healthy_upper-\ healthy_timeline_cropped_transposed,1),1))

The speed-up achieved by never ever looping over large arrays is what only made this type of analysis possible on my computer, as the speed-up is by a factor of several thousand, which makes the difference between the program running for 2 hours and 2000 hours or more.

The elements of the array t_j_list indicate for every model if it is valid, i.e. if its predictions lie within the constraints by the data. Now only for those models that survived this step, the values of the three main parameters are saved, also externally, together with a new parameter generated in this step, namely all possible values of how many days the data needs to shifted along the simulation for the model to be allowed. A model is allowed when at any point in its simulation it lies within the constraints of the data.

This entire step of generating parameters, running simulations, checking against the data, saving surviving points can be repeated as often as necessary to collect sufficient amounts of statistics for the next step, analysing the valid models and plotting the data.

The previous step was generally the hardest one. Now for the few 100 surviving models, I simply checked for every day and every population what the minimal and maximal values on that day were. Similarly one can check what the ranges of the parameters were and other properties of the data, such as when the peak was reached in the surviving models, or when the earliest day with less than one infected person is.

I am still improving the program, but the code will be available when I have sufficiently cleaned it up. I am planning to update this post as further data becomes available and as I improve the analysis.