Optimización de Red Logística Global con Algoritmo Genético

Caso de Estudio: Ahorro de $10.8M USD y Reducción de 35k Toneladas de CO2

Python Genetic Algorithms Plotly Folium Pandas

# Introducción: El Contrato Ficticio

Este proyecto presenta un escenario de simulación ficticio de alta complejidad, diseñado para modelar la logística de una red de 24 nodos de distribución internacionales. El objetivo principal no es dictar el calendario real de un evento deportivo, sino demostrar cómo la Investigación de Operaciones y la Ciencia de Datos resuelven el dilema central de la gerencia moderna: ¿Cómo ser rentables sin sacrificar los objetivos de sostenibilidad (ESG)?

En la gestión de la cadena de suministro, la «ruta intuitiva» suele ser la más cara. Cuando operamos redes globales complejas (como navieras, giras internacionales o distribución farmacéutica), la planificación humana tiende a seguir patrones históricos ineficientes.

El Reto: La Fórmula 1 es un «circo logístico» global que mueve miles de toneladas a través de 24 sedes en 9 meses. El problema central son las «rutas espagueti» heredadas, que implican saltos transatlánticos redundantes.

El Objetivo: Desarrollar un modelo matemático capaz de minimizar la distancia total y respetar restricciones climáticas estrictas (evitar monzones/nieve) y reglas comerciales innegociables.

# El Resultado: Eficiencia Cuantificable

Aplicando un modelo de optimización matemática propio, logramos desafiar la línea base operativa. A continuación, presento el impacto financiero y operativo logrado tras la validación final del algoritmo:

💰
Ahorro Neto
$10.8M USD
Reducción de OpEx Logístico
📉
Distancia
-26.5%
De 114k km a 84k km
🌿
CO2 Evitado
35,420 t
Huella de Carbono
Validación
100%
0 Violaciones Regionales

# La Solución: Evolución Computacional

Para resolver la complejidad factorial (24! combinaciones), desarrollamos un Algoritmo Genético que simula la evolución para encontrar la solución más eficiente. Implementamos una semilla fija (Random Seed 42) para garantizar la reproducibilidad científica de los resultados.

¿Por qué no podemos simplemente viajar en línea recta? Porque el mundo real tiene reglas. Clima adverso, fechas de entrega y contratos fijos. Para resolver esto, no utilicé un modelo rígido que se bloquea ante un problema. Desarrollé un Modelo de Semáforo de Riesgo.

# Gestión de Riesgo Escalonado

El algoritmo tiene «permiso» para negociar: si llegar a un destino en una fecha «no ideal» ahorra millones en transporte aéreo, el sistema evalúa si vale la pena pagar el costo de mitigación.

Zona Verde

Desviación ≤ 7 Días

Operación estándar en ventana ideal.

$0

Zona Amarilla

Desviación ≤ 45 Días

Se acepta el riesgo pagando costos de mitigación.

Penalización: $2.5M

Zona Roja

Desviación > 45 Días

Restricción dura (Nieve/Monzones). Inviable.

Penalización: $100M

Esta lógica permitió que el algoritmo encontrara «atajos» temporales que un planificador humano habría descartado por miedo al riesgo, logrando un equilibrio matemático entre seguridad y ahorro.

# Visualización Geoespacial

El resultado no es solo numérico, es físico. La optimización eliminó los cruces transatlánticos redundantes, creando una ruta fluida que barre regiones geográficas completas (Oceanía, Asia, Medio Oriente, América, Europa) de manera eficiente.

F1 Map Optimization
Abrir Simulación Interactiva 🌍

*Nota: Se abrirá una nueva pestaña. Podrás comparar la ruta original (roja) contra la optimizada (verde) haciendo zoom libremente.

Vistazo al Núcleo Algorítmico

Esta es la función de 'Fitness' simplificada que evalúa cada ruta. Observa cómo las penalizaciones financieras se integran directamente en el cálculo del costo total, forzando al algoritmo a evitar la Zona Roja.

sustainable_optimizer.py
# --- 3. MOTOR DE OPTIMIZACIÓN (CON EARLY STOPPING AJUSTADO) ---
class SustainableOptimizer:
    def __init__(self, data_manager, pop_size=100, generations=500, mutation_rate=0.20):
        self.dm = data_manager
        self.pop_size = pop_size
        self.generations = generations
        self.mutation_rate = mutation_rate
        self.history = []
        
        # OBTENER ANCLAS
        self.fixed_start, self.fixed_end = self.dm.get_fixed_start_end_indices()
        all_indices = set(range(self.dm.num_races))
        self.middle_indices = list(all_indices - {self.fixed_start, self.fixed_end})

    def _calculate_logistics_kpi(self, total_dist_km):
        p = self.dm.params
        fuel_liters = total_dist_km * 12 
        total_co2_ton = (fuel_liters * 3.16) / 1000
        costo_fuel = fuel_liters * p['Costo_JetFuel_USD_L']
        costo_ops = total_dist_km * 250 
        total_cost_usd = costo_fuel + costo_ops
        return total_cost_usd, total_co2_ton, fuel_liters

    def _check_date_constraints(self, route):
        penalty_score = 0
        violations = 0
        current_date = datetime(2025, 3, 1) 
        
        for race_idx in route:
            # Lógica de ventanas de tiempo...
            if win_start <= current_date <= win_end: days_diff = 0
            else:
                diff_start = abs((current_date - win_start).days)
                diff_end = abs((current_date - win_end).days)
                days_diff = min(diff_start, diff_end)
            
            # SEMÁFORO DE RIESGO
            if days_diff <= 7: penalty_score += 0
            elif days_diff <= 45: penalty_score += 2_500_000 
            else:
                penalty_score += 100_000_000 
                violations += 1
            current_date += timedelta(days=11)
        return penalty_score, violations

    def fitness(self, route):
        total_dist = 0
        for i in range(len(route) - 1):
            total_dist += self.dm.distance_matrix[route[i]][route[i+1]]
        cost_usd, co2_ton, fuel_L = self._calculate_logistics_kpi(total_dist)
        date_penalty, _ = self._check_date_constraints(route)
        total_score = cost_usd + date_penalty
        return total_score, total_dist, cost_usd

    # --- MÉTODO RUN CON PACIENCIA (150) ---
    def run(self):
        population = []
        # Inicialización de población...
        
        best_route = None
        best_fitness = float('inf')
        
        # CONFIGURACIÓN DE PARADA
        generations_without_improvement = 0
        PATIENCE_LIMIT = 150 
        
        for gen in range(self.generations):
            pop_fitness = []
            for indiv in population:
                score, _, _ = self.fitness(indiv)
                pop_fitness.append((score, indiv))
                
                if score < best_fitness:
                    best_fitness = score
                    best_route = indiv
                    generations_without_improvement = 0
            
            if pop_fitness[0][0] >= best_fitness:
                generations_without_improvement += 1
            
            # EARLY STOPPING
            if generations_without_improvement >= PATIENCE_LIMIT:
                break

            # Selección, Cruce y Mutación...
            # (Código abreviado para visualización)

        return best_route, best_fitness
# --- 3. MOTOR DE OPTIMIZACIÓN (CON EARLY STOPPING AJUSTADO) ---
class SustainableOptimizer:
    def __init__(self, data_manager, pop_size=100, generations=500, mutation_rate=0.20):
        self.dm = data_manager
        self.pop_size = pop_size
        self.generations = generations
        self.mutation_rate = mutation_rate
        self.history = []
        
        # OBTENER ANCLAS
        self.fixed_start, self.fixed_end = self.dm.get_fixed_start_end_indices()
        all_indices = set(range(self.dm.num_races))
        self.middle_indices = list(all_indices - {self.fixed_start, self.fixed_end})

    def _calculate_logistics_kpi(self, total_dist_km):
        p = self.dm.params
        fuel_liters = total_dist_km * 12 
        total_co2_ton = (fuel_liters * 3.16) / 1000
        costo_fuel = fuel_liters * p['Costo_JetFuel_USD_L']
        costo_ops = total_dist_km * 250 
        total_cost_usd = costo_fuel + costo_ops
        return total_cost_usd, total_co2_ton, fuel_liters

    def _check_date_constraints(self, route):
        penalty_score = 0
        violations = 0
        current_date = datetime(2025, 3, 1) 
        
        for race_idx in route:
            # Lógica de ventanas de tiempo...
            if win_start <= current_date <= win_end: days_diff = 0
            else:
                diff_start = abs((current_date - win_start).days)
                diff_end = abs((current_date - win_end).days)
                days_diff = min(diff_start, diff_end)
            
            # SEMÁFORO DE RIESGO
            if days_diff <= 7: penalty_score += 0
            elif days_diff <= 45: penalty_score += 2_500_000 
            else:
                penalty_score += 100_000_000 
                violations += 1
            current_date += timedelta(days=11)
        return penalty_score, violations

    def fitness(self, route):
        total_dist = 0
        for i in range(len(route) - 1):
            total_dist += self.dm.distance_matrix[route[i]][route[i+1]]
        cost_usd, co2_ton, fuel_L = self._calculate_logistics_kpi(total_dist)
        date_penalty, _ = self._check_date_constraints(route)
        total_score = cost_usd + date_penalty
        return total_score, total_dist, cost_usd

    # --- MÉTODO RUN CON PACIENCIA (150) ---
    def run(self):
        population = []
        # Inicialización de población...
        
        best_route = None
        best_fitness = float('inf')
        
        # CONFIGURACIÓN DE PARADA
        generations_without_improvement = 0
        PATIENCE_LIMIT = 150 
        
        for gen in range(self.generations):
            pop_fitness = []
            for indiv in population:
                score, _, _ = self.fitness(indiv)
                pop_fitness.append((score, indiv))
                
                if score < best_fitness:
                    best_fitness = score
                    best_route = indiv
                    generations_without_improvement = 0
            
            if pop_fitness[0][0] >= best_fitness:
                generations_without_improvement += 1
            
            # EARLY STOPPING
            if generations_without_improvement >= PATIENCE_LIMIT:
                break

            # Selección, Cruce y Mutación...
            # (Código abreviado para visualización)

        return best_route, best_fitness

# Arquitectura del Código

El proyecto utiliza una arquitectura modular Orientada a Objetos (OOP) para escalabilidad:

  • Gestión de Datos: Carga dinámica de CSVs y anclas geográficas.
  • Motor de Optimización: Algoritmo Genético con Early Stopping.
  • Visualización: Generación automática de dashboards HTML con Plotly y Folium.

¿Quieres auditar el repositorio completo?

Ver en GitHub