Cómo Resolver Problemas de Scheduling con Python
Resuelve problemas de scheduling y rostering de empleados en Python con pyworkforce. Asigna trabajadores individuales a turnos respetando cobertura, reglas de descanso, turnos prohibidos y preferencias.
En el artículo anterior sobre planeación de personal vimos cómo determinar cuántos trabajadores necesitas. Ahora abordaremos un problema distinto y más concreto: dado un conjunto de empleados disponibles, ¿a quién asignas a qué turno en qué día? Esto es el problema de scheduling o rostering, y agrega un ingrediente extra: las asignaciones son individuales en lugar de agregadas.
La diferencia conceptual es importante. La planeación de personal trabaja con números agregados (cuántas personas por día). El rostering trabaja con asignaciones individuales (¿trabaja Alice el turno de la mañana el martes?). Esta granularidad extra lo hace más rico pero también más complejo — y es justamente para lo que está hecho el módulo de rostering de pyworkforce.
¿Qué Es la Optimización de Scheduling?
El scheduling de empleados es el proceso de asignar trabajadores específicos a turnos específicos satisfaciendo un conjunto de restricciones. En su forma más simple, cada turno en cada día tiene un requerimiento de personal y cada trabajador puede asignarse a él o no.
Las restricciones típicas incluyen:
- Cobertura mínima: cada turno en cada día debe tener al menos N personas.
- Horas máximas/mínimas: un trabajador debe trabajar dentro de un rango de horas permitido.
- Reglas de descanso: un trabajador necesita un descanso mínimo, y ciertas secuencias de turno (un turno de noche seguido de uno de mañana) deben prohibirse.
- Disponibilidad: algunos trabajadores no pueden trabajar ciertos días o turnos.
- Preferencias: en lo posible, respetar los turnos que los trabajadores prefieren.
La función objetivo puede minimizar la brecha entre el personal requerido y el asignado, minimizar el total de horas o el costo, o maximizar la satisfacción de preferencias.
El Problema de Rostering
Internamente, el rostering es un programa entero mixto con variables de decisión binarias: x[w][s][d] = 1 si el trabajador w se asigna al turno s el día d, y 0 en caso contrario. Los requerimientos de cobertura, los límites de horas, los turnos prohibidos y las reglas de descanso se convierten en restricciones lineales sobre esas variables, y el objetivo minimiza el total de horas asignadas (o el costo).
Escribir todo eso a mano es tedioso y propenso a errores — cada restricción debe codificarse variable por variable. pyworkforce construye y resuelve ese modelo por ti a partir de una descripción de alto nivel, usando el solver CP-SAT de Google OR-Tools por debajo.
Rostering con pyworkforce: MinHoursRoster
El solver MinHoursRoster asigna recursos con nombre a turnos a lo largo de un horizonte de días, minimizando el total de horas de trabajo mientras respeta cada restricción que declares.
pip install pyworkforce
from pyworkforce.rostering.binary_programming import MinHoursRoster
resources = ["Alice", "Bob", "Carlos", "Diana", "Eve"]
solver = MinHoursRoster(
num_days=7,
resources=resources,
shifts=["Mañana", "Tarde", "Noche"],
shifts_hours=[8, 8, 8],
min_working_hours=40,
max_resting=2,
non_sequential_shifts=[{"origin": "Noche", "destination": "Mañana"}],
banned_shifts=[
{"resource": "Alice", "shift": "Noche", "day": 5},
],
required_resources={
"Mañana": [2, 2, 2, 2, 2, 1, 1],
"Tarde": [2, 2, 2, 2, 2, 1, 1],
"Noche": [1, 1, 1, 1, 1, 1, 1],
},
resources_preferences=[
{"resource": "Bob", "shift": "Mañana"},
],
)
result = solver.solve()
print(f"Estado: {result['status']}, Costo: {result['cost']}")
print(f"Turnos asignados: {result['total_shifts']}")
Cada argumento corresponde a una regla del mundo real:
shifts_hours— la duración de cada turno, para que el solver pueda hacer cumplir el rango de horas de trabajo.min_working_hours— el mínimo de horas que cada recurso debe ser programado a lo largo del horizonte.max_resting— el máximo número de días de descanso que un recurso puede tener.required_resources— para cada turno, cuántas personas se necesitan cada día (la salida de la etapa de planeación/scheduling).non_sequential_shifts— transiciones de turno prohibidas, p. ej. un turno deNocheno puede ser seguido por uno deMañana.banned_shifts— indisponibilidad estricta para un recurso, turno y día específicos.resources_preferences— preferencias suaves que el solver intenta respetar.
Leyendo el Horario desde la Solución
El diccionario de solución contiene las asignaciones por persona y los días de descanso, de modo que todo el manejo de restricciones que de otra forma harías a mano ya está hecho:
# Asignaciones individuales: quién trabaja qué turno y qué día
for assignment in result["resource_shifts"]:
if assignment["shift"] != "False": # "False" marca un espacio de descanso
print(
f"Día {assignment['day']:>1} | "
f"{assignment['resource']:<8} -> {assignment['shift']}"
)
# Días de descanso por recurso
print("\nDías de descanso:")
for rest in result["resting_resource"]:
print(f" {rest['resource']}: día {rest['day']}")
Como pyworkforce devuelve la asignación completa más los días de descanso de cada persona, construir un calendario semanal legible o un resumen de carga por empleado es solo cuestión de agrupar esta lista — sin necesidad de decodificar variables crudas del solver.
Modelando Restricciones del Mundo Real
La fortaleza del modelo de rostering es lo naturalmente que las reglas de negocio se mapean a sus parámetros.
Reglas de Descanso y Secuencias Prohibidas
El descanso mínimo se maneja con max_resting (cuántos días libres se permiten) junto con min_working_hours (el piso de horas trabajadas). Las transiciones prohibidas — el clásico "ningún turno de mañana justo después de uno de noche" — van en non_sequential_shifts:
non_sequential_shifts=[
{"origin": "Noche", "destination": "Mañana"},
{"origin": "Noche", "destination": "Tarde"},
]
Disponibilidad y Preferencias
La indisponibilidad estricta (vacaciones, capacitación, límites legales) se expresa como banned_shifts, que el solver trata como inviolable. Los deseos más suaves van en resources_preferences, que el solver respeta cuando puede sin romper una restricción dura:
banned_shifts=[
{"resource": "Diana", "shift": "Mañana", "day": 0}, # Diana libra el lunes en la mañana
]
resources_preferences=[
{"resource": "Bob", "shift": "Mañana"}, # Bob prefiere las mañanas
{"resource": "Eve", "shift": "Noche"}, # Eve prefiere las noches
]
Equipos Multi-Habilidad
Cuando los trabajadores cubren distintos roles, pyworkforce ofrece un módulo dedicado pyworkforce.staffing para la optimización del mix de personal multi-habilidad, de modo que puedes modelar quién está calificado para qué rol por separado de quién está asignado a qué turno — en lugar de codificar a mano variables específicas por rol.
Consideraciones de Escalabilidad
El rostering con variables binarias es un programa entero mixto, que es NP-difícil en general. pyworkforce delega el trabajo pesado al solver CP-SAT de OR-Tools, un motor de última generación que maneja instancias realistas (decenas de empleados, múltiples turnos, un horizonte semanal) de forma eficiente. Para problemas muy grandes:
- Descompón el horizonte: cuando los días son independientes, resuelve semana por semana (un horizonte rodante).
- Ajusta el modelo: menos combinaciones elegibles (recurso, turno) — vía
banned_shiftsy filtrado por habilidades — reduce drásticamente el espacio de búsqueda. - Revisa el estado: siempre inspecciona
result["status"]. Un resultadoINFEASIBLEsignifica que las restricciones se contradicen entre sí (por ejemplo, exigir más cobertura de la que los recursos disponibles pueden ofrecer).
Conclusión
El scheduling de empleados es un problema de optimización combinatoria que se presta naturalmente a la programación con restricciones. pyworkforce permite modelarlo de manera declarativa — describiendo recursos, turnos, cobertura y reglas — sin sacrificar el poder del solver de OR-Tools subyacente.
La clave para formular estos problemas correctamente sigue siendo distinguir entre:
- Lo que controlamos: las asignaciones individuales (quién trabaja qué y cuándo).
- Lo que queremos lograr: cobertura suficiente, mínimo de horas, preferencias respetadas (la función objetivo).
- Lo que no se puede violar: reglas de descanso, disponibilidad, capacidad (las restricciones duras).
Con esa estructura clara, pyworkforce se encarga del resto. El solver encuentra en segundos asignaciones que tomarían horas de coordinación manual, y la solución es óptima dentro del modelo planteado.
Sobre el autor
Rodrigo Arenas es arquitecto de software e ingeniero de machine learning en Medellín, Colombia. Construye productos, plataformas y software open source para IA, Data Engineering y Machine Learning — es el creador de Ciaren, sklearn-genetic-opt y PyWorkforce.
Lectura recomendada
Rodrigo Arenas — ML Engineer & Open Source Maintainer
¿Estás trabajando en algo similar? Puedo ayudarte a llevarlo a producción.