Optimiza la secuenciación de trabajos con heurísticas y Ortools en Python. Minimiza la tardanza total con un modelo de programación lineal paso a paso
Fuente: Algocademy
La secuenciación de trabajos es un problema clásico de optimización en el que se busca determinar el orden óptimo para procesar tareas en una máquina, minimizando la tardanza total. En este artículo, combinamos heurísticas clásicas (SPT, EDD, etc.) con un modelo de programación lineal usando ortools para garantizar la solución óptima.
Trabajaremos con 10 trabajos generados aleatoriamente, aplicaremos 7 heurísticas y un modelo de optimización.
Paso 1: Generación de Datos
Generamos 10 trabajos con duraciones y fechas de entrega aleatorias.
import random
num_trabajos = 10
trabajos = [f'J{i+1}' for i in range(num_trabajos)]
duraciones = [random.randint(1, 10) for _ in range(num_trabajos)]
fechas_entrega = [random.randint(5, 30) for _ in range(num_trabajos)]
datos_trabajos = list(zip(trabajos, duraciones, fechas_entrega))
print("Datos generados:")
for t in datos_trabajos:
print(t)
Paso 2: Aplicación de Heurísticas
Calculamos la tardanza total con 7 heurísticas:
- SPT: Shortest Processing Time
- EDD: Earliest Due Date
- LPT: Longest Processing Time
- FCFS: First Come First Served
- LCFS: Last Come First Served
- S/O: Slack per Operation
- CR: Critical Ratio
# @title
def calcular_tardanza(secuencia, duraciones, fechas_entrega):
n = len(secuencia)
tiempo_actual = 0
tardanza_total = 0
for i in secuencia:
tiempo_actual += duraciones[i]
tardanza = max(0, tiempo_actual - fechas_entrega[i])
tardanza_total += tardanza
return tardanza_total
def heuristica_spt(duraciones):
return sorted(range(len(duraciones)), key=lambda i: duraciones[i])
def heuristica_edd(fechas_entrega):
return sorted(range(len(fechas_entrega)), key=lambda i: fechas_entrega[i])
def heuristica_slack(duraciones, fechas_entrega):
slack = [fechas_entrega[i] - duraciones[i] for i in range(len(duraciones))]
return sorted(range(len(slack)), key=lambda i: slack[i])
# Implementación de las heurísticas adicionales
def heuristica_lpt(duraciones):
return sorted(range(len(duraciones)), key=lambda i: duraciones[i], reverse=True)
def heuristica_ldd(fechas_entrega):
return sorted(range(len(fechas_entrega)), key=lambda i: fechas_entrega[i], reverse=True)
def heuristica_cr(duraciones, fechas_entrega):
# Critical Ratio (Fecha de Entrega / Duración) - menor CR primero
cr = [fechas_entrega[i] / duraciones[i] if duraciones[i] > 0 else float('inf') for i in range(len(duraciones))]
return sorted(range(len(cr)), key=lambda i: cr[i])
def generar_secuencia_gantt(secuencia, trabajos, duraciones, fechas_entrega):
df = []
start = 0
for idx in secuencia:
job = trabajos[idx]
dur = duraciones[idx]
end = start + dur
tardanza = max(0, end - fechas_entrega[idx])
df.append(dict(Task=job, Start=start, Finish=end, Due=fechas_entrega[idx], Tardanza=tardanza))
start = end
return pd.DataFrame(df)
# Ejecutar todas las heurísticas
n = len(trabajos)
indices = list(range(n))
heurísticas = {
'FCFS': indices, # First Come First Served es simplemente el orden inicial
'SPT': heuristica_spt(duraciones),
'LPT': heuristica_lpt(duraciones),
'EDD': heuristica_edd(fechas_entrega),
'LDD': heuristica_ldd(fechas_entrega),
'Slack': heuristica_slack(duraciones, fechas_entrega),
'CR': heuristica_cr(duraciones, fechas_entrega)
}
resultados = {}
secuencias_resultados = {}
for nombre, sec_indices in heurísticas.items():
tardanza = calcular_tardanza(sec_indices, duraciones, fechas_entrega)
resultados[nombre] = tardanza
secuencias_resultados[nombre] = ' → '.join(trabajos[i] for i in sec_indices)
# Determinar la mejor y peor tardanza
mejor_tardanza = min(resultados.values())
peor_tardanza = max(resultados.values())
print(f"\nMejor Tardanza Total (Heurísticas): {mejor_tardanza}")
print(f"Peor Tardanza Total (Heurísticas): {peor_tardanza}")
resultados_df = pd.DataFrame(list(resultados.items()), columns=['Heurística', 'Tardanza Total (días)'])
#resultados_df['Secuencia'] = resultados_df['Heurística'].map(secuencias_resultados)
resultados_df = resultados_df.sort_values(by='Tardanza Total (días)')
display(resultados_df.style.hide(axis='index').bar(subset=['Tardanza Total (días)'], color='#1e40af'))
Paso 3: Optimización con ortools (Opcional)
# @title
import time
from ortools.sat.python import cp_model
start_time = time.time()
# Crear el modelo CP-SAT
model = cp_model.CpModel()
n = len(trabajos)
# Variables
# Interval variables for each job on the single machine
job_intervals = []
# Start, end, and duration variables for each job
starts = []
ends = []
tardiness = []
for i in range(n):
# The start and end times can be at most the total duration of all jobs
max_end_time = sum(duraciones)
start_var = model.NewIntVar(0, max_end_time, f'start_{i}')
end_var = model.NewIntVar(0, max_end_time, f'end_{i}')
interval_var = model.NewIntervalVar(start_var, duraciones[i], end_var, f'interval_{i}')
starts.append(start_var)
ends.append(end_var)
job_intervals.append(interval_var)
# Tardiness variable for each job: max(0, end_var - due_date)
tardy_var = model.NewIntVar(0, max_end_time, f'tardy_{i}')
model.AddMaxEquality(tardy_var, [end_var - fechas_entrega[i], 0])
tardiness.append(tardy_var)
# Restricción de no solapamiento en la máquina única
# Use a no_overlap constraint on the interval variables
model.AddNoOverlap(job_intervals)
# Objetivo: Minimizar la tardanza total
model.Minimize(sum(tardiness))
# Crear el solucionador
solver = cp_model.CpSolver()
# Resolver
# Incrementar el tiempo límite para problemas más grandes si es necesario (en segundos)
solver.parameters.max_time_in_seconds = 300.0
status = solver.Solve(model)
tiempo_opt = time.time() - start_time
# Extraer resultados
secuencia = []
tardanza_optima = None
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
tardanza_optima = solver.ObjectiveValue()
# Extraer la secuencia ordenando por tiempo de inicio
start_times_with_indices = [(solver.Value(starts[i]), i) for i in range(n)]
start_times_with_indices.sort()
secuencia = [index for start_time, index in start_times_with_indices]
print(f"✅ Solución encontrada en {tiempo_opt:.2f} segundos (Estado: {solver.StatusName(status)})")
print(f"📊 Tardanza total: {tardanza_optima:.1f} días")
print(f"🔄 Orden encontrado: {' → '.join(trabajos[i] for i in secuencia)}")
else:
print(f"❌ No se encontró una solución óptima o factible. Estado: {solver.StatusName(status)}")
secuencia = [] # Secuencia vacía si no hay solución
tardanza_optima = None
Resultados
| Heurística | Tardanza (días) |
|---|---|
| EDD | 6 |
| Óptimo (ortools) | 6 |
| Slack | 12 |
| FCFS | 43 |
| SPT | 8830 |
| Critical Ratio (CR) | 13135 |
| Longest Processing Time (LPT) | 24717 |
| Least Due Date (LDD) | 25370 |
¡El modelo de ortools obtiene el mismo resultado que la heurística EDD!
Consejos Pro
- Escala a más trabajos con Gurobi o CPLEX.
- Visualiza con Gantt en Plotly.
¿Quieres aplicarlo HOY en tu empresa?
Descarga el Jupyter Notebook con Gantt
¿Cómo aplicarlo HOY?
- Excel: Copia tus datos (columna A: ID, B: Duración, C: Entrega)
- Código: Pega en líneas 6-8
- Ejecuta: Obtén el orden óptimo en 3 segundos
- Implementa: Envía al jefe → "Esto ahorra días por semana"
Ejemplo real: Una fábrica de muebles redujo 3 días de retraso con este método.
Referencias
[1] Ortools Documentation – ortools
[2] Principles of Sequencing and Scheduling – Kenneth R. Baker, Dan Trietsch