Manpower Planning Example

This model is an example of a staffing problem. In staffing planning problems, choices must be made regarding the recruitment, training, layoffs (redundancy) and scheduling of staff. These problems are common across a broad range of both manufacturing and service industries.

In this example we’ll model and solve a manpower planning problem. We have three type of workers with different skills levels. For each year in the planning horizon the predicted number of required workers of each skill is given. It is possible to recruit new people, train workers to improve/decrease their skills or put them into part-time (short-time). The aim is to create an optimal multi-period operation plan to minimize the total number of layoffs over the whole horizon. An alternative aim is to minimize the total costs.

Note: you can download the model, implemented in Python, here. More information on this type of model can be found in the fifth edition of Model Building in Mathematical Programming, by H. Paul Williams.


H. Paul Williams, Model Building in Mathematical Programming, fifth edition (Page 255-256, 354-356)


Problem Description

A company is changing how it runs its business, and therefore its staffing needs are expected to change.

Through the purchase of new machinery, it is expected that there will be less need for unskilled labor and more need for skilled and semi-skilled labor. In addition, a lower sales forecast, driven by an expected economic slowdown in the next year, is expected to further reduce labor needs across all categories.

The forecast for labor needs over the next three years is as follows:

  Unskilled Semi-skilled Skilled
Current Strength 2000 1500 1000
Year 1 1000 1400 1000
Year 2 500 2000 1500
Year 3 0 2500 2000

The company needs to determine the following for each of the next three years:

  1. Recruitment
  2. Retraining
  3. Layoffs (redundancy)
  4. Part-time vs. full-time employees

It is important to note that labor is subject to a certain level of natural attrition each year. The rate of attrition is relatively high in the first year after a new employee is hired and relatively low in subsequent years. The expected attrition rates are as follows:

  Unskilled (%) Semi-skilled (%) Skilled (%)
Less than one year's service 25 20 10
More than one year's service 10 5 5





All of the current workers have been with the company for at least one year.


Recruitment

Each year, it is possible to hire a limited number of employees in each classification from outside the company as follows:

Unskilled Semi-skilled Skilled
500 800 500

Retraining

Each year, it is possible to train up to 200 unskilled workers to make them into semi-skilled workers. This training costs the company $400 per worker.

In addition, it is possible train semi-skilled workers to make them into skilled workers. However, this number can not exceed 25% of the current skilled labor force and this training costs $500 per worker.

Lastly, downgrading workers to a lower skill can be done. However, 50% of the downgraded workers will leave the company, increasing the natural attrition rate described above.


Layoffs

Each laid-off worker is entitled to a separation payment at the rate of $200 per unskilled worker and $500 per semi-skilled or skilled worker.


Excess employees

It is possible to have workers in excess of the actual number needed, but this will result in the following additional cost per excess employee per year.

Unskilled Semi-skilled Skilled
$1500 $2000 $3000

Part-time worker

Up to 50 employees of each skill level can be assigned to part-time work. The cost doing so (per employee, per year) is as follows:

Unskilled Semi-skilled Skilled
$500 $400 $400



Note: A part-time employee is half as productive as a full-time employee.


The company’s objective is to minimize layoffs. What plan should they adopt in order to do this?

If their objective was to minimize costs, how much could they further reduce costs?

Determine the annual savings possible across each job.

Model Formulation

Let \( T \) be a set of time periods, where \( t_0 \in T \) is the first period and \( t_e \in T \) the last period. Let \( S = \{S_1,S_2,S_3\}\) be a set of three skill levels.

For each time period \(t \in T\) and each skill level \(s \in S\) we introduce continuous non negative variables \(a_{t,s}\), \(b_{t,s}\), \(c_{t,s}\), \(d_{t,s}\), \(e_{t,s}\).
\(a_{t,s}\) describes how many workers of skill level \(s \) get recruited in the time period \(t\).
\(b_{t,s}\) describes how many workers of skill level \(s \) are short-time working in the time period \(t\).
\(c_{t,s}\) describes how many workers of skill level \(s \) are available in the time period \(t\).
\(d_{t,s}\) describes how many workers of skill level \(s \) are redundant in the time period \(t\).
\(e_{t,s}\) describes how many workers of skill level \(s \) are overmanned in the time period \(t\).

For each time period \(t \in T\) and each pair of distinct skill level \(s_1,s_2 \in S\) we introduce a continuous non-negative variable \(f_{t,s_1,s_2}\), which describes how many worker of the skill level \(s_1\) get retrained to the skill level \(s_2\) in the time period \(t\).

For each time period \(t \in T\) and each skill level \(s \in S\) we have given the required manpower \(g_{t,s}\),
For each skill level \(s \in S\) we have given the current manpower \(h_{s}\), the percentage of workers who leave in the first year \(i_s\), the percentage of workers who leave in the other years \(j_s\), the maximal amount of workers, who can get recruited \(k_s\), the retraining cost \(l_s\) to the next level, the cost to layoff and employee \(m_s\), the part-time worker cost \(n_s\) and the overmanning cost \(o_s\).
Workers who get retrained to a lower skill level, leave the company with a percetange of \(p\).

The variables can be formulated as:
\begin{equation} a_{t,s},b_{t,s},c_{t,s},d_{t,s},e_{t,s} \geq 0 \quad \forall t \in T, \ \forall s \in S \end{equation} \begin{equation} f_{t,s_1,s_2} \geq 0 \quad \forall t \in T, \ \forall s_1,s_2 \in S \end{equation}


The continuity constraints ensure that per skill level and per year the current needed workers (LaborForce), the number of workers laid off and the workers who gets retrained to the current level, minus the people who gets retrained from the current level to a different skill level, equals the LaborForce of the last year (or the CurrentStrength in the first year) plus the recruited workers. A certain amount of workers leave the company each year, so this is also considered with a factor. This constraint describes the change in the total amount of employed workers.
\begin{equation} c_{t,s} + d_{t,s} - i_s a_{t,s} + \sum_{z \in S : z < s} (- j_sf_{t,z,s} + f_{t,s,z}) + \sum_{z \in S : z > s} (f_{t,s,z} - pf_{t,z,s})= j_s c_{t-1,s} \forall t \in T \setminus t_0, \ \forall s \in S \end{equation} \begin{equation} c_{t_0,s} + d_{t_0,s} - i_s a_{t_0,s} + \sum_{z \in S : z < s} (- j_sf_{t_0,z,s} + f_{t_0,s,z}) + \sum_{z \in S : z > s} (f_{t_0,s,z} - pf_{t_0,z,s})= j_s h_{s} \ \forall s \in S \end{equation}


The RetrainMaxUnskilled constraints force that per year only 200 people can be retrained from Unskilled to Semiskilled due to capacity limitations. Also there cannot be people trained in one year from Unskilled to Skilled.
\begin{equation} f_{t,s_2,s_3} \leq 200 \quad \forall t \in T \end{equation} \begin{equation} f_{t,s_1,s_3} = 0 \quad \forall t \in T \end{equation}


The RetrainingSemiSkilled states that the retraining of semiskilled workers to skilled workers is limited no more than one quarter of the skilled labor force at this time. This is due to capacity limitations.
\begin{equation} f_{t,s_2,s_3} \leq 0.25 c_{t,s} \quad \forall t \in T \end{equation}


The overmanning constraints ensure that the total overmanning over all skill levels in one year is no more than 150.
\begin{equation} \sum_{s \in S} e_{t,s} \leq 150 \quad \forall t \in T \end{equation}


The requirements constraints ensure that the number of workers of each level and year equals the required amount of workers plus the Overmanned workers and the amount of workers who are in part-time work.
\begin{equation} c_{t,s} = g_{t,s} + e_{t,s} + 0.5 * b_{t,s} \quad \forall t \in T \end{equation}


The first objective is to minimze the total number of workers that are laid off. This can be stated as:
\begin{equation} \min \sum_{t \in T} \sum_{s \in S} d_{t,s} \end{equation}


The second alternative objective is to minimize the total cost of all employed workers and the costs for retraining.
\begin{equation} \min \sum_{t \in T} l_s f_{t,s_1,s_2} + l_s f_{t,s_2,s_3} + \sum_{s \in S} m_s b_{t,s} + n_s d_{t,s} + o_s e_{t,s} \end{equation}


The full model (with the first objective) can be stated as:
\begin{equation} \min \sum_{t \in T} \sum_{s \in S} d_{t,s} \end{equation} \begin{equation} c_{t,s} = g_{t,s} + e_{t,s} + 0.5 * b_{t,s} \quad \forall t \in T \end{equation} \begin{equation} \sum_{s \in S} e_{t,s} \leq 150 \quad \forall t \in T \end{equation} \begin{equation} f_{t,s_2,s_3} \leq 0.25 c_{t,s} \quad \forall t \in T \end{equation} \begin{equation} f_{t,s_2,s_3} \leq 200 \quad \forall t \in T \end{equation} \begin{equation} f_{t,s_1,s_3} = 0 \quad \forall t \in T \end{equation} \begin{equation} c_{t,s} + d_{t,s} - i_s a_{t,s} + \sum_{z \in S : z < s} (- j_sf_{t,z,s} + f_{t,s,z}) + \sum_{z \in S : z > s} (f_{t,s,z} - pf_{t,z,s})= j_s c_{t-1,s} \forall t \in T \setminus t_0, \ \forall s \in S \end{equation} \begin{equation} c_{t_0,s} + d_{t_0,s} - i_s a_{t_0,s} + \sum_{z \in S : z < s} (- j_sf_{t_0,z,s} + f_{t_0,s,z}) + \sum_{z \in S : z > s} (f_{t_0,s,z} - pf_{t_0,z,s})= j_s h_{s} \ \forall s \in S \end{equation} \begin{equation} a_{t,s},b_{t,s},c_{t,s},d_{t,s},e_{t,s} \geq 0 \quad \forall t \in T, \ \forall s \in S \end{equation} \begin{equation} f_{t,s_1,s_2} \geq 0 \quad \forall t \in T, \ \forall s_1,s_2 \in S \end{equation}


The full model (with the second objective) can be stated as:
\begin{equation} \min \sum_{t \in T} l_s f_{t,s_1,s_2} + l_s f_{t,s_2,s_3} + \sum_{s \in S} m_s b_{t,s} + n_s d_{t,s} + o_s e_{t,s} \end{equation} \begin{equation} c_{t,s} = g_{t,s} + e_{t,s} + 0.5 * b_{t,s} \quad \forall t \in T \end{equation} \begin{equation} \sum_{s \in S} e_{t,s} \leq 150 \quad \forall t \in T \end{equation} \begin{equation} f_{t,s_2,s_3} \leq 0.25 c_{t,s} \quad \forall t \in T \end{equation} \begin{equation} f_{t,s_2,s_3} \leq 200 \quad \forall t \in T \end{equation} \begin{equation} f_{t,s_1,s_3} = 0 \quad \forall t \in T \end{equation} \begin{equation} c_{t,s} + d_{t,s} - i_s a_{t,s} + \sum_{z \in S : z < s} (- j_sf_{t,z,s} + f_{t,s,z}) + \sum_{z \in S : z > s} (f_{t,s,z} - pf_{t,z,s})= j_s c_{t-1,s} \forall t \in T \setminus t_0, \ \forall s \in S \end{equation} \begin{equation} c_{t_0,s} + d_{t_0,s} - i_s a_{t_0,s} + \sum_{z \in S : z < s} (- j_sf_{t_0,z,s} + f_{t_0,s,z}) + \sum_{z \in S : z > s} (f_{t_0,s,z} - pf_{t_0,z,s})= j_s h_{s} \ \forall s \in S \end{equation} \begin{equation} a_{t,s},b_{t,s},c_{t,s},d_{t,s},e_{t,s} \geq 0 \quad \forall t \in T, \ \forall s \in S \end{equation} \begin{equation} f_{t,s_1,s_2} \geq 0 \quad \forall t \in T, \ \forall s_1,s_2 \in S \end{equation}

Implementation with comments

First, we import the Gurobi Python Module and initialize the data structures with the given data.

from gurobipy import *
# tested with Python 2.7.6 & Gurobi 7.0.1
years = tuplelist(range(2+1))
skill_levels = [0, 1, 2]  # 0 = Unskilled, 1 = Semiskilled, 2 = Skilled
Unskilled = 0
Semiskilled = 1
Skilled = 2

CurrentStrength = [2000, 1500, 1000]
Requirement = [[1000, 1400, 1000],
               [500, 2000, 1500],
               [0, 2500, 2000]]
LeaveFirstYear = [0.25, 0.20, 0.10]
LeaveEachYear = [0.10, 0.05, 0.05]
ContinueFirstYear = [1 - a for a in LeaveFirstYear]
ContinueEachYear = [1 - a for a in LeaveEachYear]
LeaveDownGraded = 0.50
ContinueDownGraded = 1 - LeaveDownGraded
MaxRecruit = [500, 800, 500]
MaxRetrainUnskilled = 200
MaxOverManning = 150
MaxShortTimeWorking = 50
RetrainSemiSkilled = 0.25
ShortTimeUsage = 0.50

RetrainCost = [400, 500, 0]
RedundantCost = [200, 500, 500]
ShortTimeCost = [500, 400, 400]
OverManningCost = [1500, 2000, 3000]

Create the recruit vars upper bound dictionary.
MaxRecruit2 = {(level, year) : MaxRecruit[level] for level in skill_levels for year in years}

Next, we create a model and the variables. For each of the three skill levels and for each year we will create variables for the amount of workers that get recruited, put into part-time work, are available as workers, are redundant, are overmanned. For each pair of skill levels and each year we have a variable for the amount of workers that get retrained to a higher/lower skill level. The amount of people which are in part-time and can be recruited is limited.

model = Model('Manpower planning')

Recruit = model.addVars(skill_levels, years, ub= MaxRecruit2, name="Recruit")
ShortTime = model.addVars(skill_levels, years, ub=MaxShortTimeWorking,
                          name="ShortTime")
LaborForce = model.addVars(skill_levels, years, name="LaborForce")
Redundant = model.addVars(skill_levels, years, name="Redundant")
OverManned = model.addVars(skill_levels, years, name="OverManned")
Retrain = model.addVars(skill_levels, skill_levels, years, name="Retrain")

Next, we insert the constraints. The continuity constraints ensure that per skill level and per year the current needed workers (LaborForce) and the laidoff people and the people who gets retrained to the current level, minus the people who gets retrained from the current level to a different skills, equals the LaborForce of the last year (or the CurrentStrength in the first year) plus the recruited people. A certain amount of people leave the company each year, so this is also considered with a factor. This constraint describes the change in the total amount of employed workers.

model.addConstrs(
    (LaborForce[level, year] + Redundant[level, year] 
    - ContinueFirstYear[level] * Recruit[level, year]
    + quicksum(Retrain[level, level2, year]
               - ContinueEachYear[level] * Retrain[level2, level, year]
               for level2 in skill_levels if level2 < level)
    + quicksum(Retrain[level, level2, year]
               - 0.5 * Retrain[level2, level, year]
               for level2 in skill_levels if level2 > level)
    == ContinueEachYear[level] * (
        CurrentStrength[level] if year == years[0]
        else LaborForce[level, years[years.index(year)-1]])
    for year in years for level in skill_levels),
    "Continuity")

The RetrainMaxUnskilled constraints force that per year only 200 workers can be retrained from Unskilled to Semiskilled due to capacity limitations. Also, no one can be trained in one year from Unskilled to Skilled.

# RetainMaxUnskilled
model.addConstrs(
    (Retrain[Unskilled, Semiskilled, year] <= MaxRetrainUnskilled
    for year in years), "RetrainMaxUnskilled")
model.addConstrs(
    (Retrain[Unskilled, Skilled, year] <= 0 for year in years), "ForbidRetrainUnskilledToSkilled")

The RetrainingSemiSkilled states that the retraining of semiskilled workers to skilled workers is limited to no more than one quarter of the skilled labor force at this time. This is due to capacity limitations.

# RetrainingSemiSkilled
model.addConstrs(
    (Retrain[Semiskilled, Skilled, year] <=
     RetrainSemiSkilled * LaborForce[Skilled, year] for year in years), "RetrainingSemiSkilled")

The overmanning constraints ensure that the total overmanning over all skill levels in one year is no more than 150.

## Overmanning
model.addConstrs(
    (OverManned.sum('*', year) <= MaxOverManning for year in years), "Overmanning")

The requirements constraints ensure that the amount of workers of each level and year equals the required amount of workers plus the Overmanned workers and the amount of workers who are in part-time work.

# Requirements
model.addConstrs(
    (LaborForce[level, year] ==
     Requirement[year][level] +
     OverManned[level, year] +
     ShortTimeUsage * ShortTime[level, year]
    for year in years for level in skill_levels), "Requirements")

The first objective is to minimze the total number of laidoff workers. This can be stated as:

# Minimize TotalRedundantMen
obj = Redundant.sum()

The second alternative objective is to minimize the total cost of all employed workers and costs for retraining.

# Minimize TotalCost
# obj = quicksum(
#     RetrainCost[level]*(Retrain[level, level+1, year] if level < 2 else 0)
#     + RedundantCost[level] * Redundant[level, year]
#     + ShortTimeCost[level] * ShortTime[level, year]
#     + OverManningCost[level] * OverManned[level, year]
#     for year in years
#     for level in skill_levels)

model.setObjective(obj)

Next start the optimization, Gurobi tries to find the optimal solution.

model.optimize()
for v in model.getVars():
    if v.X != 0:
        print("%s %f" % (v.Varname, v.X))

Note: If you want to write your solution to a file, rather than print it to the terminal, you can use the model.write() command. An example implementation is:

 model.write("manpower-planning-output.sol")

Gurobi Output and Analysis

Optimize a model with 30 rows, 72 columns and 117 nonzeros
Coefficient statistics:
  Matrix range     [2e-01, 1e+00]
  Objective range  [1e+00, 1e+00]
  Bounds range     [5e+01, 8e+02]
  RHS range        [2e+02, 2e+03]
Presolve removed 18 rows and 44 columns
Presolve time: 0.00s
Presolved: 12 rows, 28 columns, 56 nonzeros

Iteration    Objective       Primal Inf.    Dual Inf.      Time
       0    8.4000000e+02   5.187500e+02   0.000000e+00      0s
       8    8.4179688e+02   0.000000e+00   0.000000e+00      0s

Solved in 8 iterations and 0.00 seconds
Optimal objective  8.417968750e+02

Analyzing the Output

With the objective of minimizing layoffs, the optimal policies to pursue are given below:

Recruitment

  Unskilled Semi-skilled Skilled
Year 1 0 0 0
Year 2 0 649 500
Year 3 0 677 500

Retaining and downgrading

  Unskilled to semi-skilled Semi-skilled to skilled Semi-skilled to unskilled Skilled to unskilled
Year 1 200 258 0 0
Year 2 200 80 0 0
Year 3 200 132 0 0

Layoffs

  Unskilled Semi-skilled Skilled
Year 1 455 0 0
Year 2 164 0 0
Year 3 232 0 0

Part-time Employment

  Unskilled Semi-skilled Skilled
Year 1 50 50 50
Year 2 50 0 0
Year 3 50 0 0

Overmanning

  Unskilled Semi-skilled Skilled
Year 1 130 18 18
Year 2 150 0 0
Year 3 150 0 0

These policies result in a total layoffs of 842 workers over the three years. The total cost of pursuing these policies is $1 438 383. In order to obtain this total cost, it is important to recognize that there are alternative solutions and we should choose that with minimum cost.


If the objective is to minimize the cost, the optimal policies are those given below:

Recruitment

  Unskilled Semi-skilled Skilled
Year 1 0 0 55
Year 2 0 800 500
Year 3 0 800 500

Retaining and downgrading

  Unskilled to semi-skilled Semi-skilled to skilled Semi-skilled to unskilled Skilled to unskilled
Year 1 0 0 25 0
Year 2 142 105 0 0
Year 3 96 132 0 0

Layoffs

  Unskilled Semi-skilled Skilled
Year 1 812 0 0
Year 2 258 0 0
Year 3 354 0 0

Part-time Employment

  Unskilled Semi-skilled Skilled
Year 1 0 0 0
Year 2 0 0 0
Year 3 0 0 0

Overmanning

  Unskilled Semi-skilled Skilled
Year 1 0 0 0
Year 2 0 0 0
Year 3 0 0 0

These policies cost $498 677 over the three years and result in a total layoffs of 1424 workers. Again, alternative solutions should be considered if necessary to ensure that these layoffs are the minimum possible for this (min- imum) level of cost. Clearly, minimizing costs instead of layoffs saves $939 706 but results in 582 extra redundancies. The cost of saving each job (when minimizing layoffs) could, therefore, be regarded as $1615.