Python Genetic Algorithm Guide

A genetic algorithm is an optimization technique inspired by evolution. It keeps a population of candidate solutions, scores each candidate with a fitness function, selects better candidates, and creates new candidates through crossover and mutation.

Genetic algorithms are useful when a search space is large, rough, or hard to optimize with direct formulas. They are common in scheduling, feature selection, parameter tuning, route planning, and toy optimization problems where a good solution is enough.

They do not guarantee the perfect answer. Instead, they explore many candidates and try to improve the population over time. That makes them practical for problems where checking a candidate is easier than calculating the best candidate directly.

The DEAP documentation and PyGAD documentation cover established libraries. This guide uses small pure-Python examples so the moving parts are easy to see. For related array operations, see NumPy repeat.

Create A Candidate

A candidate is one possible answer. For a simple binary problem, represent each candidate as a list of zeros and ones.

import random

def make_candidate(length):
    return [random.randint(0, 1) for _ in range(length)]

candidate = make_candidate(8)

print(candidate)

Real projects may use numbers, strings, routes, model settings, or custom objects. The representation should make crossover and mutation possible.

Choose the representation carefully because every later step depends on it. A candidate should be easy to score, copy, combine, and change in small ways.

Score Fitness

The fitness function decides how good a candidate is. In this toy example, more ones means a better score.

def fitness(candidate):
    return sum(candidate)

print(fitness([1, 0, 1, 1]))
print(fitness([0, 0, 1, 0]))

The fitness function is the most important part of the algorithm. If it rewards the wrong behavior, the search will move in the wrong direction.

Good fitness functions are consistent and cheap enough to run many times. If scoring one candidate is expensive, population size and generation count become important performance controls.

Select Parents

Selection chooses candidates that will produce the next generation. A simple approach is to keep the highest-scoring candidates.

def select_parents(population, count):
    ranked = sorted(population, key=fitness, reverse=True)
    return ranked[:count]

population = [
    [1, 0, 1, 0],
    [1, 1, 1, 0],
    [0, 0, 1, 0],
]

print(select_parents(population, 2))

Selection pressure controls how strongly the algorithm favors current winners. Too much pressure can reduce diversity too early.

Keeping some variety in the population helps the algorithm explore. If every candidate becomes nearly identical too quickly, crossover and mutation have less useful material to work with.

Apply Crossover

Crossover combines two parents into a child. One-point crossover swaps the tail of two lists after a split point.

def crossover(left, right):
    point = len(left) // 2
    return left[:point] + right[point:]

parent_a = [1, 1, 0, 0, 1, 0]
parent_b = [0, 0, 1, 1, 0, 1]

print(crossover(parent_a, parent_b))

Crossover works best when useful building blocks can be shared between candidates. If the representation is poor, crossover may mostly create broken children.

For some problems, a different crossover method is better than a simple halfway split. The operator should respect the structure of the candidate, such as routes, schedules, or parameter ranges.

Apply Mutation

Mutation makes small random changes. It helps the search escape local patterns and keeps the population diverse.

import random

def mutate(candidate, rate=0.1):
    child = candidate[:]
    for index, bit in enumerate(child):
        if random.random() < rate:
            child[index] = 1 - bit
    return child

print(mutate([1, 1, 1, 1], rate=0.5))

A mutation rate that is too high can turn the search into random noise. A rate that is too low may make the population converge too early.

Mutation is the exploration step. It introduces new possibilities that are not present in the selected parents, which can help the search escape a weak local result.

Run A Small Genetic Algorithm

This compact loop creates a population, selects parents, creates children, mutates them, and tracks the best candidate.

import random

def run(length=8, size=20, generations=10):
    population = [make_candidate(length) for _ in range(size)]

    for _ in range(generations):
        parents = select_parents(population, size // 2)
        children = []

        while len(children) < size:
            left, right = random.sample(parents, 2)
            child = mutate(crossover(left, right), rate=0.05)
            children.append(child)

        population = children

    return max(population, key=fitness)

best = run()
print(best, fitness(best))

This example is intentionally small. Production genetic algorithms need better selection, stopping rules, constraints, logging, and repeatable random seeds.

Stopping rules can use a fixed generation count, a target score, or a limit on generations without improvement. Logging the best score per generation helps show whether the search is still making progress.

Best Practices

Start by defining a clear fitness function and a representation that can be changed safely. Then choose population size, mutation rate, crossover method, and stopping rules. Track the best score over time so you can see whether the search is improving.

Use libraries such as DEAP or PyGAD when you need mature operators, constraints, parallel evaluation, or experiment tooling. Use a hand-written version only when learning, prototyping, or solving a small custom problem.

The reliable pattern is to keep the first version simple, test each operator independently, and measure whether each generation improves the score. A genetic algorithm is useful only when the fitness function and representation match the real optimization goal.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted