├── requirements.txt ├── quarter.xlsx ├── workers.xlsx ├── example_inputs.py ├── LICENSE.txt ├── example_schedule.csv ├── README.md ├── requirements_integer_programming.txt └── model.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | pulp 3 | xlrd 4 | openpyxl 5 | -------------------------------------------------------------------------------- /quarter.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbiedma/shift-scheduling/HEAD/quarter.xlsx -------------------------------------------------------------------------------- /workers.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbiedma/shift-scheduling/HEAD/workers.xlsx -------------------------------------------------------------------------------- /example_inputs.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | NUM_WORKERS = 25 4 | AM_PERIODS = 42 5 | 6 | quarters = [8, 5, 8, 10, 10, 5, 12, 12, 12, 14, 10, 12, 12, 5, 14, 6, 5, 5, 5, 5, 5, 5, 10, 6, 8, 10, 10, 12] 7 | 8 | periods = [ 9 | "{} {}-{}".format( 10 | day, hour*4, (hour+1)*4 11 | ) for day in ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] for hour in range(6) 12 | ] 13 | 14 | worker_data = {} 15 | 16 | for worker in range(NUM_WORKERS): 17 | worker_data["worker{}".format(str(worker))] = { 18 | "period_avail": [random.randint(0,1) for period in range(AM_PERIODS)], 19 | "skill_level": random.randint(0,100), 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Luis Ariel Biedma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example_schedule.csv: -------------------------------------------------------------------------------- 1 | worker0, Mon 0-4, Mon 4-8, Wed 16-20, Wed 20-24, Fri 4-8, Sat 12-16, Sat 16-20, Sun 4-8, Sun 20-24 2 | worker1, Wed 4-8, Thu 16-20, Fri 16-20 3 | worker2, Wed 0-4, Sat 12-16 4 | worker3, Wed 16-20, Fri 16-20, Sat 8-12, Sun 8-12, Sun 16-20 5 | worker4, Tue 4-8, Tue 16-20, Thu 0-4, Sun 16-20 6 | worker5, Mon 0-4, Tue 16-20, Wed 4-8, Wed 20-24, Thu 12-16, Thu 16-20, Fri 4-8, Fri 12-16, Sun 16-20 7 | worker6, Thu 0-4, Thu 4-8, Fri 16-20 8 | worker7, Fri 20-24 9 | worker8, Mon 16-20, Tue 0-4, Tue 16-20, Wed 4-8, Thu 0-4, Thu 16-20, Sun 16-20 10 | worker9, Tue 0-4, Tue 16-20, Wed 4-8, Thu 0-4, Thu 4-8, Sun 12-16 11 | worker10, Mon 4-8, Tue 0-4, Tue 4-8, Thu 12-16, Fri 4-8, Fri 16-20, Sat 12-16, Sat 16-20 12 | worker11, Wed 4-8, Thu 12-16, Thu 16-20, Sat 4-8, Sun 20-24 13 | worker12, Mon 16-20, Mon 20-24, Tue 0-4, Tue 16-20, Wed 16-20, Thu 0-4, Thu 12-16, Thu 16-20, Sun 4-8, Sun 8-12, Sun 16-20 14 | worker13, Tue 16-20, Wed 16-20, Thu 16-20, Sun 4-8 15 | worker14, Mon 0-4, Mon 4-8, Mon 16-20, Tue 16-20, Wed 4-8, Wed 8-12, Thu 0-4, Thu 4-8, Thu 12-16, Sat 16-20, Sun 4-8 16 | worker15, Mon 0-4, Mon 16-20, Mon 20-24, Tue 4-8, Wed 4-8, Wed 16-20, Thu 12-16, Thu 16-20, Sun 4-8, Sun 8-12 17 | worker16, Mon 16-20, Tue 16-20, Wed 4-8, Wed 16-20, Sat 16-20, Sun 4-8, Sun 16-20 18 | worker17, Mon 16-20, Wed 4-8, Thu 0-4, Sat 4-8 19 | worker18, Mon 4-8, Mon 12-16, Tue 16-20, Wed 4-8, Wed 8-12, Wed 16-20, Sat 0-4, Sat 4-8, Sun 4-8 20 | worker19, Mon 16-20, Tue 0-4, Tue 4-8, Tue 16-20, Fri 4-8, Sat 4-8, Sat 12-16, Sat 16-20, Sun 16-20, Sun 20-24 21 | worker20, Mon 8-12, Wed 12-16, Wed 16-20, Thu 0-4, Thu 20-24, Fri 8-12, Sat 20-24, Sun 0-4, Sun 16-20 22 | worker21, Tue 12-16, Sat 12-16 23 | worker22, Mon 20-24, Tue 0-4, Tue 8-12, Thu 8-12, Thu 16-20 24 | worker23, Tue 16-20, Wed 4-8, Wed 20-24, Thu 4-8 25 | worker24, Tue 20-24, Wed 4-8, Wed 16-20, Fri 0-4, Sun 16-20 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shift-scheduling 2 | ### Shift Scheduling for workforce 3 | 4 | This is a shift planner, that takes data from Excel files (quarter.xlsx and workers.xlsx) and returns a CSV with weekly shifts for each worker. 5 | 6 | ### Problem Description 7 | 8 | Suppose we have a place that needs to work 24/7, and we have a minimum amount of workers needed to run it on each quarter of day in the week (Monday from 0 to 6, Monday from 6 to 12, ... Sunday from 12 to 18, Sunday from 18 to 24). 9 | We have to create a shift schedule that is subject to certain constraints. 10 | 11 | In this case, the constraints added are: 12 | * We have each worker and their weekly availability in an Excel file, along with their "skill level" (from 1 to 100). 13 | * Every worker has to have a 12 hour rest per day. 14 | * Every worker has to have a 24 hour rest per week. 15 | * Every worker has to work at most 12 hours for each day. 16 | * Work days are separated in six 4-hour shifts (0-4, 4-8, 8-12, 12-16, 16-20, 20-24). 17 | * A worker with skill level < 25 can't be left alone. 18 | 19 | ### Output 20 | 21 | This program returns the turns for each worker during the week, according to the constraints, in a CSV file called schedule.csv. 22 | 23 | ### Execution 24 | 25 | To run, you have to install Pandas and PuLP. 26 | Then, in shell: 27 | 28 | python model.py 29 | 30 | It will take around a minute to solve, depending on the computer. 31 | Then, we will have, for every worker in worker_data, a dictionary called "schedule", where it tells which period corresponds to each worker. 32 | 33 | ### Some reading 34 | 35 | This work was done adapting the idea from: https://www.me.utexas.edu/~jensen/ORMM/models/unit/linear/subunits/workforce/index.html, adding constraints where it was needed. 36 | 37 | 38 | ### NEXT STEPS 39 | 40 | Add more flexible shifts, including the capability of scheduling breaks, this can be done following article: https://link.springer.com/article/10.1007/s10479-019-03487-6. 41 | -------------------------------------------------------------------------------- /requirements_integer_programming.txt: -------------------------------------------------------------------------------- 1 | INPUT #1: NUMBER OF EMPLOYEES NEEDED PER QUARTER OF DAY: 2 | =DayOfWeek 3 | =Quarter of the day 4 | =Employees Needed For each of the quarter of the day 5 | INPUT #2: EMPLOYEES AVAILABILITY AND SHIFTS RESTRICTIONS 6 | Employee ID 7 | from what time to what time he is an available to shift 8 | Skill Level(From 0 to 100) a skill level <= 25 can't be Alone 9 | ---------------------------------------------------------- 10 | Others Global Restrictions 11 | Minimum number of hours he requires if work=2H 12 | maximum number of hours he can work during the day=12H 13 | Maximum consecutive hour of work without break=6H 14 | Minimum break duration =30 Min 15 | 12H rest between the end of the previous day's shift and the beginning of the next day's shift. 16 | ---------------------------------------------------------------------------------------- 17 | We want the program to give us the scheduling, AND satisfying all the restrictions 18 | OUTPUT: CSV FILE Telling us for each employee what should be the shifts, while satisfying 19 | the required number of employees + the Employee shift requirements. 20 | 21 | ---------------------------------------------------------------------------------------- 22 | Constraints per Day 23 | 24 | An employee must work only in his or her available hours. 25 | Minimum duration if work : 2H 26 | Maximum daily duration: 10 hours 27 | Minimum break of 45 minutes every 6 hours 28 | Daily hourly amplitude: 13 hours 29 | ------------------------------------------------------------------------------------------ 30 | Weekly Constraints 31 | 32 | Rest time between 2 days: 12 hours 33 | Weekly rest: 2 consecutive days 34 | Absolute maximum hourly duration: 48 hours 35 | Each employee must plan his or her quota of hours. 36 | ------------------------------------------------------------------------------------------ 37 | I 38 | for each week you have 7 data entry like shift_requirements = [8,5,8,10,10,5,12,12,12,14,10,12, 39 | 12,5,14,6,5,5,5,5,5,5,10,6,8,10,10,12,10,15,15,16,16,16,14,10,12,5,5,6,5,5,5,3,5,5,5,5]; 40 | 41 | for the disponibility of each staff member for each day 42 | 43 | The python script should return the following data : "StafFWorkerA": { 44 | "Monday": [24-31,39-44], 45 | "ETC": [24-31,39-44], 46 | }, 47 | "StafFWorkerB": { 48 | 49 | ------------------------------------------------------------------------------------------- 50 | 51 | -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | import pandas 3 | import pulp 4 | 5 | from example_inputs import ( 6 | periods, 7 | ) 8 | 9 | AM_PERIODS = 42 10 | AM_QUARTERS = 28 11 | 12 | # Divide week in 42 4-hour periods. 0 = Monday 0-4, 1 = Monday 4-8, 2 = Monday 8-12........ 39 = Sunday 12-16, 40 = Sunday 16-20, 41 = Sunday 20-24 13 | # 14 | 15 | # workers_data = { 16 | # worker1: { 17 | # "period_avail": [0, 1, 1, 1, 0, 0,........, 1, 1, 0, 0]. Length 42, 18 | # "skill_level": int in 0-100 19 | # } 20 | # } 21 | 22 | # quarters = [5, 4, 10, .... , 8, 9, 12] Amount of workers needed for each quarter of day. Length 28 23 | 24 | def model_problem(): 25 | 26 | workerdf = pandas.read_excel("workers.xlsx", header=0) 27 | workers_data = {} 28 | for iteration in workerdf.iterrows(): 29 | row = iteration[1] 30 | name = row[0] 31 | workers_data[name] = {} 32 | workers_data[name]["skill_level"] = row[1] 33 | workers_data[name]["period_avail"] = [] 34 | for day in range(7): 35 | for period in range(6): 36 | workers_data[name]["period_avail"].append( 37 | int((period*4 >= row[2 + day * 2]) and ((period+1)*4 <= row[2 + day*2 + 1])) 38 | ) 39 | 40 | quarters = pandas.read_excel("./quarter.xlsx", header=0).loc[0].tolist() 41 | 42 | problem = pulp.LpProblem("ScheduleWorkers", pulp.LpMinimize) 43 | 44 | workerid = 0 45 | for worker in workers_data.keys(): 46 | workerstr = str(workerid) 47 | periodid = 0 48 | 49 | workers_data[worker]["worked_periods"] = [] 50 | workers_data[worker]["rest_periods"] = [] 51 | workers_data[worker]["weekend_periods"] = [] 52 | 53 | for period in workers_data[worker]["period_avail"]: 54 | 55 | periodstr = str(periodid) 56 | # worked periods: worker W works in period P 57 | workers_data[worker]["worked_periods"].append( 58 | pulp.LpVariable("x_{}_{}".format(workerstr, periodstr), cat=pulp.LpBinary, upBound=period) 59 | ) 60 | # rest periods: worker W takes a 12-hour rest starting on period P 61 | workers_data[worker]["rest_periods"].append( 62 | pulp.LpVariable("d_{}_{}".format(workerstr, periodstr), cat=pulp.LpBinary) 63 | ) 64 | # weekend periods: worker W takes a 48-hour rest starting on period P 65 | workers_data[worker]["weekend_periods"].append( 66 | pulp.LpVariable("f_{}_{}".format(workerstr, periodstr), cat=pulp.LpBinary) 67 | ) 68 | 69 | periodid += 1 70 | 71 | workerid += 1 72 | 73 | # Create objective function (amount of turns worked) 74 | objective_function = None 75 | for worker in workers_data.keys(): 76 | objective_function += sum(workers_data[worker]["worked_periods"]) 77 | 78 | problem += objective_function 79 | 80 | # Every quarter minimum workers constraint 81 | for quarter in range(AM_QUARTERS): 82 | workquartsum = None 83 | for worker in workers_data.keys(): 84 | workquartsum += workers_data[worker]["worked_periods"][quarter + quarter // 2] + workers_data[worker]["worked_periods"][quarter + quarter // 2 + 1] 85 | 86 | problem += workquartsum >= quarters[quarter] 87 | 88 | # No worker with skill <= 25 is left alone 89 | for period in range(AM_PERIODS): 90 | skillperiodsum = None 91 | for worker in workers_data.keys(): 92 | skillperiodsum += workers_data[worker]["worked_periods"][period] * workers_data[worker]["skill_level"] 93 | 94 | problem += skillperiodsum >= 26 95 | 96 | # Each worker must have one 12-hour break per day 97 | for day in range(7): 98 | for worker in workers_data.keys(): 99 | problem += sum(workers_data[worker]["rest_periods"][day * 6:(day + 1) * 6]) >= 1 100 | 101 | # If a worker takes a 12-hour break, can't work in the immediate 3 periods 102 | 103 | for period in range(AM_PERIODS): 104 | for worker in workers_data.keys(): 105 | access_list = [period, (period + 1) % 42, (period + 2) % 42] 106 | problem += sum(list(itemgetter(*access_list)(workers_data[worker]["worked_periods"]))) <= 3 * (1 - workers_data[worker]["rest_periods"][period]) 107 | 108 | # A worker can't work more than 12 hours every 24 hours 109 | for period in range(AM_PERIODS): 110 | for worker in workers_data.keys(): 111 | access_list = [period, (period + 1) % 42, (period + 2) % 42, (period + 3) % 42, (period + 4) % 42, (period + 5) % 42] 112 | problem += sum(list(itemgetter(*access_list)(workers_data[worker]["worked_periods"]))) <= 3 113 | 114 | # Each worker must have one 48-hour break per week 115 | 116 | for worker in workers_data.keys(): 117 | problem += sum(workers_data[worker]["weekend_periods"]) == 1 118 | 119 | # If a worker takes a 48-hour break, can't work in the inmediate 12 periods 120 | 121 | for period in range(AM_PERIODS): 122 | for worker in workers_data.keys(): 123 | for miniperiod in range(12): 124 | problem += workers_data[worker]["worked_periods"][(period + miniperiod) % AM_PERIODS] <= (1 - workers_data[worker]["weekend_periods"][period]) 125 | problem += workers_data[worker]["worked_periods"][(period + 12) % AM_PERIODS] >= workers_data[worker]["weekend_periods"][period] 126 | 127 | try: 128 | problem.solve() 129 | except Exception as e: 130 | print("Can't solve problem: {}".format(e)) 131 | 132 | for worker in workers_data.keys(): 133 | workers_data[worker]["schedule"] = [] 134 | for element in range(len(workers_data[worker]["worked_periods"])): 135 | if workers_data[worker]["worked_periods"][element].varValue == 1: 136 | workers_data[worker]["schedule"].append(periods[element]) 137 | 138 | return problem, workers_data 139 | 140 | if __name__ == "__main__": 141 | problem, workers_data = model_problem() 142 | 143 | f = open("./schedule.csv", "w") 144 | for worker in workers_data.keys(): 145 | f.write(worker) 146 | for element in workers_data[worker]["schedule"]: 147 | f.write(", " + element) 148 | f.write("\n") 149 | f.close() 150 | --------------------------------------------------------------------------------