carloalbe
A-nurse-scheduling-problem-solved-with-Gurobi
Jupyter Notebook

An LP problem with hiearchical objectives, modeled as a vehicle routing problem , implemented with gurobipy python API in a jupyter notebook, and solved by using the Gurobi solver

Last updated Feb 3, 2026
25
Stars
3
Forks
2
Issues
0
Stars/day
Attention Score
9
Language breakdown
Jupyter Notebook 100.0%
Files click to expand
README

Nurse Scheduling Problem

A large hospital has to cover a set of day-care activities with the available nurses.Each nurse can perform different subsets of the activities, which are implicitly definedby time and skill constraints. In particular, for each activity the starting time andthe ending time are given, so it is known whether or not it is possible for the samenurse to perform both in sequence (it is assumed that it takes a short enough timeto move across the hospital so that this can be ignored). Furthermore, nurses can beof three skill levels (beginner, intermediate, advanced), and each activity is markedwith the required skill level: only nurses with the required skill, or one above, canperform it. Union regulations dictate the maximum working time (sum of the timeshe’s performing activities) for each nurse; furthermore, nurses can’t be left waitingfor more that a given period (say, two hours) between subsequent activities. Giventhe available number of nurses with each skill level, the problem is to assign a feasibleset of duties to the smallest possible number of nurses in order to have each activityperformed by exactly one of them. Among solutions with the same number of nurses,these where less nurses perform activities requiring a skill level below their own arepreferred; the more the skill level is below, the more this should be avoided (but notat the cost of using more than the minimum number of nurses).

Note: You can download the repository containing this and other examples by clicking here In order to run this Jupyter Notebook properly, you must have a Gurobi license. If you do not have one, you can request an evaluation license as a commercial user, or download a free license as an academic user.

# This command imports the Gurobi functions and classes.

import gurobipy as gp #this is package provided by Gurobi to model problems from gurobipy import GRB #and interact with the solver #Manage the data import pandas as pd import numpy as np #Graphic tools import matplotlib import matplotlib.pyplot as plt

Mathematical Model

The model is an oriented graph representation in which each activity is a node of the graph, connected each other if them can be performed consecutively. Every link has capacity 1 and the nurses are represented by the flow that moves in this network, they start from the source and come back to the source. Is a 3-index model similar to the one typically used for the vehicle routing problem.
#INITIALIZATION OF THE MODEL

The Model() constructor creates a model object m. The name of the model object m is Nurses Scheduling

m = None m = gp.Model('Nurses Scheduling')

#READING DAT FROM CSV FILES activities = pd.read_csv("activities.csv") nurses = pd.read_csv("nurses.csv")

Data definition

Input data are defined over the following sets:

  • K is the set of the nurses, two values are assigned to each nurse: level and maximum work time;
  • Ais the set of the activities, each activity has three values: level, start time, end time;
  • I is the super-set of A united with the auxiliary activities 0 that has the function of source;
  • W is the maximum waiting time between two activities done by the same nurse.

#SETS
I = range(len(activities) + 1) #index for all the tasks more the source
K = range(len(nurses)) #index for all the nurse
A = I[1:]       #index for all tasks (source and thin are exclused)

#PARAMETERS q = list(nurses['maxh'].copy()) l = list(nurses['level'].copy()) s = list(activities['start_time'].copy()) t = list(activities['end_time'].copy()) h = list(activities['hard'].copy())

Source

For use the graph in a better and easiest way, a source and a thi are added at the sety of athe activities,rispectively as the first and the last one.

Three main sets are defined:

    • 'K' for the nurses
    • 'A' and 'I' for the activities
N.B.While 'A' contains all and only the activities 'I' contains also the source :
    • I[0] : recall the source
    • I[1:]: recall all tasks

#SOURCE
s.insert(0,0)
t.insert(0,0)
h.insert(0,0)

Support Sets

Backward star \& forward star

For each activity we defined his Backward Star and Forward Star, constraining the duration of the waiting time between two activity to be less than W.
#defining the  backward star 
back=[]
back.append([])  #source has an empty set as backward star
for i in I[1:-1]:
    backi=[0]
    for j in I[1:-1]:
        if (s[i]>=t[j] and s[i]-t[j]<=2):
            backi.append(j)
    back.append(backi)
back.append([i for i in I[1:-1]]) #instead, tink has all other nodes except
                                  # the source and itself
#defining the forward star

forward=[] forward.append([i for i in I[1:-1]]) #all tasks for the source for i in I[1:-1]: forwardi=[I[-1]] for j in I[1:-1]: if (s[j]>=t[i] and s[j]-t[i]<=2): forwardi.append(j) forward.append(forwardi) forward.append([]) #empty forward for the thin

Level compatible

We defined support sets for activities and nurses. For each nurse a subset of activities that can be assigned to the nurse and vice-versa.
#SUBSETS OF NURSES
levelok = []
levelok.append([k for k in K])
for i in A:
    levelokk = []
    for k in K:
        if l[k] >= h[i] : levelokk.append(k)
    levelok.append(levelokk)
levelok.append([k for k in K])
KL = levelok.copy()

#SUBSETS OF ACTIVITIES levelok = [] for k in K: levelokk = [0] for i in A: if l[k] >= h[i] : levelokk.append(i) levelokk.append(I[-1]) levelok.append(levelokk) AL = levelok.copy()

Decision Variables

We defined the binary variable: $x_{ij}^{k}$ that represent the transition of a nurse from a terminated activity to the next one (both assigned to a same nurse).
#Only the necessary variables are instanciated
variables = []
for i in I:
    for j in I:
        if (s[j] >= t[i] and s[j] <= t[i]+2) or (i== 0 and j!= 0 )or ( i!=0 and j == 0) : # j in forward of i
            for k in K:
                if h[i] <= l[k] and h[j] <= l[k] and q[k]>= t[i] - s[i] + t[j] - s[j]: # k is able to do both i and j
                    variables.append((i,j,k))
                        
X = m.addVars(variables, vtype=GRB.BINARY, name="x")    #route variables

df = pd.DataFrame(variables, columns = ['I', 'J', 'K'])

Objectives

  • The main objective minimize the number of nurses working during the day by minimizing the sum of nurses leaving the node 0 (`Source').
  • The secondary minimize the total difference between the level of the nurse and the tasks in which are involved.
Since the objectives hierarchical, we can solve the primary, set it as a constraint and then solve the second. Having a solution for the first problem, allow the solver to concentrate more in is solving the secondary one that is the hardest one. However Gurobi offers a hierachical objective instantiation.
#OBJECTIVE

The setObjectiveN() method of the model object m allows to define multiple objectives.

The first argument is the linear expression defining the most important objective, called primary objective, in this case

it is the minimization of extra workers required to satisfy shift requirements. The second argument is the index of the

objective function, we set the index of the primary objective to be equal to 0. The third argument is the priority of the

objective. The fourth argument is the relative tolerance to degrade this objective when a lower priority

objective is optimized. The fifth argument is the name of this objective.

A hierarchical or lexicographic approach assigns a priority to each objective, and optimizes for the objectives in

decreasing priority order. For this problem, we have two objectives, and this objective has the highest priority which is

equal to 2. When the secondary objective is minimized, since the relative tolerance is 0.2, we can only degrade the

minimum number of extra workers up to 20%. BUT IN OUR CASE MUST BE 0

#main m.setObjectiveN(gp.quicksum(X[(i,j,k)]for (i,j,k) in df[df.I == 0].values), index = 0,priority=2,name= 'number of working nurses')

#secondary m.setObjectiveN( gp.quicksum(X[(i,j,k)]*(l[k]-h[i]) for (i,j,k) in df[df.I != 0].values), index = 1, priority=1,name="skill not used")

m.ModelSense = gp.GRB.MINIMIZE

Constraints

We defined 4 constraints:
  • Each nurse doesn't exceed his maximum time of work;
  • Each activity is carried out by exactly one nurse;
  • Each working nurse can exit from the source node exactly one time;
  • If a nurse is not assigned to any activity it remains in the source, instead if the nurse is assigned to some activities, this constraint assures us that the nurse will exits from the source and will return to the source.
#COSTRAINTS
#1 sum of time requests to work at the task to which a nurse is assigned can't be more than 

the max quantity of time

for k in K : m.addConstr(gp.quicksum(X[(i,j,k)]*(t[i]-s[i]) for (i,j,k) in df[(df.K == k)&(df.I!=0)].values) <= q[k] , name='maxhours')

#2) each activity must be assigned to exactly one nurse for i in A: m.addConstr(gp.quicksum(X[(i,j,k)] for (i,j,k) in df[df.I==i].values) == 1 , name='Assignment') m.addConstr(gp.quicksum(X[(j,i,k)] for (j,i,k) in df[df.J==i].values) == 1 , name='Assignment')

#3) each activity, for each nurse, must have the same number of incoming arc and outgoing arcs

and those must exists only if the task is assigned to that nurse

for i in A: for k in set(df[df.I==i].K.values): m.addConstr(gp.quicksum(X[(i,j,k)] for (i,j,k) in df[(df.I==i)&(df.K==k)].values) == gp.quicksum(X[(j,i,k)] for (j,i,k) in df[(df.J==i)&(df.K==k)].values))

#4) each nurse that go out from the source (start to work), must end his day at the thin for k in K : m.addConstr(gp.quicksum(X[(i,j,k)] for (i,j,k) in df[(df.I==0)&(df.K==k)].values) == gp.quicksum(X[(i,j,k)] for (i,j,k) in df[(df.J==0)&(df.K==k)].values)) m.addConstr(gp.quicksum(X[(i,j,k)] for (i,j,k) in df[(df.I==0)&(df.K==k)].values)<=1)

Solving

m.params.TimeLimit = 300 #setting a time limit of 300 seconds
m.optimize()

Changed value of parameter TimeLimit to 300.0 Prev: inf Min: 0.0 Max: inf Default: inf Gurobi Optimizer version 9.0.1 build v9.0.1rc0 (win64) Optimize a model with 769 rows, 2017 columns and 8747 nonzeros Model fingerprint: 0xae5d746b Variable types: 0 continuous, 2017 integer (2017 binary) Coefficient statistics: Matrix range [1e+00, 4e+00] Objective range [1e+00, 2e+00] Bounds range [1e+00, 1e+00] RHS range [1e+00, 7e+00] --------------------------------------------------------------------------- Multi-objectives: starting optimization with 2 objectives ... --------------------------------------------------------------------------- Multi-objectives: applying initial presolve ... --------------------------------------------------------------------------- Presolve removed 213 rows and 203 columns Presolve time: 0.09s Presolved: 556 rows and 1814 columns --------------------------------------------------------------------------- Multi-objectives: optimize objective 1 (number of working ) ... --------------------------------------------------------------------------- Presolve removed 87 rows and 109 columns Presolve time: 0.14s Presolved: 469 rows, 1705 columns, 7589 nonzeros Variable types: 0 continuous, 1705 integer (1705 binary) Root relaxation: objective 1.122222e+01, 695 iterations, 0.10 seconds Nodes | Current Node | Objective Bounds | Work Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time 0 0 11.22222 0 94 - 11.22222 - - 0s H 0 0 23.0000000 11.22222 51.2% - 0s H 0 0 22.0000000 11.22222 49.0% - 0s H 0 0 21.0000000 11.22222 46.6% - 0s H 0 0 19.0000000 11.22222 40.9% - 1s 0 0 11.84638 0 154 19.00000 11.84638 37.7% - 1s H 0 0 17.0000000 11.84638 30.3% - 1s 0 0 12.22772 0 153 17.00000 12.22772 28.1% - 1s 0 0 12.61755 0 164 17.00000 12.61755 25.8% - 1s 0 0 12.61755 0 160 17.00000 12.61755 25.8% - 1s 0 0 12.93296 0 150 17.00000 12.93296 23.9% - 1s H 0 0 16.0000000 12.93296 19.2% - 1s 0 0 12.93296 0 145 16.00000 12.93296 19.2% - 1s 0 0 12.96858 0 176 16.00000 12.96858 18.9% - 1s 0 0 12.96858 0 172 16.00000 12.96858 18.9% - 1s 0 0 12.97633 0 188 16.00000 12.97633 18.9% - 2s 0 0 12.97633 0 149 16.00000 12.97633 18.9% - 2s H 0 0 15.0000000 12.97718 13.5% - 2s 0 2 12.97718 0 148 15.00000 12.97718 13.5% - 2s 832 552 13.67063 20 86 15.00000 13.10278 12.6% 27.6 5s 1109 715 13.62500 38 155 15.00000 13.14659 12.4% 25.5 10s 1126 727 13.47561 10 196 15.00000 13.33031 11.1% 25.1 15s 1142 740 13.43859 15 195 15.00000 13.37708 10.8% 35.7 20s 1275 745 13.92919 23 170 15.00000 13.45944 10.3% 50.7 25s * 1373 712 33 14.0000000 13.49880 3.58% 56.2 26s Cutting planes: Gomory: 32 Cover: 5 Implied bound: 19 Clique: 57 MIR: 52 StrongCG: 5 Flow cover: 39 Inf proof: 1 Zero half: 52 RLT: 10 Explored 1387 nodes (81413 simplex iterations) in 26.72 seconds Thread count was 4 (of 4 available processors) Solution count 8: 14 15 16 ... 23 Optimal solution found (tolerance 1.00e-04) Best objective 1.400000000000e+01, best bound 1.400000000000e+01, gap 0.0000% --------------------------------------------------------------------------- Multi-objectives: optimize objective 2 (skill not used) ... --------------------------------------------------------------------------- Loaded user MIP start with objective 10 Presolve removed 73 rows and 97 columns Presolve time: 0.17s Presolved: 484 rows, 1717 columns, 8110 nonzeros Variable types: 0 continuous, 1717 integer (1717 binary) Root simplex log... Iteration Objective Primal Inf. Dual Inf. Time 0 0.0000000e+00 6.200000e+01 0.000000e+00 27s 524 5.0000000e+00 0.000000e+00 0.000000e+00 27s Root relaxation: objective 5.000000e+00, 524 iterations, 0.05 seconds Nodes | Current Node | Objective Bounds | Work Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time 0 0 5.00000 0 30 10.00000 5.00000 50.0% - 27s H 0 0 9.0000000 5.00000 44.4% - 27s H 0 0 8.0000000 5.00000 37.5% - 27s 0 0 5.00000 0 73 8.00000 5.00000 37.5% - 27s 0 0 5.42500 0 70 8.00000 5.42500 32.2% - 27s 0 0 5.62500 0 64 8.00000 5.62500 29.7% - 27s 0 0 5.62500 0 24 8.00000 5.62500 29.7% - 28s 0 0 5.62500 0 103 8.00000 5.62500 29.7% - 28s 0 0 5.62500 0 95 8.00000 5.62500 29.7% - 28s 0 0 5.64410 0 103 8.00000 5.64410 29.4% - 28s 0 0 5.64410 0 108 8.00000 5.64410 29.4% - 28s 0 0 5.69565 0 99 8.00000 5.69565 28.8% - 28s 0 0 5.69565 0 114 8.00000 5.69565 28.8% - 28s 0 0 5.69565 0 118 8.00000 5.69565 28.8% - 28s 0 0 5.69565 0 82 8.00000 5.69565 28.8% - 28s 0 2 5.71429 0 82 8.00000 5.71429 28.6% - 28s 645 170 cutoff 35 8.00000 6.38458 20.2% 28.1 30s Cutting planes: Gomory: 5 Cover: 19 Implied bound: 15 Clique: 18 MIR: 9 StrongCG: 1 Inf proof: 7 Zero half: 15 RLT: 3 Explored 1193 nodes (36402 simplex iterations) in 30.68 seconds Thread count was 4 (of 4 available processors) Solution count 3: 8 9 10 Optimal solution found (tolerance 1.00e-04) Best objective 8.000000000000e+00, best bound 8.000000000000e+00, gap 0.0000% --------------------------------------------------------------------------- Multi-objectives: solved in 30.72 seconds, solution count 10

Visualize the solution

🔗 More in this category

© 2026 GitRepoTrend · carloalbe/A-nurse-scheduling-problem-solved-with-Gurobi · Updated daily from GitHub