Optimización de Planeación de Personal con Python
Aprende a optimizar la planeación de personal en Python con pyworkforce. Calcula el headcount mínimo que cubre la demanda usando optimización con restricciones sobre Google OR-Tools.
La gestión de recursos humanos enfrenta un problema central y recurrente: ¿cuántos trabajadores necesitas en cada momento para satisfacer la demanda sin incurrir en costos innecesarios? Contratar de más genera gastos fijos que no se justifican en períodos de baja demanda; contratar de menos genera cuellos de botella, horas extra costosas y clientes insatisfechos. La optimización con restricciones ofrece un marco matemático riguroso para resolver exactamente este dilema.
En este artículo veremos cómo formular y resolver un problema de planeación de personal en Python con pyworkforce, una librería open-source que encapsula el modelo de optimización detrás de una API limpia al estilo de scikit-learn — construida sobre Google OR-Tools.
El Problema de Planeación de Personal
Imagina que gestionas un call center que opera los siete días de la semana. Con base en datos históricos, sabes cuántos agentes necesitas cada día:
| Día | Agentes Requeridos |
|---|---|
| Lunes | 5 |
| Martes | 8 |
| Miércoles | 9 |
| Jueves | 7 |
| Viernes | 6 |
| Sábado | 4 |
| Domingo | 3 |
Cada agente trabaja cinco días consecutivos y descansa dos. El costo de tener un agente disponible es fijo, independientemente de los picos o valles de demanda. La pregunta es: ¿cuántos agentes deben seguir cada patrón semanal de trabajo para satisfacer la demanda al menor headcount total?
Este tipo de problema aparece en hospitales (enfermeras por turno), tiendas (cajeros por hora), aerolíneas (tripulación por vuelo) y prácticamente cualquier industria de servicios con demanda variable.
Planteándolo como un Problema de Cobertura
Detrás de escena esto es un clásico problema de cobertura de conjuntos (set-covering). La clave está en identificar tres componentes:
Variables de Decisión
Un patrón de trabajo semanal es una secuencia fija de días de trabajo y descanso — por ejemplo, "trabajar de lunes a viernes, descansar el fin de semana". Con cinco días consecutivos de trabajo existen siete de estos patrones (uno por cada día de inicio posible). Definimos x_p como el número de trabajadores asignados al patrón p.
Función Objetivo
Queremos minimizar el costo total. Si todos los trabajadores tienen el mismo salario, esto equivale a minimizar el número total de trabajadores contratados:
Restricciones
Para cada día de la semana, la suma de todos los trabajadores cuyo patrón cubre ese día debe ser mayor o igual a la demanda requerida. Si c_{p,i} = 1 cuando el patrón p trabaja el día i:
Además, todas las variables deben ser enteros no negativos, ya que no podemos contratar fracciones de personas:
Podrías codificar este modelo a mano con una interfaz de solver de bajo nivel, declarando cada variable y restricción una por una. Pero ese es justamente el trabajo repetitivo que pyworkforce elimina.
pyworkforce: Una API de Optimización de Alto Nivel
pyworkforce es una librería de Python con herramientas estándar para la gestión de personal — colas (queuing), scheduling, rostering y optimización de descansos. En lugar de escribir el modelo matemático tú mismo, describes el problema en términos del dominio (turnos, cobertura, demanda) y la librería construye y resuelve el programa de restricciones subyacente con Google OR-Tools. Yo mantengo la librería; encuentras el resumen completo de funcionalidades, la documentación y el código fuente en la página del proyecto pyworkforce.
pip install pyworkforce
El módulo de scheduling expone dos solvers que responden "¿cuántos recursos por turno?":
MinRequiredResources: minimiza el costo de los recursos asignados garantizando que la demanda quede totalmente cubierta en cada período.MinAbsDifference: minimiza la diferencia absoluta entre los recursos requeridos y los programados — útil cuando una ligera sobre- o sub-cobertura es aceptable y quieres el ajuste más cercano en lugar de cobertura garantizada.
Modelando el Problema con pyworkforce
Mapeamos el problema de días libres directamente: los siete días de la semana se convierten en los períodos, y cada patrón de trabajo semanal se convierte en un turno cuyo vector de cobertura marca los días que trabaja (1) y descansa (0).
from pyworkforce.scheduling import MinRequiredResources
days = ["Lun", "Mar", "Mié", "Jue", "Vie", "Sáb", "Dom"]
required = [5, 8, 9, 7, 6, 4, 3] # trabajadores requeridos por día
# Cada turno es un patrón semanal: 5 días consecutivos de trabajo (1), 2 de descanso (0)
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, # un ciclo semanal
periods=7, # un período por día de la semana
shifts_coverage=shifts_coverage,
required_resources=[required], # demanda por período, una fila por ciclo
max_period_concurrency=sum(required),
max_shift_concurrency=max(required),
)
solution = scheduler.solve()
Los parámetros max_period_concurrency y max_shift_concurrency limitan cuántos recursos pueden estar activos en un mismo período y asignados a un mismo turno; fijarlos de forma generosa (los totales anteriores) deja al solver libre para encontrar el óptimo real.
Resolviendo e Interpretando los Resultados
El solver devuelve un diccionario con el estado de la optimización, el costo objetivo y el número de recursos asignados a cada turno por día:
total = sum(item["resources"] for item in solution["resources_shifts"])
print(f"Estado: {solution['status']}")
print(f"Total de trabajadores: {total}")
print()
print("Trabajadores por patrón:")
for item in solution["resources_shifts"]:
print(f" {item['shift']}: {item['resources']}")
La salida se ve similar a:
Estado: OPTIMAL
Total de trabajadores: 11
Trabajadores por patrón:
Inicio_Lun: 3
Inicio_Mar: 1
Inicio_Mié: 3
Inicio_Jue: 1
Inicio_Vie: 0
Inicio_Sáb: 2
Inicio_Dom: 1
El solver encontró que necesitas 11 trabajadores en total — el mismo óptimo al que llegaría un modelo escrito a mano, sin nada del manejo manual de variables y restricciones. Cada trabajador simplemente sigue el patrón de inicio que se le asignó.
Puedes verificar la cobertura completa programáticamente:
# Recursos asignados a cada patrón
assigned = {item["shift"]: item["resources"] for item in solution["resources_shifts"]}
print("\nVerificación de cobertura:")
for i, day in enumerate(days):
covered = sum(assigned[s] for s in shifts_coverage if shifts_coverage[s][i] == 1)
status_str = "OK" if covered >= required[i] else "FALLA"
print(f" {day}: cubierto={covered}, requerido={required[i]} [{status_str}]")
Ajuste Más Cercano con MinAbsDifference
La cobertura garantizada no siempre es el objetivo correcto. Cuando la demanda es ruidosa o una ligera sub-cobertura es tolerable, a menudo prefieres el plan que queda más cerca de la demanda en lugar de uno que siempre la iguala o supera. MinAbsDifference cambia el objetivo por exactamente eso — minimizar la brecha absoluta total entre recursos requeridos y programados — manteniendo la misma interfaz:
from pyworkforce.scheduling import MinAbsDifference
scheduler = MinAbsDifference(
num_days=1,
periods=7,
shifts_coverage=shifts_coverage,
required_resources=[required],
max_period_concurrency=sum(required),
max_shift_concurrency=max(required),
)
solution = scheduler.solve()
print(f"Estado: {solution['status']}, Costo: {solution['cost']}")
Cambiar una clase por otra te permite comparar un plan con cobertura garantizada contra un plan balanceado sin tocar el resto de tu código.
Más Allá del Headcount: El Pipeline Completo de Planeación
Calcular cuántos trabajadores necesitas es solo la primera etapa. pyworkforce compone todo el pipeline de gestión de personal:
- Queuing (
pyworkforce.queuing) — antes del scheduling, estima la demanda misma. El modeloErlangCconvierte el volumen de llamadas, el tiempo promedio de atención y un nivel de servicio objetivo en el número de posiciones requeridas por intervalo, considerando el shrinkage (tiempo no productivo del agente). - Scheduling (
pyworkforce.scheduling) — los solversMinRequiredResources/MinAbsDifferenceque usamos arriba, que convierten la demanda por período en headcount por turno. - Rostering (
pyworkforce.rostering) — una vez que sabes cuántas personas necesita cada turno, asigna individuos específicos a los turnos respetando reglas de descanso, turnos prohibidos y preferencias. Este es el tema del siguiente artículo.
Un plan realista los encadena: ErlangC produce los required_resources, el scheduler convierte eso en headcount por turno, y el roster asigna personas con nombre a cada turno.
Validando el Modelo
Antes de confiar en los resultados de cualquier modelo de optimización, es buena práctica hacer una validación básica:
- Verificar factibilidad: confirma que
solution["status"]seaOPTIMAL(oFEASIBLE) y que la cobertura de cada período cumpla la demanda. - Cota inferior: la demanda diaria máxima (
max(required) = 9) es una cota inferior del número de trabajadores. Si el solver devuelve menos, algo está mal. - Casos límite: prueba con demanda constante (el mismo valor todos los días) — para patrones de cinco días de trabajo y dos de descanso el resultado debería ser exactamente
ceil(demanda / 5 * 7)trabajadores. - Sensibilidad: modifica ligeramente la demanda y verifica que la solución cambie de forma coherente.
Conclusión
La optimización con restricciones convierte un problema de gestión de recursos aparentemente complejo en un modelo matemático que puede resolverse en milisegundos para casos típicos. pyworkforce democratiza el acceso a estas técnicas en Python: en lugar de codificar variables y restricciones a mano, describes el problema en términos de personal — turnos, cobertura, demanda — y la librería construye y resuelve el modelo de OR-Tools por ti.
La clave sigue siendo identificar correctamente las variables de decisión (¿qué patrones de trabajo existen?), la función objetivo (¿minimizar headcount o minimizar la brecha?) y las restricciones (¿cuánta cobertura necesita cada período?). Una vez que tienes esa claridad conceptual, pyworkforce se encarga del resto.
El siguiente paso natural es el scheduling: dado un número de trabajadores, ¿cómo asignas a cada persona específica a cada turno? Ese es el problema de rostering, y el tema del siguiente artículo de esta serie.
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.