Investigación de Operaciones
Python
pyworkforce
OR-Tools
optimización
planeación de personal
scheduling

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.

22 de enero de 2026·8 min read

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íaAgentes Requeridos
Lunes5
Martes8
Miércoles9
Jueves7
Viernes6
Sábado4
Domingo3

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:

Minimizar:pxp\text{Minimizar:} \quad \sum_{p} x_p

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:

pcp,ixpdii{0,,6}\sum_{p} c_{p,i}\, x_p \geq d_i \quad \forall\, i \in \{0,\ldots,6\}

Además, todas las variables deben ser enteros no negativos, ya que no podemos contratar fracciones de personas:

xpZ0px_p \in \mathbb{Z}_{\geq 0} \quad \forall\, p

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.

bash
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).

python
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:

python
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:

text
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:

python
# 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:

python
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 modelo ErlangC convierte 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 solvers MinRequiredResources / MinAbsDifference que 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:

  1. Verificar factibilidad: confirma que solution["status"] sea OPTIMAL (o FEASIBLE) y que la cobertura de cada período cumpla la demanda.
  2. 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.
  3. 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.
  4. 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.



Rodrigo Arenas — ML Engineer & Open Source Maintainer

¿Estás trabajando en algo similar? Puedo ayudarte a llevarlo a producción.

¿Vas a construir una plataforma de IA o de datos?

Productos, plataformas y software open source para IA, Data Engineering y Machine Learning.

Ver proyectos
Visión

Construyendo un ecosistema de productos y software open source para IA, Data Engineering y Machine Learning.

Proyectos
sklearn-genetic-optPyWorkforceCiaren

Iterixs (En construcción)

© 2026 Rodrigo Arenas