Back to Office Post-Covid19

Antes de comenzar con este artículo quiero agradecer a Tristan Riquelme por darme la idea, es él alguien que siempre tiene excelentes ideas sobre dónde aplicar herramientas matemáticas a problemas reales.

Ahora comenzando con el artículo:
Aunque encontremos una cura definitiva para el COVID-19, este ya ha creado cambios culturales que seguirán con nosotros por mucho tiempo.
Uno de los grandes cambios, es que se demostró que el trabajo remoto es una posibilidad y en muchas ocasiones más eficiente que el trabajo presencial, pero eso no significa que no existan beneficios en el trabajo presencial. Es por eso que muchas empresas han optado por un sistema híbrido con turnos presenciales, los cuales se ajustan a los aforos existentes y las necesidades de la empresa.
En una empresa grande, decidir qué día va cada colaborador a trabajar es una tarea bastante compleja que abarca muchas dimensiones, por ejemplo:

  • que las personas correctas se topen
  • que se cumplan los aforos
  • preferencias de cada colaborador
  • número de veces a la semana que debe ir cada uno

El computador no es más inteligente que las personas, pero si tiene más ojos, por eso estos casos con muchas variables es mejor entregarselos al computador.La herramienta que utilizaremos será la programación matemática, donde la utilizaremos para decidir qué día va cada colaborador a trabajar, acorde a sus preferencias y las necesidades de la empresa.

En este caso, vamos a definir que las personas que deben toparse en la oficina son las del mismo equipo, haremos que cada persona pertenezca a uno y un día a la semana, todos los de un equipo deberán ir a la oficina y así realizar actividades de equipo.

Planteamiento del Modelo

Cargar Datasets

Para considerar las preferencias de cada colaborador, le pediremos a cada uno que asigne un puntaje a cada día del siguiente modo:
file

Comenzaremos cargando los datos a python y preparando la data:

>>> import pulp
>>> import itertools
>>> import pandas as pd
>>> pref = pd.read_excel("data_con_pref.xlsx","pref")
>>> pref = pref.fillna(0)
>>> print(pref)
       lun  mar  mie  jue  vie
colab                         
c1       1    4   -5    5   -5
c2      -3    5   -3    5   -4
c3       3    3   -5    4   -5
c4      -2    1    8    1   -8
c5      -4   -6    1    7    2
c6       3    3    3    1  -10
c7       2    2    2    2    2
c8      -8    3    3   -2    4
c9      -2   -8    4    2    4
c10      4    3    3   -5   -5
c11      0    5    2    3  -10
c12      1   -3   -7    8    1
c13      5    5   -3   -3   -4
c14     -2    5    5   -2   -6
c15      2   -3   -4   -3    8

Las preferencias se utilizarán para calcular el beneficio de seguirlas para el modelo, pero obviamente obligar a ir a trabajar a una persona un día que no es de su preferencia, es bastante molesto, por eso luego de escalar los puntajes, multiplicaremos por 2 los valores negativos.

>>> #escalado a 1 y doble ponderación de negativos
>>> 
>>> for c in pref.index:
...     pref.loc[c,:] = pref.loc[c,:]/pref.loc[c,:].abs().sum()
...     pref.loc[c,:] = [x if x > 0 else 2*x for x in pref.loc[c,:]]
... 
>>> print(pref)
        lun   mar   mie   jue   vie
colab                              
c1     0.05  0.20 -0.50  0.25 -0.50
c2    -0.30  0.25 -0.30  0.25 -0.40
c3     0.15  0.15 -0.50  0.20 -0.50
c4    -0.20  0.05  0.40  0.05 -0.80
c5    -0.40 -0.60  0.05  0.35  0.10
c6     0.15  0.15  0.15  0.05 -1.00
c7     0.20  0.20  0.20  0.20  0.20
c8    -0.80  0.15  0.15 -0.20  0.20
c9    -0.20 -0.80  0.20  0.10  0.20
c10    0.20  0.15  0.15 -0.50 -0.50
c11    0.00  0.25  0.10  0.15 -1.00
c12    0.05 -0.30 -0.70  0.40  0.05
c13    0.25  0.25 -0.30 -0.30 -0.40
c14   -0.20  0.25  0.25 -0.20 -0.60
c15    0.10 -0.30 -0.40 -0.30  0.40

además de una tabla con la pertenencia a cada equipo:
file

>>> team = pd.read_excel("data_con_pref.xlsx","team")
>>> team = team.set_index('colab')
>>> print(team)
      team
colab     
c1      t1
c2      t1
c3      t1
c4      t1
c5      t2
c6      t2
c7      t2
c8      t2
c9      t2
c10     t3
c11     t3
c12     t3
c13     t3
c14     t3
c15     t3

Dimensiones

Son como las coordenadas de los datos

  • colab: colaboradores
  • dias: día de la semana
  • team : equipo (team)

los sacamos de los mismos datos:

# indices
dias = pref.columns
colab = pref.index
teams = set(team["team"])

Variables de Decisión

Qué día va cada colaborador: 1 si va un día 0 si no:

CD = {e:{d:pulp.LpVariable(cat ='Binary', name ='{} el {}'.format(e, d)) for d in dias} for e in emp}

Cuál será el día que cada equipo se encontrara

TD = {t:{d:pulp.LpVariable(cat ='Binary', name ='{} el {}'.format(t, d)) for d in dias} for t in teams}

Función Objetivo

La función objetivo es la que nos permite comparar con un número diferentes combinaciones de turnos.

La función objetivo es discutible, podrían muchas por ejemplo: maximizar el colaborador que peor queda con respecto a sus preferencias o maximizar el beneficio global. En este caso construiremos el segundo caso, donde hay beneficio por ir los días de preferencia y por no ir los días sin preferencia o score negativo:

obj = \sum_{c,d}{CD_{c,d} * pref_{c,d} - (1-CD_{c,d}) * pref_{c,d} }

prob = pulp.LpProblem("DIAS", pulp.LpMaximize)
prob += pulp.lpSum([ CD[e][d] * pref.loc[e, d] - (1 - CD[e][d]) * pref.loc[e, d] for e, d in itertools.product(emp, dias)])

Restricciones

Por último definiremos la interacción de las variables como restricciones que reflejan las condiciones que hacen que una combinación de turnos sea válida:

Todos van 2 veces a la semana

\forall e \in colab \sum_{d \in dias} CD_{c,d} = 2

for e in emp:
    prob += pulp.lpSum([CD[e][d] for d in dias]) == 2
Cada equipo tiene su día

\forall t \in team \sum_{d \in dias} TD_{t,d} = 1

for t in teams:
    prob += pulp.lpSum([TD[t][d] for d in dias]) == 1
Todos van en el día del equipo
for e, t, d in itertools.product(colab,teams,dias):
    if team.loc[e,'team'] == t:
        prob += CD[e][d] >= TD[t][d]
Aforo máximo de 10 personas diarias en la oficina

\forall d \in dias \sum_{e \in colab} CD_{e,d} <= 10

for d in dias:
    prob += pulp.lpSum([CD[e][d] for e in emp]) <= 10

Resolvemos el problema y vemos los resultados:

solver = pulp.getSolver('PULP_CBC_CMD')
prob.solve(solver)

vemos la solución

vemos que en este caso solo 2 colaboradores tendrán que ir en días que no son de su preferencia

>>> for e,d in itertools.product(colab,dias):
...     if(pulp.value(CD[e][d]) > 0.9):
...         print('{} va el {} y esta {}'.format(e,d,'ok' if pref.loc[e,d] > 0 else 'mal'))
... 
c1 va el mar y esta ok
c1 va el jue y esta ok
c2 va el mar y esta ok
c2 va el jue y esta ok
c3 va el mar y esta ok
c3 va el jue y esta ok
c4 va el mie y esta ok
c4 va el jue y esta ok
c5 va el mie y esta ok
c5 va el jue y esta ok
c6 va el lun y esta ok
c6 va el mie y esta ok
c7 va el mar y esta ok
c7 va el mie y esta ok
c8 va el mie y esta ok
c8 va el vie y esta ok
c9 va el mie y esta ok
c9 va el vie y esta ok
c10 va el lun y esta ok
c10 va el mie y esta ok
c11 va el lun y esta mal
c11 va el mar y esta ok
c12 va el lun y esta ok
c12 va el jue y esta ok
c13 va el lun y esta ok
c13 va el mar y esta ok
c14 va el lun y esta mal
c14 va el mie y esta ok
c15 va el lun y esta ok
c15 va el vie y esta ok

obteniendo un score de 17.4

>>>print("Valor Objetivo")
>>>print(pulp.value(prob.objective))
17.400000000000006

si quieres ver el código lo puedes descargar desde mi github: https://github.com/danielfm123/turnos_back_to_office

Print Friendly, PDF & Email