Back to Office Post-Covid19

Before starting this article I want to thank Tristan Riquelme for giving me the idea, he is someone who always has excellent ideas about where to apply mathematical tools to real problems.

Now starting with the article:
Although we find a definitive cure for COVID-19, it has already created cultural changes that will stay with us for a long time.
One of the big changes is that it was demonstrated that remote work is a possibility and in many cases more efficient than in-person work, but that doesn’t mean there are no benefits to in-person work. That is why many companies have opted for a hybrid system with in-person shifts, which adjust to existing capacity limits and the needs of the company.
In a large company, deciding which day each collaborator goes to work is a fairly complex task that covers many dimensions, for example:

  • that the right people meet each other
  • that capacity limits are met
  • each collaborator’s preferences
  • number of times per week each one must go

The computer is not smarter than people, but it does have more eyes, that is why these cases with many variables are better left to the computer. The tool we will use will be mathematical programming, where we will use it to decide which day each collaborator goes to work, according to their preferences and the needs of the company.

In this case, we are going to define that the people who must meet in the office are those from the same team, we will make each person belong to one day per week, everyone on a team will have to go to the office and thus carry out team activities.

Model Formulation

Load Datasets

To consider each collaborator’s preferences, we will ask each one to assign a score to each day as follows:
file

We will start by loading the data into python and preparing the 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

The preferences will be used to calculate the benefit of following them for the model, but obviously forcing a person to go to work on a day that is not their preference is quite annoying, so after scaling the scores, we will multiply the negative values by 2.

>>> #scaled to 1 and double weighting of negatives
>>>
>>> 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

in addition to a table with the membership of each team:
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

Dimensions

They are like the coordinates of the data

  • colab: collaborators
  • dias: day of the week
  • team: team

we extract them from the data itself:

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

Decision Variables

Which day each collaborator goes: 1 if they go on a day, 0 if not:

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

Which day each team will meet

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

Objective Function

The objective function is what allows us to compare different shift combinations with a number.

The objective function is debatable, there could be many, for example: maximize the worst-off collaborator with respect to their preferences or maximize the overall benefit. In this case we will build the second case, where there is a benefit for going on preferred days and for not going on days without preference or with a negative score:

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)])

Constraints

Finally we will define the interaction of the variables as constraints that reflect the conditions that make a combination of shifts valid:

Everyone goes 2 times a week

\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
Each team has its day

\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
Everyone goes on their team’s day
for e, t, d in itertools.product(colab,teams,dias):
    if team.loc[e,'team'] == t:
        prob += CD[e][d] >= TD[t][d]
Maximum capacity of 10 people per day in the office

\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

We solve the problem and look at the results:

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

We look at the solution

We see that in this case only 2 collaborators will have to go on days that are not their preference

>>> for e,d in itertools.product(colab,dias):
...     if(pulp.value(CD[e][d]) > 0.9):
...         print('{} goes on {} and is {}'.format(e,d,'ok' if pref.loc[e,d] > 0 else 'not ok'))
...
c1 goes on tue and is ok
c1 goes on thu and is ok
c2 goes on tue and is ok
c2 goes on thu and is ok
c3 goes on tue and is ok
c3 goes on thu and is ok
c4 goes on wed and is ok
c4 goes on thu and is ok
c5 goes on wed and is ok
c5 goes on thu and is ok
c6 goes on mon and is ok
c6 goes on wed and is ok
c7 goes on tue and is ok
c7 goes on wed and is ok
c8 goes on wed and is ok
c8 goes on fri and is ok
c9 goes on wed and is ok
c9 goes on fri and is ok
c10 goes on mon and is ok
c10 goes on wed and is ok
c11 goes on mon and is not ok
c11 goes on tue and is ok
c12 goes on mon and is ok
c12 goes on thu and is ok
c13 goes on mon and is ok
c13 goes on tue and is ok
c14 goes on mon and is not ok
c14 goes on wed and is ok
c15 goes on mon and is ok
c15 goes on fri and is ok

obtaining a score of 17.4

>>>print("Objective Value")
>>>print(pulp.value(prob.objective))
17.400000000000006

if you want to see the code you can download it from my github: https://github.com/danielfm123/turnos_back_to_office

Be the first to comment

Leave a Reply

Your email address will not be published.




This site uses Akismet to reduce spam. Learn how your comment data is processed.