Genetic Algorithms: Complete Guide — Principles, Examples, and Python Implementation
Summary — Genetic algorithms are optimization techniques inspired by natural evolution. A population of candidate solutions evolves through selection, crossover, and mutation operators, progressively converging toward optimal or near-optimal solutions. Applicable to continuous, discrete, or combinatorial problems, they excel where classical gradient methods fail.
Mathematical principle
A genetic algorithm models an evolutionary process in four cyclic phases:
1. Individual representation: Each candidate solution is encoded as a chromosome — a vector (binary, integer, real, or permutation) of genes. For example, to optimize a function f(x,y), the chromosome [x, y] contains two real genes.
2. Fitness (evaluation function): Each individual is assigned a fitness score, which measures the quality of its solution. For a maximization of f, fitness = f(x). For a minimization problem, fitness = -f(x) or fitness = 1/(1+f(x)).
3. Evolutionary operators:
a) Selection: Individuals are chosen for reproduction according to their fitness. Two methods dominate:
– Tournament selection: Draw k individuals at random, keep the best. Tournament size = selective pressure.
– Roulette wheel (fitness-proportionate): Probability p_i = fitness_i / Σ fitness_j. The best have more chances but average ones can still be selected.
b) Crossover: Two parents produce children by combining their chromosomes.
– Single-point: Cut chromosomes at a random point, swap halves.
– Two-point: Two cuts, swap the middle segment.
– Blending (real-valued): Child = α·parent1 + (1-α)·parent2.
c) Mutation: Random modification of one or more genes with probability p_m (typically 1-5%). For real-valued chromosomes: add Gaussian noise. For binary: flip the bit.
4. Replacement and elitism: The new generation replaces the old one. Elitism consists of keeping the best individuals intact, ensuring that the best fitness never decreases from one generation to the next.
Convergence: The algorithm is not guaranteed to find the global optimum, but converges asymptotically to it under theoretical conditions (Holland’s schema theorem). In practice, we stop after a fixed number of generations or when the best fitness plateaus.
Intuition
Imagine breeding racing pigeons. You start with a diverse group — some fly fast, others are enduring, others good navigators. Your goal: create the perfect pigeon.
Each generation, you:
1. Select the best breeders (those with the best performance)
2. Cross their characteristics — a fast pigeon × an endurance pigeon might produce a pigeon that is fast AND enduring
3. Mutate occasionally — a pigeon is born with an unexpected characteristic (better night vision?) that could be an advantage
After dozens of generations, you get pigeons far better than the best individual from the initial generation. That’s exactly what a genetic algorithm does — except instead of breeding pigeons over years, it evaluates millions of solutions in seconds on a processor.
The key: unlike gradient methods that follow a local slope, genetic algorithms simultaneously explore multiple regions of the search space. Mutation introduces novelty, crossover combines good ideas, and selection eliminates bad combinations.
Python implementation
Example 1: From-scratch implementation — function optimization
import numpy as np
import matplotlib.pyplot as plt
def fitness(x):
return np.sin(x*3) + np.cos(x*2) + x/5 # multi-modal function
def genetic_algorithm(pop_size=100, n_gen=50, x_min=-5, x_max=5,
mut_prob=0.1, elite_frac=0.1, seed=42):
np.random.seed(seed)
# Uniform random initialization
population = np.random.uniform(x_min, x_max, pop_size)
best_history = []
avg_history = []
for gen in range(n_gen):
# Evaluation
scores = fitness(population)
# History
best_history.append(scores.max())
avg_history.append(scores.mean())
# Tournament selection
def tournament(pop, scores, k=3):
idx = np.random.choice(len(pop), k)
return pop[idx[scores[idx].argmax()]]
# Elitism
n_elite = max(1, int(pop_size * elite_frac))
elite_idx = np.argsort(scores)[-n_elite:]
new_pop = list(population[elite_idx])
# Reproduction
while len(new_pop) < pop_size:
p1 = tournament(population, scores, k=3)
p2 = tournament(population, scores, k=3)
# Blending crossover
alpha = np.random.uniform(0.2, 0.8)
child = alpha * p1 + (1 - alpha) * p2
# Gaussian mutation
if np.random.random() < mut_prob:
child += np.random.normal(0, 0.5)
child = np.clip(child, x_min, x_max)
new_pop.append(child)
population = np.array(new_pop)
best_idx = fitness(population).argmax()
best_sol = population[best_idx]
best_fit = fitness(best_sol)
print(f"Optimal solution: x = {best_sol:.4f}, f(x) = {best_fit:.4f}")
return best_sol, best_fit, best_history, avg_history
# Execution
solution, optimum, best_hist, avg_hist = genetic_algorithm()
# Convergence visualization
plt.figure(figsize=(8, 4))
plt.plot(best_hist, 'r-', label='Best fitness', linewidth=2)
plt.plot(avg_hist, 'b--', label='Average fitness', linewidth=1)
plt.title('Genetic algorithm convergence')
plt.xlabel('Generation')
plt.ylabel('Fitness')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('genetic_convergence.png', dpi=150)
print(f"Chart saved")
Example 2: Solving the Traveling Salesman Problem (TSP)
import numpy as np
# Cities (coordinates)
cities = np.array([
[60, 200], [180, 200], [80, 180], [140, 180],
[20, 160], [100, 160], [200, 160], [140, 140],
[40, 120], [100, 120], [180, 100], [60, 80],
[120, 80], [180, 60], [20, 40], [100, 40],
[200, 40], [20, 20], [60, 20], [160, 20],
])
n_cities = len(cities)
def distance_matrix(cities):
n = len(cities)
dist = np.zeros((n, n))
for i in range(n):
for j in range(n):
dist[i, j] = np.linalg.norm(cities[i] - cities[j])
return dist
dist = distance_matrix(cities)
def tour_length(route):
return sum(dist[route[i], route[i+1]] for i in range(len(route)-1)) + dist[route[-1], route[0]]
def ga_tsp(pop_size=50, n_gen=200, mut_prob=0.3):
np.random.seed(42)
# Initial population of permutations
population = [np.random.permutation(n_cities) for _ in range(pop_size)]
best_hist = []
for gen in range(n_gen):
scores = [-tour_length(r) for r in population] # negative for maximization
best_hist.append(-min(tour_length(r) for r in population))
# Elitism
n_elite = 5
elite_idx = np.argsort(scores)[-n_elite:]
new_pop = [population[i] for i in elite_idx]
# Tournament selection
def tournament(idx_list):
k = min(3, len(idx_list))
chosen = np.random.choice(idx_list, k, replace=False)
return chosen[np.argmax([scores[i] for i in chosen])]
all_idx = list(range(pop_size))
while len(new_pop) < pop_size:
p1 = population[tournament(all_idx)].copy()
p2 = population[tournament(all_idx)].copy()
# Order Crossover (OX)
start, end = sorted(np.random.choice(n_cities, 2, replace=False))
child = [None] * n_cities
child[start:end+1] = p1[start:end+1]
remaining = [g for g in p2 if g not in child[start:end+1]]
j = 0
for i in range(n_cities):
if child[i] is None:
child[i] = remaining[j]
j += 1
# Swap mutation
if np.random.random() < mut_prob:
i1, i2 = np.random.choice(n_cities, 2, replace=False)
child[i1], child[i2] = child[i2], child[i1]
new_pop.append(np.array(child))
population = new_pop
best_route = min(population, key=tour_length)
best_dist = tour_length(best_route)
print(f"Best TSP distance: {best_dist:.2f}")
return best_route, best_dist, best_hist
route, distance, history = ga_tsp()
Hyperparameters
| Hyperparameter | Typical value | Description | Impact |
|---|---|---|---|
population_size |
50-200 | Number of individuals per generation | Larger = better exploration, slower |
n_generations |
50-500 | Number of evolution iterations | More = better convergence, more costly |
crossover_probability |
0.7-0.9 | Probability of crossing two parents | Too low = slow evolution, too high = loss of good individuals |
mutation_probability |
0.01-0.1 | Probability of mutating each gene | Too low = local stagnation, too high = random search |
mutation_rate |
0.1-1.0 | Mutation amplitude (Gaussian noise) | Controls the size of exploration steps |
tournament_size |
2-5 | Selection tournament size | Larger = more selective pressure, risk of premature convergence |
elitism_count |
1-5 | Number of individuals kept intact | Guarantees non-degradation of best fitness |
Advantages of genetic algorithms
- No gradient required: They work on non-differentiable, discontinuous, noisy, or black-box functions — where gradient methods are inapplicable.
- Global search: Multi-point exploration reduces the risk of getting trapped in a local optimum, unlike gradient descent.
- Extreme flexibility: Applicable to continuous, discrete, combinatorial problems (TSP, scheduling), and even to neural network design (neuroevolution).
- Natural parallelization: Fitness evaluation is independent — perfectly parallelizable on clusters or GPUs.
- Conceptual simplicity: The core algorithm fits in a few lines of code, making it accessible and easy to adapt.
Limitations of genetic algorithms
- No optimality guarantee: No bound on the quality of the final solution — we get a “good” solution, not necessarily the best.
- Hyperparameter tuning: Performance depends heavily on the choice of population size, mutation and crossover rates — few universal rules.
- Computational cost: Thousands of fitness evaluations are needed — problematic if each evaluation is expensive (CFD simulation, network training).
- Premature convergence: If selective pressure is too strong, the population quickly converges to a local optimum and genetic diversity collapses.
- No real-time response: The iterative process (generations) is inherently sequential — unsuitable for decisions that must be made in milliseconds.
4 concrete use cases
1. Delivery route optimization (logistics)
Delivery companies use genetic algorithms to optimize the routes of their truck fleets — the famous Traveling Salesman Problem (TSP) and its variants (VRP, CVRP). Each chromosome represents a delivery sequence, and fitness is the total distance or cost. Constraints (vehicle capacity, time windows) are incorporated into the fitness function as penalties.
2. Aerospace component design
Airbus and Boeing use evolutionary approaches to optimize the shape of airplane wings. The chromosome encodes the geometry (curvature, thickness, wingspan), and fitness is the lift-to-drag ratio evaluated by CFD simulation. The flexibility of GAs allows exploring counter-intuitive shapes that engineers would not have considered.
3. Machine learning — feature selection and model tuning
Genetic algorithms are used to select the most informative variables in a dataset (each bit of the chromosome = inclusion or exclusion of a feature). Combined with a classifier as the fitness score, they often find feature subsets that perform better than classical forward/backward methods. Frameworks like TPOT even automate the design of entire ML pipelines through a genetic approach.
4. Production scheduling and timetabling
In manufacturing, job scheduling on machines (job-shop scheduling) is an NP-hard problem. Genetic algorithms find near-optimal schedules that minimize total production time and waiting times, while respecting resource and sequencing constraints. Each chromosome encodes an execution order of tasks.
Best practices
- Start with standard parameters: population=100, crossover=0.8, mutation=0.05, elitism=5 — then refine.
- Monitor diversity: If the variance of fitness in the population drops sharply, it is a sign of premature convergence — increase mutation or population size.
- Use elitism systematically: Keeping at least the best individual avoids regression between generations.
- Hybridization: Combine a GA with a local method (gradient, Nelder-Mead) to refine the best solutions — this is the “memetic algorithm” approach.
- Reproducibility: Fix the random seed so that results are reproducible, especially in research or production.
Conclusion
Genetic algorithms represent a powerful and elegant approach for solving complex optimization problems where classical methods fail. Their strength lies in their ability to explore vast search spaces without assumptions about the problem structure — no differentiability, convexity, or continuity required.
They are not, however, a universal solution: for convex, well-conditioned problems, gradient descent will always be faster and more precise. Genetic algorithms shine where the solution landscape is rugged, discontinuous, or combinatorial.
See also
- Multiplying character strings with Python
- Master the Art of Reverse Engineering in Python: Complete Guide for Beginners and Experts

