# 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:
# 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.
$0Zona Amarilla
Desviación ≤ 45 Días
Se acepta el riesgo pagando costos de mitigación.
Penalización: $2.5MZona Roja
Desviación > 45 Días
Restricción dura (Nieve/Monzones). Inviable.
Penalización: $100MEsta 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.
*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.
# --- 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.