# Genetic algorithm in Python that plots its evolution

After programming in Haskell for a while, I've gotten attached to a functional style. This is clearly evident in the code for my genetic algorithm.

Could you provide me with some hints as to how I can make this code more pythonic? By that, I mean provide some method of organisation rather than throwing a bunch of functions around. Any other recommendations are also welcome.

import copy
import matplotlib.pyplot as pyplot
import random

def create_member(genes):
return (sum(genes), genes)

def shuffle(pool):
pool_shuffled = copy.deepcopy(pool)
random.shuffle(pool_shuffled)
return pool_shuffled

def calculate_pool_fitness(pool):
return sum([member for member in pool])

def calculate_member_fitness(member):
return sum(member)

def recalculate_fitneses(pool):
return [(calculate_member_fitness(member), member) for member in pool]

def select_members_roulette(pool, count):
selection = []
while len(selection) < count:
member = select_member_roulette(pool)
selection.append(copy.deepcopy(pool[member]))
return selection

def select_member_roulette(pool):
drop = random.randint(0, calculate_pool_fitness(pool))
total_fitness = 0
for member in range(0, len(pool)):
total_fitness += pool[member]
if total_fitness >= drop:
return member

def mutate_gene(gene, rate=1):
return 1 - gene if random.random() <= rate else gene

def mutate_genes(genes, rate=1):
return [mutate_gene(gene, rate) for gene in genes]

def mutate_member(member, rate=1):
return member, mutate_genes(member, rate)

def mutate_pool(pool, rate=1):
return [mutate_member(member, rate) for member in pool]

def create_random_gene():
return random.choice([0, 1])

def create_random_genes(size):
return [create_random_gene() for _ in range(size)]

def crossover_genes(mother, father, rate=1):
if random.random() <= rate:
split = random.randint(1, len(mother))
daughter = mother[:split] + father[split:]
son = father[:split] + mother[split:]
else:
daughter = copy.deepcopy(mother)
son = copy.deepcopy(father)
return daughter, son

def crossover_members(mother, father, rate=1):
daughter_genes, son_genes = crossover_genes(mother, father)
return [(mother, daughter_genes), (father, son_genes)]

def crossover_pool(pool, rate=1):
children = []

# select every two elements for crossover
for mother, father in zip(pool[::2], pool[1::2]):
children.extend(crossover_members(mother, father, rate))

return children

def generate_pool(size, gene_size):
pool = []
for member in range(0, size):
genes = create_random_genes(gene_size)
pool.append(create_member(genes))
return pool

def evolve(pool, rate_crossover=0.9, rate_mutation=0.01):
successors = copy.deepcopy(pool)

# perform roulette selection whilst keeping best member
member_alpha = copy.deepcopy(max(successors, key=lambda member: member))
successors = select_members_roulette(pool, len(pool) - 1)
successors.append(member_alpha)

successors = shuffle(successors)
successors = crossover_pool(successors, rate_crossover)
successors = mutate_pool(successors, rate_mutation)
successors = recalculate_fitneses(successors)
return successors

def main():
random.seed
pyplot.figure(figsize=(14, 8), dpi=400)
axgraph=pyplot.subplot(111)
pool_size = 50
gene_size = 50
generations = 100
pool = generate_pool(pool_size, gene_size)
for generation in range(0, generations):
pool = evolve(pool)
axgraph.scatter(generation, sum([member for member in pool]))
pyplot.grid(True)
pyplot.axis([0, generations, 0, pool_size * gene_size])
pyplot.savefig('genetic_algorithms.png')

main()

if __name__ == '__main__':
main()


I have been facing the same issue for a few months, even though I didn't write that much functional code before switching. Please take my comments with a grain of salt even if I say "do this" instead of "this might help but I'm not sure".

• Use dictionaries instead of tuples:

def create_member(genes):
return {'sum': sum(genes), 'genes': genes}

• Use generators instead of list-expressions:

def mutate_pool(pool, rate=1):
for member in pool:
yield mutate_member(member, rate)


Yay, lazy lists! (kind of...)

• Write code for the next person which will be reading it. You shouldn't put comments only where you had some trouble ("[::2] selects one gene out of two") but try to make any function easy to understand and modify in isolation.

• Prefer longer functions and document them using docstrings. A nice function name is usually not enough for documentation:

def mutate_genes(genes, rate):
"""
Given a list of genes, flip some of them according to rate.
"""
for gene in genes:
yield 1 - gene if random.random() <= rate else gene

def mutate_pool(pool, rate=1):
"""
Returns a new pool of member where every bit in every gene could have been
flipped according to rate.
"""
for member in pool:
yield member, mutate_genes(member, rate)


Of course those examples are a bit artificial but they do feel more pythonic. You don't have to document every function, just think about your readers. :)

• Read nice Python code. I'd recommend Django and scikit-learn (Those are links to specific files that I chose randomly.)

I think it's worthwhile to try to force yourself to write pythonic code for some time so that you can tell what's nice in "the Python way" and what's actually better in a functional style.

• random.seed is a bug: add parentheses to actually call the function.
• Use collections.namedtuple so you can write member.fitness instead of the less readable member
• copy.deepcopy should not be necessary in a functional approach
• Keeping the genes in a tuple instead of a list would make the member tuple fully immutable, in line with the functional approach. This eliminates the need to deep-copy objects as you can safely copy just references.

After these changes create_member becomes like this:

import collections
Member = collections.namedtuple("Member", "fitness genes")

def create_member(genes):
genes = tuple(genes)
return Member(sum(genes), genes)


In some places you create members without calling create_member. Be sure to change them, for example:

def mutate_member(member, rate=1):
return create_member(mutate_genes(member.genes, rate))

• With immutable members there is no need to recalculate fitnesses. You can delete such functions.