home
/
blog
/
algoritmo-genetico-sorteo

Un Algoritmo Genético para generar el Sorteo de Grupos de la Copa Mundial de la FIFA

25 minutos
Usamos un algoritmo genético para generar el sorteo de grupos de la Copa Mundial de la FIFA 2026, un problema de optimización combinatoria con restricciones.

Contenido

El sorteo de grupos de la Copa Mundial de la FIFA 2026

La Copa Mundial de la FIFA 2026 será la primera en disputarse entre 48 selecciones nacionales de todo el mundo. Estos 48 equipos deben distribuirse en 12 grupos y se reparten de forma particular a través de un sorteo que pone algunas restricciones interesante en la formación de estos grupos, gracias a estas el sorteo se puede llevar a cabo de forma rápida y sin problemas. Una vez hecho el sorteo queda determinado cómo se enfrentarán los equipos en la fase inicial.
En la edición de 2026, con 48 equipos participantes, el sorteo se vuelve aún más complejo debido a las múltiples restricciones que deben cumplirse[1].
Itinerario de las rondas eliminatorias del mundial según la FIFA. Esta configuración da lugar a la restricción de no enfrentamientos entre España y Argentina, o entre Francia e Inglaterra antes de la final, lo que afecta su asignación de grupo en el sorteo.

Fig: 1. Itinerario de las rondas eliminatorias del mundial según la FIFA. Esta configuración da lugar a la restricción de no enfrentamientos entre España y Argentina, o entre Francia e Inglaterra antes de la final, lo que afecta su asignación de grupo en el sorteo.

El problema al que nos enfrentamos al generar el sorteo de grupos es un problema de optimización combinatoria con restricciones. Solo hay un número limitado de combinaciones posibles que cumplen con todas las restricciones impuestas por la FIFA, a esto se le llama soluciones factibles.

Solución factible

Una solución factible es aquella que satisface la función objetivo y no viola ninguna de las restricciones del problema o al menos aquellas que son consideradas obligatorias.
En el caso de los grupos del mundial, la solución factible es una configuración del sorteo que cumple con todas las restricciones impuestas, como la distribución de confederaciones y el ranking de los equipos. La mayoría de las combinaciones posibles que se pueden hacer con 48 equipos, 4 bombos y 12 grupos no serán soluciones factibles.
Aquí radica la dificultad del problema, en encontrar un sorteo que sea válido para todas las restricciones del sorteo.
Este espacio de soluciones puede ser extremadamente grande y complejo, especialmente cuando se consideran todas las restricciones de confederaciones y de ranking de equipos. Por ejemplo, en el Mundial de 2026, con 48 equipos y múltiples restricciones, el número de configuraciones posibles del sorteo es astronómico. Vamos a calcularlo.

Combinaciones posibles

En el sorteo hay 42 equipos distribuidos en 4 bombos (recipientes) que van a ser distribuidos en 12 grupos (del A al L). De cada bombo se irán sacando los equipos de forma aleatoria hasta vaciar el bombo, de tal manera que cada grupo tendrá un equipo de cada bombo. Los anfitriones (Canadá, México y Estados Unidos) ya tienen asignado su grupo, y en cada bombo hay 12 equipos.
Orden de los equipos en los bombos. Los equipos anfitriones no participan del sorteo, al tener ya asignados de antemano sus respectivos grupos.

Fig: 2. Orden de los equipos en los bombos. Los equipos anfitriones no participan del sorteo, al tener ya asignados de antemano sus respectivos grupos.

Ahora calculemos todas las combinaciones posibles sin considerar las restricciones y usando factoriales (n!n!).
  • Bombo 1: 9!9!,
  • Bombos 2, 3, y 4: 12!12!
  • Esto es en total: 9!×12!×12!×12!=3.9881×10319! \times 12! \times 12! \times12! = 3.9881 \times 10^{31} combinaciones posibles.
¿Qué tan grande es este número? 39, 881, 724, 135, 495, 319, 399, 956, 480, 000, 000 combinaciones posibles de organizar los equipos en los grupos. Ignorando las restricciones.
Para tener una idea de la magnitud de este número, si pudiéramos evaluar cada sorteo generado por nuestro algoritmo de fuerza bruta en 0.05 segundos, evaluar todas las soluciones posibles tardaría 6.3×10226.3 \times 10^{22} años. Si el algoritmo hubiera iniciado en el Big Bang, aún no habría terminado, de hecho llevaría el 0.00000000002% del tiempo total.
De tal manera que un algoritmo de fuerza bruta, aquel que va generando todas las combinaciones posibles hasta dar con alguna que no viole las restricciones, es decir que sea factible, puede tardar mucho tiempo incluso millones de años. Por suerte existen algoritmos de optimización cuya búsqueda es mucho más eficiente que los de fuerza bruta y reducen este tiempo a un par de segundos. En el caso del sorteo de grupos de la Copa Mundial de la FIFA, usaremos un algoritmo genético.

Restricciones del sorteo

Las restricciones del sorteo definen lo que debe suceder al ir sacando los equipos de los bombos para asignarlos a los grupos. La FIFA ha redefinido las restricciones para este mundial de 2026 ya que es el primero con 42 selecciones y 12 grupos.
Primero hay que aclarar que en el sorteo los nombres de las 42 selecciones están divididas en cuatro recipientes (bombos) ordenadas según aparecen en el Ranking FIFA [2].
Las restricciones son las siguientes [3]:
  • Anfitriones: México se le asignará la posición A1 (primero del grupo A), a Canadá, la posición B1, y a Estados Unidos, la posición D1.
  • Bombo 1: Las otras nueve selecciones del bombo 1 se identificarán mediante nueve bolas del mismo color y se asignarán automáticamente a la posición 1 del grupo en el que resulten encuadradas.
  • Distribución uniforme: En cada grupo debe haber un equipo de cada bombo.
  • Confederaciones por grupo: Ningún grupo puede tener más de una selección de la misma confederación, excepto las de la UEFA, que pueden tener hasta dos selecciones en un mismo grupo.
  • Top 4 del ranking: Por último, las selecciones de España y Argentina por un lado, y Francia e Inglaterra por otro, no podrán enfrentarse hasta la final, por ser los cuatro primeros del ranking masculino de la FIFA a la fecha[2]. Esto restringe que se agreguen a cualquier grupo.
Distribución de los brackets o itinerarios de los equipos. Los equipos de un bracket que clasifiquen a la siguiente ronda competirán contra los equipos del otro bracket. España vs Argentina, ni Inglaterra vs Francia podrán enfrentarse hasta la final del mundial. Esto bajo el supuesto que cada una de esas selecciones clasifique de primera en su grupo.

Fig: 3. Distribución de los brackets o itinerarios de los equipos. Los equipos de un bracket que clasifiquen a la siguiente ronda competirán contra los equipos del otro bracket. España vs Argentina, ni Inglaterra vs Francia podrán enfrentarse hasta la final del mundial. Esto bajo el supuesto que cada una de esas selecciones clasifique de primera en su grupo.

Esto es lo que llamamos restricciones del problema. Intentar generar todas las combinaciones posibles y luego filtrar las que cumplen con las restricciones sería computacionalmente inviable como ya hemos visto.

¿Qué es un Algoritmo Genético?

Sin entrar en muchos detalles un algoritmos genético (GA en inglés) es un algoritmo de optimización inspirado en la reproducción de poblaciones de generación en generación y en los procesos biológicos de evolución. Están enmarcados dentro de los llamados algoritmos evolutivos.
Las bases teóricas y primeras ideas de los algoritmos genéticos pertenecen a los trabajos de John Holland y Lawrence Fogel en los años 60s[4][5].
Un algoritmo genético se compone de poblaciones de individuos que representan las soluciones candidatas al problema que se quiere resolver. Cada una de estas soluciones se evalúa mediante una función de costo, aptitud o fitness que mide qué tan buena es la solución. Luego, se seleccionan las mejores soluciones para reproducirse y generar nuevas soluciones mediante operaciones de cruce y mutación.
Este proceso se repite durante varias generaciones hasta que se alcance un criterio de terminación, como un número máximo de generaciones o una solución suficientemente buena.

Algoritmo Genético básico

En el ciclo de vida de un algoritmo genético hay valores que generalmente no cambian, llamados hiperparámetros, y su valor puede determinar el éxito o fracaso del algoritmo, ya que algunas metaheurísticas son muy sensibles a estos valores. En nuestro caso estos hiperparámetros son:
  • Tamaño de la población: Cuántos individuos (soluciones candidatas) van a conformar la población en cada generación.
  • Número de generaciones: Cuántas veces se va a repetir el ciclo de vida del algoritmo.
  • Probabilidad de cruce. Se establece en un 80%, ya que este valor suele ser efectivo para explorar el espacio de búsqueda.
  • Probabilidad de mutación. Establecida en un 20%, ya que este valor suele ser efectivo para mantener la diversidad genética sin alterar demasiado las soluciones existentes.
python
1POBLACION_TAM = 10 # Tamaño de la población
2GENERACIONES = 1000 # Número de generaciones
3PROB_CROSSOVER = 0.8  # 80% probabilidad de cruce
4PROB_MUTACION = 0.2  # 20% probabilidad de mutación
El ciclo de vida de un algoritmo genético básico se puede representar con el siguiente diagrama de flujo:

Población inicial

Los algoritmos genéticos forman parte del conjunto de metaheurísticas llamadas o clasificadas como poblacionales. En este tipo de algoritmos se evalúa un conjunto de soluciones en cada iteración el cual llamamos población. Cada población está conformada por individuos y cada uno de estos debe ser evaluado por la función de costo. En el mejor de los casos se llegará una solución cuando se encuentre un individuo que tenga el óptimo fitness y no incurra en ninguna violación a ninguna de las restricciones.
Para este problema cada individuo es un sorteo, como ya hemos dicho. Este individuo puede contener esquemas que violen las reglas y las restricciones del sorteo, y es la función de fitness la que castiga a ese individuo asignándole valores que cada vez lo alejen del óptimo (de su posibilidad de reproducirse).
Antes de definir la población es importante definir primero los individuos que la conforman. A esto le llamamos codificación del individuo.
Debemos encontrar una forma de representarlo en algoritmo y crearlo (codificación).
En el siguiente código se ve el proceso de creación de un individuo en un procedimiento parecido al sorteo, es decir se van asignando los equipos bombo por bombo, desde el primero hasta el cuarto. Ignorando la restricciones, los individuos pueden ser individuos válidos o no, según violen o no las restricciones. Es el algoritmo que debe determinar quién es el mejor o el óptimo de todos los individuos.
python
1def crear_individuo(lista_equipos):
2    # Crear un individuo (sorteo) válido inicial
3    bombos = {1: [], 2: [], 3: [], 4: []}
4    # Distribuir equipos por bombo
5    for eq in lista_equipos:
6        bombos[eq.bombo].append(eq)
7    # Asignar equipos a grupos
8    grupos = [[] for _ in range(12)]
9    for b in range(1, 5):
10        equipos = bombos[b]
11        # Mezclar para aleatoriedad
12        random.shuffle(equipos)
13        pendientes = []
14        for eq in equipos:
15            if eq.grupo_fijo:
16                # Asignar al grupo fijo y saltar (convierte letra del grupo a índice)
17                idx = ord(eq.grupo_fijo) - 65
18                grupos[idx].append(eq)
19            else:
20                pendientes.append(eq)
21        idx_p = 0
22        # Asignar restantes de este bombo
23        for g_idx in range(12):
24            if len(grupos[g_idx]) < b:
25                grupos[g_idx].append(pendientes[idx_p])
26                idx_p += 1
27    return grupos

Estructura del individuo

Ya hemos dicho que cada individuo del algoritmo genético representa una solución candidata al problema. Esto es un sorteo de grupos con los 12 grupos completos, sin importar si son válidos o no. Como cada sorteo está compuesto por grupos, cada grupo representa lo que llamamos un gen dentro del individuo.
Representación de un individuo como un sorteo completo de 12 grupos y 48 equipos. Se observa que este individuo no cumple con algunas restricciones, por ejemplo el grupo que comparten Catar y Jordania. Estas dos selecciones pertenecen a la misma confederación y no está permitido que tengan el mismo grupo.

Fig: 4. Representación de un individuo como un sorteo completo de 12 grupos y 48 equipos. Se observa que este individuo no cumple con algunas restricciones, por ejemplo el grupo que comparten Catar y Jordania. Estas dos selecciones pertenecen a la misma confederación y no está permitido que tengan el mismo grupo.

En la figura 4 se observa un ejemplo de lo que nuestro algoritmo tratará como un individuo, y lo que vemos es un sorteo completo asignando cada equipo a uno de los grupos. Este individuo en particular no es válido, ya que el grupo H tiene a Catar y Jordania, ambas selecciones pertenecen a la misma confederación (AFC) y no está permitido que compartan grupo. Además el grupo de Croacia, Noruega y la repesca europea no pueden compartir grupo ya que las tres selecciones pertenecen a la UEFA. Con las otras repescas no europeas sucede algo similar.
Para nuestro código, cada gen está formado por elementos de la clase Equipo que contiene la información relevante de cada selección nacional:
python
1class Equipo:
2    def __init__():
3        self.nombre = nombre
4        self.confederacion = confederacion  # String simple
5        self.bombo = bombo
6        self.grupo_fijo = grupo_fijo
7        self.ranking_top = ranking_top  # 1, 2, 3, 4 o None
8
9    def __repr__(self):
10        # Muestra info útil al imprimir
11        r =  if self.ranking_top else ""
12        f =  if self.grupo_fijo else ""
13        return
Por ejemplo un equipo del top 4 se representaría así:
python
1Equipo("España", ["UEFA"], bombo=1, ranking_top=1)
2Equipo("Argentina", ["CONMEBOL"], bombo=1, ranking_top=2)
Y los equipos anfitriones con grupo fijo:
python
1Equipo("México", ["CONCACAF"], bombo=1, grupo_fijo="A")
2Equipo("Canadá", ["CONCACAF"], bombo=1, grupo_fijo="B")
El resto de los equipos se instancia a la clase Equipo de forma más sencilla:
python
1Equipo("Alemania", ["UEFA"], bombo=1)
2Equipo("Ecuador", ["CONMEBOL"], bombo=2)
3Equipo("Panamá", ["CONCACAF"], bombo=3)
4# Y asi hasta completar el resto...

Función de costo

La forma más sencilla de evaluar un individuo de estas características, es creando una función objetivo que cuente las veces que se violan la restricciones, y en cada una de estas le adjudique una penalización a ese individuo, es decir una valoración desfavorable de tal manera que en el ciclo de vida del algoritmo este individuo quede rezagado o no pueda reproducirse y se extinga.
Evaluación de dos individuos. El individuo A tiene un fitness de 900, ya que viola la repetición de confederaciones en los grupos C,D,E. Además de no separar los itinerarios de España y Argentina.

Fig: 5. Evaluación de dos individuos. El individuo A tiene un fitness de 900, ya que viola la repetición de confederaciones en los grupos C,D,E. Además de no separar los itinerarios de España y Argentina.

El algoritmo busca minimizar este número. Un fitness de 1400 es un desastre; un fitness de 0 es un sorteo válido.
La función calcularFitness(sorteo) es el motor del algoritmo. Su objetivo es llegar a cero, esto significa que ha encontrado un individuo cuya evaluación no viola ninguna de las restricciones,
Al encontrar uno de estos el algoritmo ha encontrado el óptimo, es decir un sorteo válido. Aquí es donde entran en el juego las restricciones mencionadas anteriormente.
Aquí aplicamos Restricciones Duras (Hard Constraints):
  • Confederaciones: Ningún grupo puede tener más de un equipo de la misma confederación (salvo Europa, que permite dos).
python
1for j in range(len(grupo)):
2            for k in range(j + 1, len(grupo)):
3                equipo_a = grupo[j]
4                equipo_b = grupo[k]
5
6                # Intersección de conjuntos:
7                set_a = set(equipo_a.confederacion)
8                set_b = set(equipo_b.confederacion)
9                interseccion = set_a.intersection(set_b)
10
11                # Si hay intersección y NO es UEFA (porque UEFA permite 2), penalizamos
12                conflicto_real = [c for c in interseccion if c != "UEFA"]
13
14                if conflicto_real:
15                    penalizacion += PENALTY_CONF_LIMIT
  • Itinerarios: Los cabezas de serie del Top 4 deben estar distribuidos en llaves opuestas para no enfrentarse antes de la semifinal. Si el Top 1 y Top 2 caen en el mismo grupo, aplicamos una penalización masiva (+500).
python
1# Creamos un diccionario para saber dónde están los Top 4
2  ubicacion_tops = {}
3    # Para cada grupo obtenemos esa ubicación:
4    for equipo in grupo:
5            if equipo.ranking_top:
6                ubicacion_tops[equipo.ranking_top] = i
7
8    # Regla: Top 1 y Top 2 deben ir por lados opuestos del itinerario
9    if 1 in ubicacion_tops and 2 in ubicacion_tops:
10        it_1 = obtener_itinerario(ubicacion_tops[1])
11        it_2 = obtener_itinerario(ubicacion_tops[2])
12    # Se penaliza si se encuentran en el mismo lado de la llave
13    if it_1 == it_2:
14        penalizacion += PENALTY_ITINERARY
Veamos la función de costo completa. Su objetivo es evaluar cada grupo y contar las veces que se viola alguna restricción y penalizarla.
python
1def calcular_fitness(sorteo):
2    """
3    Calcula la penalización total de un sorteo (cromosoma).
4    0 = Sorteo válido.
5    return: penalizacion
6    """
7    penalizacion = 0
8
9    # Constantes de Penalización (Pesos)
10    PENALTY_CONF_LIMIT = 1000  # Conflicto de confederación (Grave)
11    PENALTY_UEFA_LIMIT = 500  # Más de 2 europeos
12    PENALTY_ITINERARY = 500  # Top seeds chocando antes de la final
13
14    # Rastrear dónde cayeron los Top seeds {ranking: indice_grupo}
15    ubicacion_tops = {}
16
17    # --- 1. Ciclo por Grupos ---
18    for i, grupo in enumerate(sorteo):
19
20        # A. Revisar límite de UEFA (Máximo 2)
21        # Contamos cuántos equipos tienen 'UEFA' en su lista de confederaciones
22        uefa_count = sum(1 for equipo in grupo if ["UEFA"] in equipo.confederacion)
23
24        if uefa_count > 2:
25            # Penalizamos para corregirlo
26            penalizacion += PENALTY_UEFA_LIMIT * (uefa_count - 2)
27
28        # B. Revisar choques de Confederaciones (NO UEFA)
29        # Usamos doble for para comparar todos contra todos en el grupo
30        for j in range(len(grupo)):
31            for k in range(j + 1, len(grupo)):
32                equipo_a = grupo[j]
33                equipo_b = grupo[k]
34
35                # Intersección de conjuntos:
36                # Si Equipo A es ['CONMEBOL'] y Equipo B (Repesca) es ['CONMEBOL', 'AFC']
37                # La intersección es {'CONMEBOL'} -> ¡Conflicto!
38                set_a = set(equipo_a.confederacion)
39                set_b = set(equipo_b.confederacion)
40                interseccion = set_a.intersection(set_b)
41
42                # Si hay intersección y NO es UEFA (porque UEFA permite 2), penalizamos
43                # Nota: Si ambos son UEFA, 'UEFA' estará en la intersección,
44                # pero eso ya lo controlamos arriba con uefa_count.
45                conflicto_real = [c for c in interseccion if c != "UEFA"]
46
47                if conflicto_real:
48                    penalizacion += PENALTY_CONF_LIMIT
49
50        # C. Guardar ubicación de Tops para la fase 2
51        for equipo in grupo:
52            if equipo.ranking_top:
53                ubicacion_tops[equipo.ranking_top] = i
54
55    # --- 2. Penalizaciones de Itinerario (Bracket) ---
56
57    # Regla: Top 1 y Top 2 deben ir por lados opuestos del itinerario
58    if 1 in ubicacion_tops and 2 in ubicacion_tops:
59        it_1 = obtener_itinerario(ubicacion_tops[1])
60        it_2 = obtener_itinerario(ubicacion_tops[2])
61
62        if it_1 == it_2:
63            penalizacion += PENALTY_ITINERARY
64
65    # Regla: Top 3 y Top 4 deben ir por lados opuestos del itinerario
66    if 3 in ubicacion_tops and 4 in ubicacion_tops:
67        it_3 = obtener_itinerario(ubicacion_tops[3])
68        it_4 = obtener_itinerario(ubicacion_tops[4])
69
70        if it_3 == it_4:
71            penalizacion += PENALTY_ITINERARY
72
73    return penalizacion

Ciclo de vida del Algoritmo Genético

En un algoritmo poblacional se crea la población de individuos, y se recorre esta población evaluando el costo de cada uno de estos en busca del óptimo, de no encontrar ninguno se deben recurrir a operadores genéticos, que son los encargados de hacer variaciones en la población para generar nuevos individuos.
Estos operadores son los de mutación y cruce, simulando los aspectos de la genética. La mutación equivale a tomar un individuo (cromosoma) y cambiarle un elemento (gen) de forma aleatoria. El cruce simula la reproducción de dos individuos. Consiste en tomar dos individuos e intercambiar material genético (secciones del individuo) entre uno y otro. Tanto el cruce como la mutación tienen muchas variantes[6], en este caso aplicamos las más sencillas que son las que hemos definido anteriormente.
En el caso concreto del sorteo, el cruce se hace tomando dos individuos por el mecanismo de ruleta, estos dos van a hacer el rol de padre y madre que van a reproducirse para generar un hijo, este hijo va a ser el nuevo individuo generado.
La función cruzar toma dos padres y de forma aleatoria decide cuál va a ser el donante y receptor (aunque realmente los dos donan), cada uno de estos donantes va a intercambiar todos los grupos que ha recibido de un bombo con el otro padre.
python
1def ejecutar_ga_completo():
2    # --- CONFIGURACIÓN ---
3    POBLACION_TAM = 10
4    GENERACIONES = 1000
5    PROB_CROSSOVER = 0.8  # 80% probabilidad de cruce
6    PROB_MUTACION = 0.2  # 20% probabilidad de mutación
7    K_TORNEO = 2  # Tamaño del torneo para selección
8
9    datos = generar_datos_usuario()
10    poblacion = [crear_individuo(datos) for _ in range(POBLACION_TAM)]
11
12    print()
13
14    for gen in range(GENERACIONES):
15        # Evaluar Fitness
16        # Guardamos (score, individuo)
17        evaluados = [(calcular_fitness(ind), ind) for ind in poblacion]
18        evaluados.sort(key=lambda x: x[0])
19
20        best_score = evaluados[0][0]
21        # Monitorización
22        if gen % 100 == 0 or best_score == 0:
23            print()
24
25        if best_score == 0:
26            print()
27            return evaluados[0][1]
28
29        # Selección (Torneo simple)
30        nueva_poblacion = []
31
32        while len(nueva_poblacion) < POBLACION_TAM:
33            
34            # OPCIÓN A: TORNEO
35            # padre1 = seleccion_torneo(evaluados, k=K_TORNEO)
36            # padre2 = seleccion_torneo(evaluados, k=K_TORNEO)
37
38            # OPCIÓN B: RULETA
39            padre1 = seleccion_ruleta(evaluados)
40            padre2 = seleccion_ruleta(evaluados)
41
42            # Crossover sujeto a probabilidad (0.8)
43            if random.random() < PROB_CROSSOVER:
44                hijo1, hijo2 = cruzar(padre1, padre2)
45            else:
46                hijo1, hijo2 = copy.deepcopy(padre1), copy.deepcopy(padre2)
47            # Mutación sujeta a probabilidad (0.2)
48            hijo1 = mutar(hijo1, PROB_MUTACION)
49            hijo2 = mutar(hijo2, PROB_MUTACION)
50
51            nueva_poblacion.append(hijo1)
52            if len(nueva_poblacion) < POBLACION_TAM:
53                nueva_poblacion.append(hijo2)
54        poblacion = nueva_poblacion
55
56    print("⚠️ Límite alcanzado. Devolviendo mejor aproximación.")
57    return evaluados[0][1]

Operadores Genéticos

El primer operador en ser aplicado es llamado selección. El objetivo de la selección es simular la ley de Darwiniana de la supervivencia del más apto. Como los demás operadores, hay muchas formas de hacer el operador de selección y entre los más usados están los operadores de ruleta y torneo.
En el operador de torneo se van tomando pares de individuos y se compara su valor de fitness, en cada par o el ganador "clasifica" y se enfrenta con los otros ganadores hasta obtener un ganador del torneo. Este es el seleccionado para el cruce.
El operador de ruleta simula el comportamiento de una ruleta, sin embargo los espacios o las divisiones de esta ruleta no son iguales. La probabilidad de selección de un individuo es proporcional a su valor de fitness. A mayor fitness, mayor probabilidad de ser seleccionado. Los individuos seleccionados forman parte de los padres de los cuales surgirá la nueva población de individuos con los otros operadores genéticos.

Selección

No dejamos que cualquiera se reproduzca. Se utiliza el operador de ruleta, aunque también aparece en el código el operador de torneo. Este operador toma todos los individuos de una generación y crea un arreglo llamado pesos que contiene el inverso del valor del fitness de cada uno de estos, luego selecciona uno aleatoriamente con la función random.choices() que asigna un individuo de forma aleatoria, pero con una probabilidad proporcional a su valor de fitness, lo que en el código designamos con la variable pesos.
Este proceso se repite para generar cada Padre, para cada individuo en cada generación. Lo que genera suficientes individuos para reemplazar totalmente (en igual número) a todos los individuos de una generación.
python
1def seleccion_ruleta(evaluados):
2    """
3    Input: Lista de tuplas (fitness, individuo)
4    Selección proporcional al fitness (Ruleta).
5    Como buscamos MINIMIZAR el costo, invertimos el valor.
6    Fitness 0 -> Peso muy alto. Fitness 500 -> Peso bajo.
7    """
8    individuos = [item[1] for item in evaluados]
9    costos = [item[0] for item in evaluados]
10
11    # Invertimos los costos para obtener pesos (Mayor peso = Mayor probabilidad)
12    pesos = [1.0 / (1.0 + c) for c in costos]
13
14    # La función choices de 3 argumentos (población, pesos, cantidad)
15    seleccionado = random.choices(population=individuos, weights=pesos, k=1)[0]
16
17    return seleccionado

Cruce

Ejemplo del operador de cruce. En la imagen vemos que el Padre A dona todos los equipos que pertenecen a los bombos 1 y 4 al hijo A, y los equipos del bombo 2 y 3 al hijo B.

Fig: 6. Ejemplo del operador de cruce. En la imagen vemos que el Padre A dona todos los equipos que pertenecen a los bombos 1 y 4 al hijo A, y los equipos del bombo 2 y 3 al hijo B.

El cruce es el operador más importante, ya que es el responsable de crear diversidad y explorar el espacio de búsqueda.
El cruce que se utiliza aquí toma dos padres y crea dos hijos.
Esto da lugar al reemplazo de la población actual por una nueva generación. En nuestro caso debemos tener mucho cuidado ya que no podemos hacer un cruce en cualquier punto del individuo (cromosoma) si hacemos esto y cortamos un sorteo por la mitad y lo intercambiamos por otro, romperíamos la estructura de los bombos.
El operador de cruce definido en la función cruzar() opera bombo por bombo. Para el Bombo 1 (Cabezas de serie), lanza una moneda: ¿Hereda la distribución del Padre A o del B? Repite para los Bombos 2, 3 y 4.
Esto garantiza que el hijo sea estructuralmente válido (siempre habrá un equipo de cada bombo en cada grupo) pero genéticamente nuevo.
Para cada bombo (1, 2, 3, 4):
  • Lanza moneda: ¿de qué padre heredar este bombo?
  • Hijo1 hereda bombo completo del padre elegido
  • Hijo2 hereda bombo completo del otro padre
Resultado: 2 nuevos sorteos (hijos)
python
1def cruzar(padre1, padre2):
2    """
3    Operador de Cruce Estratificado (Uniform Crossover por Bombos).
4    El hijo hereda la configuración entera de un bombo de uno de los padres.
5    """
6    # Hijos vacíos
7    hijo1 = [[] for _ in range(12)]
8    hijo2 = [[] for _ in range(12)]
9    # Recorremos cada nivel de Bombo (0 a 3)
10    for bombo_idx in range(4):
11        # Decisión aleatoria: ¿Quién dona este bombo?
12        if random.random() < 0.5:
13            # Caso A: Hijo 1 hereda de Padre 1, Hijo 2 hereda de Padre 2
14            donante_1 = padre1
15            donante_2 = padre2
16        else:
17            # Caso B: Cruzado
18            donante_1 = padre2
19            donante_2 = padre1
20
21        # Copiamos la fila entera de ese bombo a los hijos
22        for g_idx in range(12):
23            equipo_p1 = donante_1[g_idx][bombo_idx]
24            equipo_p2 = donante_2[g_idx][bombo_idx]
25
26            hijo1[g_idx].append(equipo_p1)
27            hijo2[g_idx].append(equipo_p2)
28    return hijo1, hijo2

Mutación

Si solo cruzamos padres, eventualmente todos los hijos se parecerán y nos estancaremos en un "Mínimo Local" (una solución que parece buena pero no es la mejor).
Ejemplo del operador de mutación.

Fig: 7. Ejemplo del operador de mutación.

La función mutar(individuo) introduce caos controlado. Con una probabilidad del 20%, tomamos un sorteo, elegimos dos grupos al azar e intercambiamos dos equipos del mismo bombo.
Si muta:
  • Elige 2 grupos al azar
  • Elige un bombo al azar (1, 2, 3, o 4)
  • Intercambia los equipos de ese bombo entre los 2 grupos
  • Solo si ninguno tiene grupo fijo
python
1def mutar(individuo, prob_mutacion):
2    """
3    Intenta mutar con probabilidad 'prob_mutacion'.
4    """
5    if random.random() > prob_mutacion:
6        return individuo  # No muta
7
8    # Si muta, hacemos el intercambio
9    nuevo = copy.deepcopy(individuo)
10    idx_g1, idx_g2 = random.sample(range(12), 2)
11    bombo_idx = random.randint(0, 3)
12
13    e1 = nuevo[idx_g1][bombo_idx]
14    e2 = nuevo[idx_g2][bombo_idx]
15
16    if not e1.grupo_fijo and not e2.grupo_fijo:
17        nuevo[idx_g1][bombo_idx], nuevo[idx_g2][bombo_idx] = (
18            nuevo[idx_g2][bombo_idx],
19            nuevo[idx_g1][bombo_idx],
20        )
21
22    return nuevo

Reemplazo

La nueva generación reemplaza completamente a la anterior (esto es debatible).

Criterio de terminación

Para terminar el ciclo de vida, se debe tener un criterio de terminación. Uno de ellos termina el algoritmo si se alcanza la cantidad máxima de generaciones previamente estipulada. El otro, si detecta que un individuo tiene un fitness igual a cero, y toma este medio individuo como la solución óptima.
Criterios de terminación:
  • Si encuentra un sorteo con penalizacioˊn=0Eˊxito\text{penalización} = 0 \Rightarrow \text{Éxito}
  • Si llega a generación 10001000 \Rightarrow devuelve el mejor encontrado.
Afortunadamente, la estructura de bombos permite encontrar soluciones rápidamente con los algoritmos genéticos y no hay que recurrir al critero de generación de terminación.

Resultados

Visita esta entrada del blog (haciendo clic aquí 👀), para ver un ejemplo de sorteo generado por el algoritmo genético. También puedes probar el código completo en el repositorio de GitHub haciendo clic en el enlace de abajo.
  1. Clasificación Mundial Masculina FIFA/Coca-Cola., https://inside.fifa.com/es/fifa-world-ranking/men (Volver arriba)
  2. John Holland, Adaptation in Natural and Artificial Systems, 1975, https://mitpress.mit.edu/books/adaptation-natural-and-artificial-systems (Volver arriba)
  3. Lawrence Fogel, Intelligent decision making through a simulation of evolution, 1966, https://doi.org/10.1002/bs.3830110403 (Volver arriba)
  4. Larrañaga et al, Genetic Algorithms for the Travelling Salesman Problem: A Review of Representations and Operators, https://doi.org/10.1023/A:1006529012972 (Volver arriba)