Reputation: 129
I am trying to build an employee scheduling system for a clinic using the PuLP library. However, I keep getting the warning mentioned in the title whenever I try adding constraints as shown in the code below. I used the code in this case study found in the PuLP documentation as a reference to build the LP problem.
A quick summary of the problem: The clinic has 3 locations and I'm building a system that can build an optimal solution to scheduling a list of employees across these 3 locations. We are not scheduling/counting hours, but rather just scheduling by days (e.g. Jim works on Monday, Tuesday, and Friday). Each clinic has requirements for the number of employees of specific specialties (which I call roles in the code below) needed for each day. For now, I am trying to add a constraint that limits the number of employees of a specific role that can be scheduled at a specific location of a certain day.
The function maxnum_(day,location,role) for now just returns 3 (to test the constraint)(i.e. the max number of employees that can be scheduled at any location is 3). When I set the constraint using <= (as it should be) the program completes execution, but when I print the shift_model, I do not see any of the constraints being added. Furthermore, in attempt of trying to further explore the problem, I changed <= to == and the >=. For both cases, I received the warning of overwriting the objective function even though it does not seem like I am...
from pulp import *
def maxnum_(d,l,r):
return 3
def coefficients(instance):
#Given a shift instance, returns the weight of the preference
#based on the parameters of the instance.
#Chosen values are subject to change.
weight = 0
employee = instance[0]
day = instance[1]
location = instance[2]
role = instance[3]
global allEmployees
if day not in allEmployees[employee]['Availability']:
weight -= 5
else:
weight += 1
if location not in allEmployees[employee]['PreferredLocationOfWork']:
weight -= 2
else:
weight+=1
return weight
shifts = ['M1','M2','T1','T2','W1','W2','R1','R2','F1','F2']
allEmployees = {'Billy Bob': {'License': 'Nurse Practitioner', 'Specialty': 'Urgent', 'Age': 'Geriatric', 'Availability': ['M1', 'T1', 'F1', 'M2', 'R2', 'F2'], 'PreferredLocationOfWork': 'PPHC', 'Requested_dates_off': ['2020-05-09', '2021-01-31', 'YYYY-MM-DD']}, 'Jimmy John': {'License': 'Physician', 'Specialty': 'Emergency', 'Age': 'Pediatric', 'Availability': ['T1', 'F1', 'W2', 'R2'], 'PreferredLocationOfWork': 'CHCF', 'Requested_dates_off': ['2020-05-09', '2021-01-31', 'YYYY-MM-DD']}}
# Ignoring specialty/age/license required and min number employees required for now, will implement later
allLocations = {'CHCF': {'MinNumberEmployeesRequiredPresent': 1, 'SpecialtyRequired': ['Emergency', 'Urgent', 'Urgent'], 'AgeRequired': ['Pediatric', 'Geriatric', 'Pediatric'], 'LicenseRequired': ['Physician', 'NurseMidwife', 'NursePracticioner']}, 'THS': {'MinNumberEmployeesRequiredPresent': 1, 'SpecialtyRequired': ['Emergency', 'Urgent', 'Primary', 'Obstetrics'], 'AgeRequired': ['Pediatric', 'Geriatric', 'Family', 'Adult'], 'LicenseRequired': ['Physician', 'NurseMidwife', 'NursePracticioner', 'Physician']}, 'PPHC': {'MinNumberEmployeesRequiredPresent': 1, 'SpecialtyRequired': ['Urgent', 'Urgent', 'Urgent'], 'AgeRequired': ['Geriatric', 'Geriatric', 'Pediatric'], 'LicenseRequired': ['Physician', 'NurseMidwife', 'NursePracticioner']}}
age_ = ['Pediatric', 'Adult','Geriatric', 'Family']
specialty_ = ['Emergency', 'Urgent Care', 'Primary Care', 'Obstetrics']
license_ = ['Physician','Nurse Midwife', 'Nurse Practitioner']
roles = [','.join([a,s,l]) for a in age_ for s in specialty_ for l in license_ ]
listOfVariables = []
# Start creating the tuples of shift instances, these will be the variables of our LP problem
for employee in allEmployees.keys():
specialty = []
specialty.append(allEmployees[employee]['Age'])
specialty.append(allEmployees[employee]['Specialty'])
specialty.append(allEmployees[employee]['License'])
specialtyString = ','.join(specialty)
#TODO: Implement weighted alternates...
for day in shifts:
# Include all locations, set preferred location coefficient to 10 and non preferred to 1?
for location in allLocations.keys():
# In coefficients, set days not in preference to 1, all preferred dates to 10
listOfVariables.append((employee, day, location, specialtyString))
x = LpVariable.dicts('shift', listOfVariables, lowBound = 0, upBound = 1, cat = LpInteger)
shift_model = LpProblem("Employee_Scheduling_Model" , LpMaximize)
# The objective function
shift_model += sum([coefficients(shift_instance) * x[shift_instance] for shift_instance \
in listOfVariables])
# Add Constraint limiting the number of possible employees of a specific role to schedule on a given day at a given location
for day in shifts: # for each day in pay period
for location in allLocations.keys(): # for each clinic
for role in roles: # for each role
shift_model += sum([x[shift_instance] for shift_instance in listOfVariables if day in shift_instance and location in shift_instance\
and role in shift_instance]) == maxnum_(day, location, role), "Max employees for {} {} {}".format(day,location,role)
shift_model.solve()
print("Optimal employee schedule: ")
for shift_instance in listOfVariables:
if x[shift_instance].value() == 1.0:
print(x[shift_instance])
The shift_model is a summation of tuples (e, d, l, r) multiplied with a calculated coefficient. The tuple is a "shift_instance" where employee e works on day d at location l for role r. These tuples are the variables of the problem and can either be 0 or 1, where 1 indicates that the that shift_instance will be part of the schedule. The coefficient calculated is pretty much the preferences of the employee (e.g. if an Jimmy John is not available on Tuesdays, the the coefficient for ('Jimmy John', 'Tuesday', "Clinic A', 'Pediatrician') is a negative number, whereas if he was available, then it would be a positive number). Thus the objective is to maximize this model. Oh, and x is a dictionary mapping its shift_instance to its LpVariable and listOfVariables is a list of all the possible tuples/shift_instances.
My question is, why am I getting these warnings and why is the constraint not being added to the LpProblem?
Upvotes: 0
Views: 1125
Reputation: 864
Good day!
You should not use python's standard sum
function tu sum expressions or variables in pulp. You should be using the lpSum
function provided by the package. It's not only more efficient but, in this case, it solves your issue. I'm unable to explain why, though.
So, in the code of the constraint you should have:
shift_model += lpSum([coefficients(shift_instance) * x[shift_instance] for shift_instance \
in listOfVariables])
# Add Constraint limiting the number of possible employees of a specific role to schedule on a given day at a given location
for day in shifts: # for each day in pay period
for location in allLocations.keys(): # for each clinic
for role in roles: # for each role
shift_model += lpSum([x[shift_instance] for shift_instance in listOfVariables if day in shift_instance and location in shift_instance\
and role in shift_instance]) == maxnum_(day, location, role), "Max employees for {} {} {}".format(day,location,role)
Also, and this is a general recomendation about performance, it's more efficient to pre-filter your dictionary of variables before iterating. It also makes for cleaner code. Below is the edited model part:
x = LpVariable.dicts('shift', listOfVariables, lowBound = 0, upBound = 1, cat = LpInteger)
shift_model = LpProblem("Employee_Scheduling_Model" , LpMaximize)
# The objective function
shift_model += lpSum([coefficients(shift_instance) * x[shift_instance] for shift_instance \
in listOfVariables])
# for each day, location and role: a list of variables
x_dlr = {}
for (e, d, l, r), _x in x.items():
_tup = d, l, r
if _tup not in x_dlr:
x_dlr[_tup] = []
x_dlr[_tup].append(_x)
# Add Constraint limiting the number of possible employees of a specific role to schedule on a given day at a given location
for day in shifts: # for each day in pay period
for location in allLocations.keys(): # for each clinic
for role in roles: # for each role
_tup = day, location, role
shift_model += lpSum(x_dlr.get(_tup, [])) == maxnum_(*_tup), "Max employees for {} {} {}".format(day,location,role)
shift_model.solve()
print("Optimal employee schedule: ")
for shift_instance in listOfVariables:
if x[shift_instance].value() == 1.0:
print(x[shift_instance])
Upvotes: 2