Genetic Drift Simulation¶
August 22, 2025
Natural selection is not the only mechanism acting in evolution—other mechanisms, like mutations, gene flow, and genetic drift, contribute alongside natural selection to the evolutionary course of a taxon. Genetic drift is the change in the allele frequency in a population due to random chance. Even supposing a scenario in which every individual in a population has equal chance of producing offspring, some will fail to produce offspring due to chance encounters with predators, natural disasters, or other obstacles to reproduction.
Given a small population of beetles with 5 blue (BB/Bb) and 5 yellow (bb) beetles: if, in a freak rainstorm, 1 blue and 4 yellow beetles are killed before reproduction, then the next generation will have a much greater proportion the blue allele (B) and a much smaller proportion of yellow allele (b). Over the following generations, if the carriers of the yellow allele produce unusually many offspring by random chance, the proportion of the yellow allele may recover, but random chance favors the already abundant blue allele to continue to be passed down in greater proportions compared to the yellow alleles. The random variation in reproductive success tends to fix or lose an allele—an allele becomes fixed if it comprises 100% of the color alleles in the population—over enough generations, in an idealized population without mutation and gene flow.
I wanted to see the process of genetic drift for myself, through a simulation. I first create a barebones diploid organism with only a single gene:
import random
class Individual:
def __init__(self, maternal, paternal):
self.alleles = [maternal, paternal]
def produce_zygote(self):
return random.choice(self.alleles)
Assuming a small population of size 20, I simulate the random variation in reproductive success by randomly selecting 10 of the individuals to reproduce—the other unlucky half of the population is taken out of the gene pool. I assume that the organism mates for life with a single partner, so I set them up into pairs, with each pair produce 4 offspring to reach a population of 20 again.
def simulate_drift(gen):
reproducers = random.choices(gen, k=10)
pairs = []
while len(reproducers) > 1:
maternal_index = random.randrange(0, len(reproducers))
maternal_indiv = reproducers.pop(maternal_index)
paternal_index = random.randrange(0, len(reproducers))
paternal_indiv = reproducers.pop(paternal_index)
pairs.append((maternal_indiv, paternal_indiv))
progeny = []
for pair in pairs:
mother = pair[0]
father = pair[1]
for i in range(4):
offspring = Individual(mother.produce_zygote(), father.produce_zygote())
progeny.append(offspring)
return progeny
Then, I create a small population of 20 individuals as the first generation, and run the simulation for 15 generations.
def simulate_drift_multiple_gen(n):
allele_choices = ["A", "a"]
gen_1 = []
for i in range(20):
gen_1.append(Individual(random.choice(allele_choices), random.choice(allele_choices)))
all_gens = [gen_1]
for i in range(n-1):
gen = simulate_drift(gen_1)
gen_1 = gen
all_gens.append(gen)
return all_gens
all_gens = simulate_drift_multiple_gen(15)
print(" ".join(["".join(i.alleles) for i in all_gens[-1]]))
AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA
Pretty neat. The population fixed to the allele A, but it can fix to the allele a with equal probability in my simulation. Let's take a look at the allele frequencies across generations:
def gen_proportions(all_gens):
proportions_A = []
for gen in all_gens:
count_A = 0
for indiv in gen:
count_A += indiv.alleles.count("A")
proportions_A.append(count_A/40)
return proportions_A
import matplotlib.pyplot as plt
proportions_A = gen_proportions(all_gens)
plt.plot(list(range(1,len(proportions_A)+1)), proportions_A)
plt.xticks(list(range(1,len(proportions_A)+1)))
plt.xlabel("Generations")
plt.ylabel("Proportion of allele A")
plt.show()
The population started out with an allele A frequency of 50%, and fixed to 100%. What if we ran this simulation many times?
for i in range(10):
plt.plot(list(range(1,16)), gen_proportions(simulate_drift_multiple_gen(15)))
plt.xticks(list(range(1,16)))
plt.xlabel("Generations")
plt.ylabel("Proportion of allele A")
plt.show()
Many, but not all, fix. What if we increased the number of generations?
for i in range(10):
plt.plot(list(range(1,51)), gen_proportions(simulate_drift_multiple_gen(50)))
plt.xlabel("Generations")
plt.ylabel("Proportion of allele A")
plt.show()
Eventually, most simulations fix to allele A or a. Remarkably, there are simulations (red & purple) that veer close to 100% allele A before drastically changing course to fix to allele a. It goes to show the profound effect that random chance can have on the evolutionary outcome of an organism.