Building Simulations in Python — A Step by Step Walkthrough | by Terence Shin | Towards Data Science

Be sure to subscribe here or to my personal newsletter to never miss another article on data science guides, tricks and tips, life lessons, and more!
IntroductionThe Most Basic SimulationAdding More Variables to the SimulationAdding EVEN MORE complexityFull Code
Introduction
The Most Basic Simulation
Adding More Variables to the Simulation
Adding EVEN MORE complexity
Full Code
Computer-generated simulations are one of the fastest-growing technologies today, and it’s never been more important with COVID-19 around. You might have wondered how governments and agencies make decisions when it comes to enforcing lockdowns, closing schools, banning indoor dining, etc... These decisions aren’t blindly made. They’re made through simulations.
A simulation is simply an imitation of a situation or a process. In the case of this pandemic, governments use simulations to see how different factors, like the ones listed above, impact the spread of COVID-19.
Simulations are useful for a number of reasons:
They can turn into powerful visualizations that make it easier to communicate insights and findings.
They can be used for predictions and forecasting, but we’ll talk more about this later.
They can help you understand the most important factors in a simulation. (Eg. How does a 1% change in factor A affect the output vs a 1% change in factor B?)
A big misconception about simulations is that they’re really complicated, and that’s not necessarily the case! Simulations can end up being very complex, but they can also be very simple.
In this article, you’ll learn how to build a very simple simulation/model of population growth, as well as how it can be improved.
Note: In this article, the terms “model” and “simulation” will be used synonymously.
Be sure to subscribe here or to my personal newsletter to never miss another article on data science guides, tricks and tips, life lessons, and more!
The most basic human population simulator that we could possibly create would be something like this, where the initial population is 50 and we want to see how the population grows to 1,000,000:
totalPopulation = 50 growthFactor = 1.00005dayCount = 0 #Every 2 months the population is reportedwhile totalPopulation < 1000000:    totalPopulation *= growthFactor    #Every 56th day, population is reported    dayCount += 1    if dayCount == 56:         dayCount = 0        print(totalPopulation)
If you can tell, this program is simply an exponential function and is too basic of a model to be able to simulate population growth. As you might have already thought, the total number of people in an environment won’t increase by a set percentage every cycle, and there are many factors that affect the population that aren’t included.
This is okay! This is how a simulation starts. It starts off simple and gets more advanced. Let’s see how we can improve on this.
If we want to run a more realistic simulation of the human population, we’ll need to establish some factors and use them to control when people are born, reproduce, and die.
So, let’s set up a more complicated simulation. Although there are millions of variables that can change population, we’ll stick with just a few major ones. Once these are set up, we can program them to fluctuate, and they can be tinkered with to imitate different scenarios.
I’ve chosen eight very simple factors:
Starting populationInfant mortalityFoodFertility x & Fertility yHealthcareAgricultureChance of disasterAge of death.
Starting population
Infant mortality
Food
Fertility x & Fertility y
Healthcare
Agriculture
Chance of disaster
Age of death.
I’ll explain how each of these works as I go. Let’s create a new Python script in the IDLE. We’ll use the random function quite a lot, so import that first:
import random
Now we assign a value to the variable startingPopulation. This can be virtually whatever you want, but I’m using fifty.
startingPopulation = 50
Realistically it would be impossible to grow a population significantly from just fifty people, and inbreeding would cause endless genetic diseases, but let’s keep it simple.
Our next variable is infantMortality. This is the chance of a child dying in their first year of life. Our World in Data estimates an average infant mortality rate of ~25% over the past two millennia. I’ll use that because it can give us a good historical simulation.
infantMortality = 25
The variable, agriculture, will be how many “units” of food each person produces. One unit of food feeds a person for a single year. In our simulation, each person must eat one unit of food every year or they will die. Therefore, if agriculture is exactly 1 in a given year, everyone eats. If it is 0.5, fifty percent of the population dies. If it is 1.5, there is a fifty percent excess of food, and this can be stored and used as a backup if the agriculture in the next year is not enough to support the population.
Explained simply, if the agriculture value is not above 1, the population cannot grow, because the population would starve. To start, I’ll use a value of 5, meaning each person produces enough food for themselves, plus four more people:
agriculture = 5
Then we have disasterChance. This is the probability every year that some natural disaster comes along and damages the population. It will affect the population by a random percentage, from 5–10%. To start with, I’ll leave the disasterChance at 10%, meaning on average every ten years a disaster will come along.
disasterChance = 10
food is pretty self-explanatory. Every year we will run a “harvest” function and the output of that harvest will be the population multiplied by agriculture. Even if your agriculture is below 1, if you have stocks of food, you can theoretically maintain your population.
food = 0
Finally, we have fertilityx and fertilityy. These are the ages at which a women can become pregnant in our simulation. For my program, I’m using the range of ages 18 to 35.
fertilityx = 18fertilityy = 35
Now, we need a way for these factors to control our population on a year by year basis. We could do this a mathematical way and use all sorts of percentages, but I think it’d be more intuitive and versatile if we used object-based programming to simulate each person.
Each person will be an object with a few factors, and every cycle (that is, one year of our simulation) that person will be affected in some way — at the least, their age will increase by 1. We need something to store all these people, so let’s create a list:
peopleDictionary = []
That’s where each object will be stored and we can use a for loop to cycle through each person year by year and alter them. If you are not familiar with Python classes and objects, I would probably read up on them. But if you’re already up to scratch, let’s define a class which we can use for every human:
class Person:    def __init__(self, age):        self.gender = random.randint(0,1)        self.age = age
This is the way we’ll “spawn” a person. As you can see, each person has a gender and age. You may notice that when we “spawn” a person, their age is not set to 0. The reason for this is because when we initiate the simulation, we’ll use this class to create a group of people with random ages, so we don’t want to immediately set every person to 0 years old.
There are several functions we need to include in this program. For now, we want the program to simulate a population that exponentially increases over time — just like our very simple demo program from the start of this article. Therefore, the following must happen:
All “able” people, that is, anyone over 8 years old, works in the fields to produce food. This includes both men and women for simplicity.A certain number of women in the fertility band give birth to babies every year.Anyone over eighty years old dies.
All “able” people, that is, anyone over 8 years old, works in the fields to produce food. This includes both men and women for simplicity.
A certain number of women in the fertility band give birth to babies every year.
Anyone over eighty years old dies.
With this model, the population should increase exponentially, because enough infants are born every year to outweigh the number of people dying from old age, and as these infants reach 8+ years of age, they produce food to support the civilization.
Let’s create the harvest function:
def harvest(food, agriculture):    ablePeople = 0    for person in peopleDictionary:        if person.age > 8:            ablePeople +=1    food += ablePeople * agriculture    if food < len(peopleDictionary):        del peopleDictionary[0:int(len(peopleDictionary)-food)]        food = 0    else:        food -= len(peopleDictionary)
Every year of the simulation there is a “harvest”. The function takes the food and agriculture values. It calculates how many “able” people there are to produce food by counting those over 8 years of age. Of course, we multiply the number of able people by agriculture to get the total food produced for the year.
If the food is not enough for each person to eat 1 unit of food, then the dictionary of people is limited to the amount of food available, and the excess die from starvation. This ensures that if there is not enough food to go around, people who cannot eat die, and the population is effectively limited. But if there is excess food, each person consumes 1 unit, and whatever is left is stored for the next year!
To grow our population, we need a reproduce function:
def reproduce(fertilityx, fertilityy):    for person in peopleDictionary:        if person.gender == 1:            if person.age > fertilityx:                if person.age < fertilityy:                    if random.randint(0,5)==1:                        peopleDictionary.append(Person(0))
Here I have assumed that 1 in 5 women will become pregnant every year. Of course that figure isn’t exactly accurate to real life, but it’ll work fine for our simulation! The reproduce function cycles through every person in the dictionary of people. If any person is female and between the ages of 18 and 35, they have a 20% chance of giving birth, and a new person of age 0 is appended to the dictionary of people.
Fantastic, we have our very basic functions laid out. Although this could be done more simply using maths, setting our code out as an object-based script allows for more flexibility; if you wanted to expand the program and make it more realistic, you could add various characteristics to each person, such as medical conditions or social class.
We need a way to initiate our simulation. To get the ball rolling, we spawn fifty people, with random ages between 18 and 50. This is our beginSim function:
def beginSim():    for x in range(startPopulation):        peopleDictionary.append(Person(random.randint(18,50)))
As you can see, we add fifty people to our people dictionary, each of which has a random age. We can call this function at the bottom of our script, like this:
beginSim()
After starting the program and creating these people, we need a system to cycle through each year, and perform the harvest and reproduce functions. We’ll call this function runYear:
def runYear(food, agriculture, fertilityx, fertilityy):    harvest(food, agriculture)    reproduce(fertilityx, fertilityy)    for person in peopleDictionary:        if person.age > 80:            peopleDictionary.remove(person)        else:            person.age +=1        print(len(peopleDictionary))
Let’s see what is going on here. The runYear function takes agriculture, food, fertilityx and fertilityy. It runs the harvest function to find out if the food will leave some people to starve to death, or if there is enough to be stored. We then perform the reproduce function, so that 20% of women between 18–35 give birth to a baby.
Next, we cycle through the people dictionary and remove anyone who is over 80. The rest of the population increases by 1 year of age. If you wanted to improve the simulator, you could make it more random which age you die at.
Finally, we print out the total population. To keep the runYear function going, we’ll include this line just below where we initiated the program through beginSim:
while len(peopleDictionary)<100000 and len(peopleDictionary) > 1:    runYear(food, agriculture, fertilityx, fertilityy)
This just ensures that the program stops when the population reaches 0 or 100,000.
Congratulations! We have population growth, the basis for our simulator. If you import this data into Excel, we get this graph:
This looks just like our graph from the simple first program of the article. It makes sense based on the current code, but it’s not very interesting. Let’s spice it up a little.
Remember those variables, infantMortality and disasterChance we talked about? Let’s factor those in.
20% of women give birth every year, according to our simulation, but 25% of those infants die. So let’s update our reproduce function and include this:
if random.randint(0,5)==1:     if random.randint(0,100)>infantMortality:           peopleDictionary.append(Person(0))
Note: You’ll also want to update the runYear function, and also the line where we call it, so it can accept the infantMortality variable. Otherwise, you will get the error “infantMortality is not defined”.
As you can see, instead of giving each woman a 1 in 5 chance of giving birth, even if she does give birth, there is a 25% chance of the baby dying. You can see from our random clause that any random numbers generated between 0–100 that are below 25% = baby dies, any above = baby lives. If infantMortality is high, then very few of the infants will survive. Ideally, you want to get it down from 1% to 2%.
When we run the simulation with a 25% infant mortality rate, the results are quite gloomy and inaccurate:
Instead, let’s allow infant mortality rates to decrease over time, as healthcare gets better. You’d think after a hundred years they would make some advances, right?
To keep the infantMortality variable updated, we’ll make this change to where we call the runYear function:
infantMortality = runYear(food, agriculture, fertilityx, fertilityy, infantMortality)
That way, we can make a change to the infantMortality rate every year, and it’ll be returned from the runYear function so it can be used for the next year!
We’ll then add these two lines to the runYear function:
infantMortality *= 0.985return infantMortality
This is also really simple. We’re decreasing the infantMortality by 1.5% every year, and returning it so the updated mortality rate can be put into the function for the subsequent year.
Run the program and we get these results from the data in Excel:
Last but not least, we’ll add in our disasterChance. I feel like the 25% initial infant mortality rate will be a bit unreasonable for this simulation, so I’m setting it to 5%, and then we’re ready to add in disasterChance, right above where we print out the population:
if random.randint(0,100)<disasterChance:        del peopleDictionary[0:int(random.uniform(0.05,0.2)*len(peopleDictionary))]
This is a rather long clause, but I’ll try to explain it. If the random number between 0 and 100 falls within the disasterChance range (for us, that is 10%), then a disaster is initiated. We generate a random number to figure out how much damage we’ll do. It’ll fall between 5% and 20%. We’ll multiply this by the length of the people dictionary to get a 5–20% slice of the population. We’ll then cut that slice out (effectively killing 5–20% of our population).
Once again, you’ll want to update the line where we call the runYears function so it sends the disasterChance to the function. Those are all the remaining changes and updates you must make to finish the simple population simulator!
Let’s run it:
It’s complete! You can see that about every ten years, there is some kind of significant disaster that kills 5–20% of the population. These disasters are barely noticeable for the first seven hundred years, but they get very exaggerated thereafter.
import randomstartPopulation = 50infantMortality = 5youthMortality = 45agriculture = 5disasterChance = 10fertilityx = 18fertilityy = 35food = 0peopleDictionary = []class Person:    def __init__(self, age):        self.gender = random.randint(0,1)        self.age = age        self.pregnant = 0def harvest(food, agriculture):    ablePeople = 0    for person in peopleDictionary:        if person.age > 8:            ablePeople +=1food += ablePeople * agricultureif food < len(peopleDictionary):        del peopleDictionary[0:int(len(peopleDictionary)-food)]        food = 0    else:        food -= len(peopleDictionary)def reproduce(fertilityx, fertilityy, infantMortality):    for person in peopleDictionary:        if person.gender == 1:            if person.age > fertilityx:                if person.age < fertilityy:                    if random.randint(0,5)==1:                        if random.randint(0,100)>infantMortality:                            peopleDictionary.append(Person(0))def beginSim():    for x in range(startPopulation):        peopleDictionary.append(Person(random.randint(18,50)))def runYear(food, agriculture, fertilityx, fertilityy, infantMortality, disasterChance):    harvest(food, agriculture)    for person in peopleDictionary:        if person.age > 80:            peopleDictionary.remove(person)        else:            person.age +=1        reproduce(fertilityx, fertilityy, infantMortality)if random.randint(0,100)<disasterChance:        del peopleDictionary[0:int(random.uniform(0.05,0.2)*len(peopleDictionary))]    print(len(peopleDictionary))    infantMortality *= 0.985    return infantMortalitybeginSim()while len(peopleDictionary)<100000 and len(peopleDictionary) > 1:    infantMortality = runYear(food, agriculture, fertilityx, fertilityy, infantMortality, disasterChance)
While this is a simple and incomplete model, you now know how to build a simulation! To take this to the next level, you can update and advance this program to include way more factors like health conditions, social class, urbanization, healthcare, various kinds of disasters, geolocation, etc. Overall, I hope this inspires you to build your own simulations for the pandemic, population growth, or whatever else you can think of!
Not sure what to read next? I’ve picked another article for you:
towardsdatascience.com
If you enjoyed this, follow me on Medium for more
Sign up for my email list here!
Let’s connect on LinkedIn
Interested in collaborating? Check out my website.
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. See our Reader Terms for details. To learn about the coronavirus pandemic, you can click here.