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)*

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:

- Recruitment
- Retraining
- Layoffs (redundancy)
- 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.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}

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")

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

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

Unskilled | Semi-skilled | Skilled | |
---|---|---|---|

Year 1 | 0 | 0 | 0 |

Year 2 | 0 | 649 | 500 |

Year 3 | 0 | 677 | 500 |

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 |

Unskilled | Semi-skilled | Skilled | |
---|---|---|---|

Year 1 | 455 | 0 | 0 |

Year 2 | 164 | 0 | 0 |

Year 3 | 232 | 0 | 0 |

Unskilled | Semi-skilled | Skilled | |
---|---|---|---|

Year 1 | 50 | 50 | 50 |

Year 2 | 50 | 0 | 0 |

Year 3 | 50 | 0 | 0 |

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.

Unskilled | Semi-skilled | Skilled | |
---|---|---|---|

Year 1 | 0 | 0 | 55 |

Year 2 | 0 | 800 | 500 |

Year 3 | 0 | 800 | 500 |

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 |

Unskilled | Semi-skilled | Skilled | |
---|---|---|---|

Year 1 | 812 | 0 | 0 |

Year 2 | 258 | 0 | 0 |

Year 3 | 354 | 0 | 0 |

Unskilled | Semi-skilled | Skilled | |
---|---|---|---|

Year 1 | 0 | 0 | 0 |

Year 2 | 0 | 0 | 0 |

Year 3 | 0 | 0 | 0 |

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.