title: "Lecture 4: Assignment and Scheduling Problems" author: "Junaid Hasan" date: "June 28, 2021"¶

High level idea:¶

  • a list I of workers (m), and a list J of jobs (m).
  • x[i,j] = 1 if worker i is assigned job j and 0 otherwise.
  • Either there is a preference p[i,j] or a cost c[i,j]
  • Goal is to maximize preference $\sum_{i \in I} \sum_{j \in J} p[i,j]\cdot x[i,j]$ or minimize cost $\sum_{i \in I} \sum_{j \in J} c[i,j] \cdot x[i,j]$.
  • subject to
  • each job being assigned to one worker $\sum_{i \in I} x[i,j] = 1$ for all $j\in J$, and
  • each worker doing one job $\sum_{j \in J} x[i,j] = 1$ for all $i \in I$.

An extension may be made¶

  • Suppose we have m workers and n jobs.
  • Then we may want each worker to be doing at most $\lceil\frac{n}{m}\rceil$ and at least $\lfloor\frac{n}{m}\rfloor$ fraction of jobs.
  • Then modify the last requirement as $\lfloor\frac{n}{m}\rfloor \leq \sum_{j \in J} x[i,j] \leq \lceil\frac{n}{m}\rceil$ for all $i \in I$

A nurse scheduling problem¶

A hospital supervisor needs to create a schedule for four nurses over a three-day period, subject to the following:

  • Each day is divided into three 8-hour shifts.
  • Every day, each shift is assigned to a single nurse.
  • No nurse works more than one shift on a given day.
In [1]:
nurses = [1,2,3,4]
shifts = [1,2,3]
days = [1,2,3]

rating = {(1,1,1):0, (1,1,2):1, (1,1,3): 2,
          (2,1,1):1, (2,1,2):2, (2,1,3):2,
          (3,1,1):1, (3,1,2):0, (3,1,3):2,
          (4,1,1):2, (4,1,2):1, (4,1,3):0,
          (1,2,1):0, (1,2,2):1, (1,2,3): 2,
          (2,2,1):1, (2,2,2):2, (2,2,3):2,
          (3,2,1):2, (3,2,2):0, (3,2,3):2,
          (4,2,1):2, (4,2,2):1, (4,2,3):0,
          (1,3,1):0, (1,3,2):1, (1,3,3): 2,
          (2,3,1):1, (2,3,2):2, (2,3,3):2,
          (3,3,1):1, (3,3,2):2, (3,3,3):2,
          (4,3,1):2, (4,3,2):1, (4,3,3):0}

Let us use random numbers for the ratings. We do this via the random.choices function. It inputs a choice list and a weight list (probability) and outputs a list of size $k$ with each choice as likely as the probability specified.

Suppose we want 0,1,2 with probability 0.3, 0.5, 0.2. We do this by:

In [2]:
from random import choices
for n in nurses:
    for d in days:
        for s in shifts:
            rating[(n,d,s)] = choices([0, 1, 2], [0.3, 0.5, 0.2], k=1)[0]
rating
Out[2]:
{(1, 1, 1): 1,
 (1, 1, 2): 1,
 (1, 1, 3): 1,
 (2, 1, 1): 1,
 (2, 1, 2): 1,
 (2, 1, 3): 0,
 (3, 1, 1): 2,
 (3, 1, 2): 0,
 (3, 1, 3): 0,
 (4, 1, 1): 0,
 (4, 1, 2): 1,
 (4, 1, 3): 2,
 (1, 2, 1): 0,
 (1, 2, 2): 1,
 (1, 2, 3): 0,
 (2, 2, 1): 0,
 (2, 2, 2): 1,
 (2, 2, 3): 0,
 (3, 2, 1): 0,
 (3, 2, 2): 0,
 (3, 2, 3): 2,
 (4, 2, 1): 1,
 (4, 2, 2): 0,
 (4, 2, 3): 0,
 (1, 3, 1): 0,
 (1, 3, 2): 1,
 (1, 3, 3): 1,
 (2, 3, 1): 1,
 (2, 3, 2): 1,
 (2, 3, 3): 0,
 (3, 3, 1): 1,
 (3, 3, 2): 1,
 (3, 3, 3): 2,
 (4, 3, 1): 1,
 (4, 3, 2): 1,
 (4, 3, 3): 0}
In [3]:
from pyscipopt import Model, quicksum
model = Model("Nurse Scheduling")
num_nurses = len(nurses)
num_shifts = len(shifts)
num_days = len(days)

Create variables to the model. The dictionary variable shift_data[(i,j,k)] is 1 if nurse i is scheduled on day j and shift k and 0 otherwise

In [4]:
shift_data = {}

for n in nurses:
    for d in days:
        for s in shifts:
            shift_data[(n,d,s)] = model.addVar(vtype = "B", name = 'shift for nurse %s, on day %s, shift %s' %(n,d,s))

Next, let us assign the shifts to the nurses¶

The constraints are

  • Each shift is assigned to a single nurse per day
  • Each nurse works at most one shift per day.
In [5]:
for d in days:
    for s in shifts:
        model.addCons(quicksum(shift_data[(n,d,s)] for n in nurses) == 1, "one nurse per shift")

for n in nurses:
    for d in days:
        model.addCons(quicksum(shift_data[(n,d,s)] for s in shifts) <=1, "at most one shift a day")
In [6]:
model.setObjective(quicksum(shift_data[(n,d,s)]*rating[(n,d,s)] for n in nurses for d in days for s in shifts), "maximize")
In [30]:
model.optimize()
In [31]:
for d in days:
    print("Day: %s" %d)
    for s in shifts:
        for n in nurses:
            if model.getVal(shift_data[(n,d,s)]) == 1:
                print("Nurse %s "%n + "works shift %s" %s)
Day: 1
Nurse 2 works shift 1
Nurse 3 works shift 2
Nurse 1 works shift 3
Day: 2
Nurse 4 works shift 1
Nurse 1 works shift 2
Nurse 2 works shift 3
Day: 3
Nurse 2 works shift 1
Nurse 1 works shift 2
Nurse 4 works shift 3

4 steps of Modelling¶

  • Recall from lecture 1.
  • Model cycle

Tweaks¶

  • Suppose we want to enforce that nurses cannot work 0 rated shifts.
  • Each nurse is assigned at least two shifts during the three day period.
for n in nurses:
    for d in days:
        for s in shifts:
            model.addCons(shift_data[(n,d,s)] <= rating[(n,d,s)])
    model.addCons(quicksum(shift_data[(n,i,j)] for i in days for j in shifts)>=2)
In [7]:
for n in nurses:
    for d in days:
        for s in shifts:
            model.addCons(shift_data[(n,d,s)] <= rating[(n,d,s)])
    model.addCons(quicksum(shift_data[(n,i,j)] for i in days for j in shifts)>=2)
In [10]:
model.getLPColsData()
Out[10]:
[]
In [8]:
model.optimize()
In [11]:
model.writeProblem(filename='nurse-scheduling.cip')
wrote problem to file /Users/junaid/Sync/Math-381/Week-2/nurse-scheduling.cip
In [12]:
model.writeSol(filename='nurse-scheduling.sol')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-12-d3273d11105a> in <module>
----> 1 model.writeSol(filename='nurse-scheduling.sol')

src/pyscipopt/scip.pyx in pyscipopt.scip.Model.writeSol()

TypeError: writeSol() takes at least 1 positional argument (0 given)

printing again:

In [9]:
for d in days:
    print("Day: %s" %d)
    for s in shifts:
        for n in nurses:
            if model.getVal(shift_data[(n,d,s)]) == 1:
                print("Nurse %s "%n + "works shift %s" %s)
Day: 1
Nurse 3 works shift 1
Nurse 1 works shift 2
Nurse 4 works shift 3
Day: 2
Nurse 4 works shift 1
Nurse 2 works shift 2
Nurse 3 works shift 3
Day: 3
Nurse 2 works shift 1
Nurse 1 works shift 2
Nurse 3 works shift 3

Major Changes¶

  • Suppose now that the union has passed a resolution that shifts cannot be more than 4 hours long. Therefore 6 shifts per day.
  • Nurses cannot have more than 2 shifts per day.
  • Since the number of shifts now is 18 for a three day schedule.
  • There are 4 nurses, therefore each nurse must work at least 4 shifts in three days.
In [49]:
nurses = ["A","B","C","D"]
shifts = ["early morning","morning","day","afternoon","evening","night"]
days = [1,2,3]
rating = {}
from random import choices
for n in nurses:
    for d in days:
        for s in shifts:
            rating[(n,d,s)] = choices([0, 1, 2], [0.3, 0.5, 0.2], k=1)[0]
from pyscipopt import Model, quicksum
model = Model("Nurse Scheduling")
num_nurses = len(nurses)
num_shifts = len(shifts)
num_days = len(days)

shift_data = {}

for n in nurses:
    for d in days:
        for s in shifts:
            shift_data[(n,d,s)] = model.addVar(vtype = "B", name = 'shift for nurse %s, on day %s, shift %s' %(n,d,s))
    model.addCons(quicksum(shift_data[(n,i,j)] for i in days for j in shifts) >=4, "nurse %s works at least two shifts"%n)


for d in days:
    for s in shifts:
        model.addCons(quicksum(shift_data[(n,d,s)] for n in nurses) == 1, "one nurse per shift")

for n in nurses:
    for d in days:
        model.addCons(quicksum(shift_data[(n,d,s)] for s in shifts) <=2, "at most two shifts a day")

for n in nurses:
    for d in days:
        for s in shifts:
            model.addCons(shift_data[(n,d,s)] <= rating[(n,d,s)], "nurse %s, day %s, shift %s cannot work on rating 0" %(n,d,s))
In [50]:
model.setObjective(quicksum(shift_data[(n,d,s)]*rating[(n,d,s)] for n in nurses for d in days for s in shifts), "maximize")
model.optimize()
In [51]:
for d in days:
    print("Day: %s" %d)
    for s in shifts:
        for n in nurses:
            if model.getVal(shift_data[(n,d,s)]) == 1:
                print("Nurse %s "%n + "works shift %s" %s)
Day: 1
Nurse D works shift early morning
Nurse A works shift morning
Nurse D works shift day
Nurse C works shift afternoon
Nurse B works shift evening
Nurse C works shift night
Day: 2
Nurse D works shift early morning
Nurse C works shift morning
Nurse B works shift day
Nurse B works shift afternoon
Nurse D works shift evening
Nurse A works shift night
Day: 3
Nurse C works shift early morning
Nurse A works shift morning
Nurse B works shift day
Nurse D works shift afternoon
Nurse D works shift evening
Nurse A works shift night

Model vs Real world¶

The shifts cannot be too split. If they are working two shifts make it consecutive. To enforce this constraint we have to make sure that for each day the first hour a nurse works - last hour she works is not larger than 1.

Models never cover the full picture. For example even here we may want Nurses who work more than one shift do them at consecutive intervals and not far apart.

Takeaways¶

  • Models help us solve a mathematical problem which may resemble a real world problem.
  • However a real world problem may be far more involved and we may have to change the model multiple times.
  • Even with lots of changes the model is still a mathematical representation.
  • It is essential to get information from the model, but not to lose the sight of the real world problem.
  • There may be real world constraints which may be difficult/impossible to state/solve mathematically.
In [ ]: