Artículos / Investigación de Operaciones
Optimización de Planeación de Personal con Python
Planea una semana cíclica con pyworkforce 0.5.4. Verifica un plan de nueve personas y compara mínimo personal con mínima brecha absoluta de cobertura.

¿Cuántas personas necesitas cuando la demanda cambia durante la semana y cada una trabaja cinco días consecutivos? El pico diario no basta para incorporar los días libres. Este ejemplo modela siete patrones semanales, calcula cuántas personas siguen cada uno y comprueba la cobertura que producen.
La entrada es demanda diaria sintética de personal, no un pronóstico ni un resultado medido de un call center. pyworkforce ofrece dos objetivos útiles: cubrir todos los requerimientos con el menor personal, o minimizar la brecha absoluta total de cobertura.
El supuesto de semana cíclica
| Día | Personas requeridas |
|---|---|
| Lunes | 5 |
| Martes | 8 |
| Miércoles | 9 |
| Jueves | 7 |
| Viernes | 6 |
| Sábado | 4 |
| Domingo | 3 |
Cada patrón cubre cinco días consecutivos y deja dos días libres. Hay siete rotaciones, una por día de inicio. Es un ciclo de siete días que se repite: el patrón que comienza el jueves también cubre el lunes al cruzar la frontera semanal. No es una semana aislada en la que alguien empieza el jueves y trabaja el lunes anterior.
Codificamos ese ciclo como una fila con siete períodos. La API llama num_days al número de filas, pero aquí num_days=1 representa un ciclo semanal y periods=7 sus siete días. La librería no conecta automáticamente los cambios de personal entre filas o semanas separadas.
El modelo de mínimo personal
Sea x_p la cantidad entera no negativa de personas asignadas al patrón p, y c_{p,i} el indicador de que cubre el día i. Con costos unitarios iguales, el objetivo es:
La cobertura de cada día debe satisfacer la demanda:
MinRequiredResources construye este modelo con CP-SAT de OR-Tools. El costo predeterminado de cada patrón es 1, por lo que cost equivale al número de personas en este ejemplo. Un cost_dict personalizado convierte el objetivo en una suma ponderada; entonces no necesariamente equivale al personal total.
Resuelve las cantidades por patrón semanal
Usa Python 3.12–3.14 y PyWorkforce 0.5.4:
python -m pip install pyworkforce==0.5.4
from pyworkforce.scheduling import MinRequiredResources
days = ["Lun", "Mar", "Mié", "Jue", "Vie", "Sáb", "Dom"]
required = [5, 8, 9, 7, 6, 4, 3]
shifts_coverage = {
"Inicio_Lun": [1, 1, 1, 1, 1, 0, 0],
"Inicio_Mar": [0, 1, 1, 1, 1, 1, 0],
"Inicio_Mié": [0, 0, 1, 1, 1, 1, 1],
"Inicio_Jue": [1, 0, 0, 1, 1, 1, 1],
"Inicio_Vie": [1, 1, 0, 0, 1, 1, 1],
"Inicio_Sáb": [1, 1, 1, 0, 0, 1, 1],
"Inicio_Dom": [1, 1, 1, 1, 0, 0, 1],
}
scheduler = MinRequiredResources(
num_days=1,
periods=7,
shifts_coverage=shifts_coverage,
required_resources=[required],
max_period_concurrency=sum(required),
max_shift_concurrency=max(required),
max_search_time=5.0,
)
solution = scheduler.solve()
if solution["status"] not in {"OPTIMAL", "FEASIBLE"}:
raise RuntimeError(f"No hay un plan de cobertura utilizable: {solution['status']}")
max_period_concurrency limita la cobertura de cualquier período; max_shift_concurrency limita las personas de un patrón. Los límites elegidos, 42 y 9, admiten el certificado de nueve personas que sigue. Unos límites menores pueden restringir o volver imposible un plan de cobertura.
total = sum(item["resources"] for item in solution["resources_shifts"])
print(f"Estado: {solution['status']}")
print(f"Total de trabajadores: {total}")
print("Trabajadores por patrón:")
for item in solution["resources_shifts"]:
print(f" {item['shift']}: {item['resources']}")
Un certificado óptimo, independiente de la asignación concreta de una ejecución del solver, es:
Total de trabajadores: 9
Trabajadores por patrón:
Inicio_Lun: 4
Inicio_Mar: 1
Inicio_Mié: 1
Inicio_Jue: 0
Inicio_Vie: 0
Inicio_Sáb: 2
Inicio_Dom: 1
Cubre [7, 8, 9, 7, 6, 4, 4] personas de lunes a domingo. Cada entrada satisface la demanda. Nueve también es una cota inferior: el miércoles exige nueve personas y las 42 jornadas-persona requeridas, divididas entre cinco días de trabajo por persona, dan ceil(42 / 5) = 9. El certificado alcanza esa cota y prueba el mínimo de personal del modelo. Puede haber otras asignaciones óptimas.
Comprueba la cobertura devuelta
Valida la asignación que realmente devuelve el solver, sin suponer que coincide con ese certificado:
assigned = {item["shift"]: item["resources"] for item in solution["resources_shifts"]}
for i, day in enumerate(days):
covered = sum(assigned[shift] * mask[i] for shift, mask in shifts_coverage.items())
if covered < required[i]:
raise ValueError(f"Falta cobertura en {day}: {covered} < {required[i]}")
print(f"{day}: cubierto={covered}, requerido={required[i]}")
Los estados de CP-SAT importan: OPTIMAL incluye una prueba de optimalidad; FEASIBLE entrega un plan válido sin esa prueba. INFEASIBLE, UNKNOWN y MODEL_INVALID no entregan un plan utilizable. En 0.5.4 esos resultados incluyen cantidades -1 y un turno Unknown; comprueba el estado antes de sumar personas o consultar vectores de cobertura. Un tiempo límite no garantiza encontrar una solución.
Compara el objetivo de ajuste más cercano
MinAbsDifference minimiza la suma de brechas absolutas por período. No garantiza cubrir la demanda ni minimiza la cantidad de personas. Conserva su resultado separado y reporta los faltantes, además del objetivo:
from pyworkforce.scheduling import MinAbsDifference
fit_scheduler = MinAbsDifference(
num_days=1,
periods=7,
shifts_coverage=shifts_coverage,
required_resources=[required],
max_period_concurrency=sum(required),
max_shift_concurrency=max(required),
max_search_time=5.0,
)
fit_solution = fit_scheduler.solve()
if fit_solution["status"] not in {"OPTIMAL", "FEASIBLE"}:
raise RuntimeError(f"No hay un plan de ajuste utilizable: {fit_solution['status']}")
fit_assigned = {item["shift"]: item["resources"] for item in fit_solution["resources_shifts"]}
print(f"Estado: {fit_solution['status']}")
print(f"Total de trabajadores: {sum(fit_assigned.values())}")
print(f"Brecha absoluta: {fit_solution['cost']}")
for i, day in enumerate(days):
covered = sum(fit_assigned[shift] * mask[i] for shift, mask in shifts_coverage.items())
print(f"{day}: cubierto={covered}, faltante={max(0, required[i] - covered)}")
Un certificado de ajuste cercano usa cantidades [3, 3, 0, 0, 0, 1, 1] en el mismo orden de patrones. Sus ocho personas cubren [5, 8, 8, 7, 6, 4, 2]: falta una el miércoles y una el domingo, para una brecha absoluta total de 2. Es óptimo para este objetivo porque la cobertura total es un múltiplo de cinco y la demanda suma 42; una brecha absoluta menor que dos es imposible. Una brecha pequeña no vuelve aceptable el plan cuando cada requerimiento diario es obligatorio.
Límites de la versión y siguiente paso
En la versión publicada 0.5.4, ambos solvers de scheduling ignoran el num_search_workers solicitado porque se asigna fuera del objeto de parámetros de CP-SAT. MinAbsDifference también limita sus variables de brecha con max_period_concurrency: por ejemplo, demanda 9 con capacidad 1 no puede representar la brecha válida de 8 y puede devolver INFEASIBLE. La capacidad amplia del ejemplo evita ese defecto. Aumentar una capacidad operativa real sólo para obtener una solución cambiaría el modelo.
Estos defectos del código de 0.5.4 tienen correcciones locales preparadas; no están en el paquete fijado. Verifica una versión que las incluya antes de depender de esos comportamientos corregidos. Aquí no se promete un paralelismo ni un tiempo de ejecución específico.
En una operación real, la demanda viene de un pronóstico o de un cálculo de personal como Erlang C, usando volumen de llamadas, tiempos de atención y metas de servicio suministrados. Scheduling transforma esos requerimientos de personal en cantidades por patrón; no pronostica la demanda entrante. El siguiente paso es asignar personas con nombre. La elegibilidad, disponibilidad, descanso entre turnos y cambios entre semanas necesitan reglas explícitas adicionales a este ejemplo agregado de semana cíclica.
¿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.