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:

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:

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

Leave a Reply