Maybe you could try to encode the cost and the rating using binary numbers.
For example, let's assume the most expensive plane ticket you can get is $16384, you could store that in 14 bits (2^14 = 16384) and the rating is a number from 0 to 10 so you can store that in 4 bits so in total you can store your individuals using 18 bits.
Now you need a function to decode it:
def decode_individual(individual):
decoded_individual = ['', '']
# Decode cost (14 bits)
for i in range(0, 14):
decoded_individual[0] += str(individual[i])
# Decode rating (4 bits)
for i in range(1, 3):
decoded_individual[0] += str(individual[13 + i])
return tuple(map(lambda x: int(x, 2), decoded_individual))
You need to set up your fitness functions for a multi-objective problem, i.e. you need to provide some weights for each function that is positive if you are trying to maximize the function and negative if you are trying to minimize it. In your case, I guess you are trying to maximize de rating and minimize the cost so you could set it up as follows:
creator.create('Fitness', base.Fitness, weights=(1.0, -0.5,))
creator.create('Individual', list, fitness=creator.Fitness)
Your fitness methods should return the result of the functions you are trying to maximize and minimize in the same order as specified in the weights:
def function_cost(individual):
decoded_individual = decode_individual(individual)
return decoded_individual[0]
def function_rating(individual):
decoded_individual = decode_individual(individual)
return decoded_individual[1]
def fitness(individual):
return (function_cost(individual), function_rating(individual)),
Then, instead of registering 2 fitness functions like in your example, register just one:
toolbox.register('evaluate', fitness)
Configure DEAP to use binary data:
toolbox.register('attrBinary', random.randint, 0, 1)
toolbox.register('individual', tools.initRepeat, creator.Individual, toolbox.attrBinary, n=18) # Here you need to specify the number of bits you are using
toolbox.register('population', tools.initRepeat, list, toolbox.individual)
# Register the evaluation function (was named fitness in this example)
toolbox.register('evaluate', fitness)
# Configure your mate and mutation methods, e.g.
toolbox.register('mate', tools.cxTwoPoint)
toolbox.register('mutate', tools.mutFlipBit, indpb=0.15)
Your selection method must support multi-objective problems, NSGA2 as pointed out in your question can be used:
toolbox.register('select', tools.selNSGA2)
Then run the algorithm, you can try different values for the number of individuals (population) the number of generations and the ratings of mating and mutation:
num_pop = 50
num_gen = 100
cx_prob = 0.7
mut_prob = 0.2
best = []
for gen in range(num_gen):
offspring = algorithms.varAnd(population, toolbox, cxpb=cx_prob, mutpb=mut_prob)
fits = toolbox.map(toolbox.evaluate, offspring)
for fit, ind in zip(fits, offspring):
ind.fitness.values = fit
population = toolbox.select(offspring, k=len(population))
top = tools.selBest(population, k=1)
fitness = fitness(top[0])
print(gen, fitness, decode_individual(top[0]), top[0])
best.append(fitness[0])
You may also want to display the best individuals of each generation in a graph:
x = list(range(num_gen))
plt.plot(x, best)
plt.title("Best ticket - Cost / Rating")
plt.show()
Haven't tested this myself and I got largely inspired by some exercise I did at University so hopefully it will work for you.