Artículos / Investigación de Operaciones
Cómo Resolver Problemas de Scheduling con Python
Construye turnos con pyworkforce 0.5.4: cobertura, disponibilidad, horas mínimas, estados del solver y límites de las reglas de descanso y preferencias.

Necesitas dos personas en la mañana, dos en la tarde y una en la noche. Ese total todavía no indica quién trabaja cada turno. El rostering asigna personas con nombre a días y turnos; la planeación de personal determina primero los requerimientos de cobertura.
Este ejemplo usa pyworkforce para construir un horario de siete días con cinco personas. Comprueba el estado del solver antes de leer las asignaciones y distingue las restricciones del modelo de las reglas que no puede hacer cumplir.
El modelo de MinHoursRoster
MinHoursRoster construye un modelo binario y lo resuelve con CP-SAT de OR-Tools. Una variable vale 1 cuando una persona trabaja un turno en un día determinado, y 0 en caso contrario. El modelo exige:
- Como mínimo, la cobertura solicitada para cada día y turno.
- Como máximo, un turno por persona al día.
- Al menos
min_working_hourspor persona en todo el horizonte. - Como máximo,
max_restingdías libres por persona en ese horizonte. - Ninguna asignación a un día/turno expresamente prohibido para esa persona.
Sin preferencias, el objetivo minimiza el total de horas programadas. Con preferencias, resta una recompensa por las asignaciones preferidas, ponderada por resources_prioritization (peso predeterminado 1). El campo cost contiene ese valor objetivo; no es un costo de nómina.
Un horario ejecutable de siete días
Usa Python 3.12–3.14 y el paquete publicado PyWorkforce 0.5.4. El ejemplo omite las transiciones dirigidas entre turnos por el defecto de esa versión que se explica más adelante.
python -m pip install pyworkforce==0.5.4
Los días se numeran del 0 al 6. Todos los turnos duran ocho horas; la cobertura baja en los días 5 y 6. Alice no puede trabajar el turno de noche del día 5.
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,
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],
},
max_search_time=5.0,
)
result = solver.solve()
status = result["status"]
if status not in {"OPTIMAL", "FEASIBLE"}:
raise RuntimeError(f"No hay un horario utilizable: {status}")
print(f"Estado: {status}")
print(f"Horas programadas: {result['shifted_hours']}")
print(f"Objetivo: {result['cost']}")
print(f"Turnos asignados: {result['total_shifts']}")
La cobertura exige 31 asignaciones: 25 en los días 0–4 y seis en los días 5–6. Con ocho horas por turno, la cota inferior es 248 horas. Este modelo puede alcanzarla: un resultado OPTIMAL tiene 31 asignaciones y objetivo 248. Un resultado solamente FEASIBLE no necesariamente alcanza esa cota. Las asignaciones concretas de personas a turnos pueden variar entre ejecuciones.
Lee las asignaciones después de comprobar el estado
Ejecuta este bloque después del ejemplo anterior. Los turnos trabajados están en resource_shifts; los días libres están en la lista separada resting_resource. Una asignación trabajada no usa el texto "False" para marcar descanso.
for assignment in result["resource_shifts"]:
print(
f"Día {assignment['day']} | "
f"{assignment['resource']:<8} -> {assignment['shift']}"
)
print("\nDías libres:")
for rest in result["resting_resource"]:
print(f" {rest['resource']}: día {rest['day']}")
Los estados de CP-SAT distinguen:
- OPTIMAL: hay una solución factible y se probó su optimalidad.
- FEASIBLE: hay una solución, pero no se probó su optimalidad.
- INFEASIBLE: se probó que el modelo es imposible.
- UNKNOWN: la búsqueda terminó sin encontrar una solución ni probar imposibilidad.
- MODEL_INVALID: el modelo enviado no pasó la validación.
Para un estado sin solución, PyWorkforce 0.5.4 devuelve entradas de marcador como recurso -1 y turno "Unknown". No representan un horario. Por ejemplo, exigir seis personas en la mañana cuando sólo existen cinco es imposible; revisa capacidad y disponibilidad en lugar de aumentar el tiempo de búsqueda.
Días libres, descanso entre turnos y un límite de 0.5.4
max_resting=2 limita los días libres a dos; no garantiza dos días libres. min_working_hours=40 es un mínimo sobre los siete días. Ninguno establece un máximo semanal de trabajo, un límite de días consecutivos ni una cantidad mínima de horas entre turnos. Los nombres y duraciones de los turnos no aportan sus horarios de inicio y fin.
La API non_sequential_shifts busca prohibir una pareja dirigida en días consecutivos, como Noche seguida de Mañana. En la versión publicada 0.5.4, su restricción también bloquea otras asignaciones del día siguiente al turno de origen; varias parejas prohibidas pueden bloquear el origen por completo. Por eso el ejemplo no afirma cumplir esa regla. Se preparó una corrección del código fuente, pero no está incluida en el paquete fijado de PyPI. Usa una versión verificada que contenga la corrección antes de depender de esta función.
La misma versión asigna num_search_workers fuera del objeto de parámetros de CP-SAT, de modo que la cantidad solicitada no llega al motor. El ejemplo no promete un nivel específico de paralelismo. Estos límites se reprodujeron con la implementación de 0.5.4.
Disponibilidad, preferencias y habilidades
Una prohibición afecta a una persona, turno y día. Para dejar a alguien indisponible durante todo un día, prohíbe todos sus turnos de ese día. Una preferencia es una recompensa suave del objetivo, no una asignación garantizada:
# Entradas opcionales para otro modelo con los mismos recursos y turnos.
banned_shifts = [
{"resource": "Diana", "shift": shift, "day": 0}
for shift in ["Mañana", "Tarde", "Noche"]
]
resources_preferences = [
{"resource": "Bob", "shift": "Mañana"},
]
Cuando activas una preferencia, reporta shifted_hours y cost por separado: el objetivo incluye la recompensa. Quitar a Diana del día 0 también deja cuatro personas disponibles frente a cinco posiciones requeridas; ese cambio de disponibilidad haría imposible este ejemplo concreto.
pyworkforce.staffing.MultiSkillStaffing optimiza cantidades de perfiles de habilidades. No restringe automáticamente las personas de MinHoursRoster a roles para los que estén calificadas. El horario necesita restricciones explícitas de elegibilidad para sus propios recursos y turnos.
Para un horizonte mayor, registra el estado y el tiempo límite, valida las asignaciones frente a las reglas operativas y conserva las asignaciones de frontera en cualquier modelo de horizonte rodante. Resolver cada semana por separado puede perder una regla que cruce del domingo al lunes. El modelo sirve como punto de partida para un horario operativo; las reglas adicionales de descanso, equidad, habilidades y carga laboral necesitan su propia representación y comprobación.
¿Vas a construir una plataforma de IA o de datos?
Si necesitas adaptar este enfoque a tu sistema, cuéntame qué datos tienes y qué decisión técnica necesitas resolver.