Call Center Agent Assignment
Call center agent assignment involves scheduling shifts to meet call demand. This problem consists of two parts: first, estimating the demand, and second, assigning agent shifts. In this article, we will focus on agent shift assignment, using R to generate solver data and GLPK to solve the problem programmed in AMPL.
The Problem
The difficulty in shift scheduling lies in the business rules associated with it, which directly affect the call center work environment.
The business rules we will implement in this article come from a real case and are as follows:
- Each agent must have at least one 2‑consecutive‑day break per week.
- Agents can request days off.
- If an agent works on a Sunday, the following Sunday must be off.
- The agent can specify days they want to work; these can be used as initial conditions for the previous rule.
The challenge is to plan this at least four weeks in advance (plus one week detailing the Sunday shifts).
Methodology
We will model the problem using linear programming in AMPL and solve it using SIMPLEX with the GLPK package. The model input will be generated in R, and the GLPK output will be read in R and displayed on screen.
Parameters:
- Names of the executives.
- Daily call demand.
- Days off requested by agents.
- Days agents must work.
Output
Agent shifts and business rules that could not be met.
Implementation:
Generate Model Parameters
First, we will generate the model input data in R. To do this, we will read the parameters from an Excel database with the following format (only the first rows are shown).
Executive Names:
| name_executive |
|---|
| Executive 1 |
| Executive 2 |
| Executive 3 |
| Executive 4 |
| Executive 5 |
| Executive 6 |
| Executive 7 |
Daily Executive Demand:
| week | day | demand_executives |
|---|---|---|
| 1 | 1 | 16 |
| 1 | 2 | 15 |
| 1 | 3 | 15 |
| 1 | 4 | 15 |
| 1 | 5 | 16 |
| 1 | 6 | 9 |
| 1 | 7 | 9 |
| 2 | 1 | 16 |
Requested Days Off:
| name_executive | week | day |
|---|---|---|
| Executive 1 | 1 | 1 |
| Executive 1 | 1 | 2 |
| Executive 1 | 1 | 3 |
| Executive 4 | 2 | 1 |
| Executive 5 | 2 | 2 |
| Executive 6 | 2 | 3 |
Mandatory Shifts:
| name_executive | week | day |
|---|---|---|
| Executive 1 | 2 | 1 |
| Executive 1 | 2 | 2 |
| Executive 1 | 2 | 3 |
R Code to Read Excel and Generate Input for GLPK
options(stringsAsFactors = FALSE)
library(reshape2)
library("xlsx")
# Read parameters
ejecutivos = read.xlsx2("datos.xlsx", sheetName = "ejecutivo")
fechas = read.xlsx("datos.xlsx", sheetName = "demanda")
dias_libres = read.xlsx("datos.xlsx", sheetName = "dias_libres")
dias_libres$dias_libres = 1
turno_obligado = read.xlsx("datos.xlsx", sheetName = "turno_obligado")
turno_obligado$turno_obligado = 1
# Weeks
semana = data.frame(semana = unique(fechas$semana))
# Table with daily parameters for executives
fecha_ejecutivo = expand.grid(nombre_ejecutivo = ejecutivos$nombre_ejecutivo,
semana = semana$semana, dia = 1:7)
fecha_ejecutivo = merge(fecha_ejecutivo, dias_libres,
by = c("nombre_ejecutivo", "semana", "dia"), all.x = T)
fecha_ejecutivo$dias_libres = as.numeric(!is.na(fecha_ejecutivo$dias_libres))
fecha_ejecutivo = merge(fecha_ejecutivo, turno_obligado,
by = c("nombre_ejecutivo", "semana", "dia"), all.x = T)
fecha_ejecutivo$turno_obligado = as.numeric(!is.na(fecha_ejecutivo$turno_obligado))
# Auxiliary table with number of weeks
parametros = data.frame(parametro = c("semanas"),
valor = max(semana$semana))
# Write tables
write.csv(fechas, "fechas.csv", row.names = F)
write.csv(ejecutivos, "ejecutivos.csv", row.names = F)
write.csv(fecha_ejecutivo, "fecha_ejecutivo.csv", row.names = F)
write.csv(semana, "semana.csv", row.names = F)
write.csv(parametros, "parametros.csv", row.names = F)
The Model
/*Indices*/
set E; /*Executives*/
set S; /*Weeks*/
set D := {1 .. 7}; /*Day of the week*/
set DS, within D cross S; /*Week cross day, for demand*/
set EDS, within E cross D cross S; /*Executive day week*/
set P;
/*Declare Parameters*/
param dias_libres{(e,d,s) in EDS} default 0, binary; /*true if the executive requested that day off*/
param turno_obligado{(e,d,s) in EDS} default 0, binary; /*true if the executive must work that day*/
param demanda_ejecutivos{(d,s) in DS} default 0, integer; /*demand for executives on that day*/
param parametros{p in P};
table csv_parametros IN "CSV" "parametros.csv":
P <- [parametro], parametros ~ valor;
table csv_fechas IN "CSV" "ejecutivos.csv":
E <- [nombre_ejecutivo];
table csv_semana IN "CSV" "semana.csv":
S <- [semana];
table csv_fechas IN "CSV" "fechas.csv":
DS <- [dia,semana], demanda_ejecutivos ~ demanda_ejecutivos;
table fecha_ejecutivo IN "CSV" "fecha_ejecutivo.csv":
EDS <- [nombre_ejecutivo,dia,semana], dias_libres ~ dias_libres, turno_obligado ~ turno_obligado;
/*Variables*/
var W{(e,d,s) in EDS} binary; /* 1 if executive e works on day d of week s*/
var I{e in E, d in 1..6, s in S} binary; /*1 if executive starts a double rest on day d, week s*/
var error_DescDobleSem{e in E, s in S} >= 0; /*number of times a double rest is not assigned to an agent*/
var error_DomingoAnterior{e in E, s in S} >= 0; /*number of times an agent must work two consecutive Sundays*/
var error_DiaLibre{(e,d,s) in EDS} >= 0; /*number of times an agent requested a day off and it could not be assigned*/
var error_DescansoDobleDia1{e in E, d in 1..6, s in S} >= 0; /*assigned a double rest but day 1 could not be given*/
var error_DescansoDobleDia2{e in E, d in 1..6, s in S} >= 0; /*assigned a double rest but day 2 could not be given*/
/*Objective function*/
minimize obj:
sum{e in E, s in S} error_DescDobleSem[e,s] +
sum{e in E, s in S} error_DomingoAnterior[e,s] +
sum{e in E, d in 1..6, s in S} error_DiaLibre[e,d,s] +
sum{e in E, d in 1..6, s in S} error_DescansoDobleDia1[e,d,s] +
sum{e in E, d in 1..6, s in S} error_DescansoDobleDia2[e,d,s];
/*Constraints*/
/*R1 Satisfy demand*/
s.t. SatDem{(d,s) in DS}: sum{e in E} W[e,d,s] = demanda_ejecutivos[d,s];
/*R2 Each executive must have at least one double rest per week*/
s.t. DescDobleSem{e in E, s in S}: 1 - sum{d in 1..6} I[e,d,s] <= error_DescDobleSem[e,s];
/*R3 If a double rest is assigned, the next day is also free (at least one 2‑day rest per week)*/
s.t. DescansoDobleDia1{e in E, d in 1..6, s in S}: W[e,d,s] - (1 - I[e,d,s]) <= error_DescansoDobleDia1[e,d,s];
s.t. DescansoDobleDia2{e in E, d in 1..6, s in S}: W[e,d+1,s] - (1 - I[e,d,s]) <= error_DescansoDobleDia2[e,d,s];
/*R4 If an agent rested on Sunday of the previous week, the next Sunday must be off (this data must be filled)*/
s.t. DomingoAnterior{e in E, s in 2 .. parametros['semanas']}: W[e,7,s] - (1 - W[e,7,s-1]) <= error_DomingoAnterior[e,s];
/*R5 If an executive requests a day off, they do not work*/
s.t. DiaLibre{(e,d,s) in EDS}: W[e,d,s] - (1- dias_libres[e,d,s]) <= error_DiaLibre[e,d,s];
/*R6 If a day is mandatory, the executive must work*/
s.t. DiaObligado{(e,d,s) in EDS}: W[e,d,s] >= turno_obligado[e,d,s];
solve;
table tout {(nombre_ejecutivo,dia,semana) in EDS} OUT "CSV" "solucion.csv":
nombre_ejecutivo, dia, semana, W[nombre_ejecutivo,dia,semana];
table tout {(nombre_ejecutivo,dia,semana) in EDS} OUT "CSV" "error_DiaLibre.csv":
nombre_ejecutivo, dia, semana, error_DiaLibre[nombre_ejecutivo,dia,semana];
table tout {nombre_ejecutivo in E, semana in S} OUT "CSV" "error_DescDobleSem.csv":
nombre_ejecutivo, semana, error_DescDobleSem[nombre_ejecutivo,semana];
table tout {nombre_ejecutivo in E, semana in S} OUT "CSV" "error_DomingoAnterior.csv":
nombre_ejecutivo, semana, error_DomingoAnterior[nombre_ejecutivo,semana];
table tout {nombre_ejecutivo in E, dia in 1..6, semana in S} OUT "CSV" "error_DescansoDobleDia1.csv":
nombre_ejecutivo, dia, semana, error_DescansoDobleDia1[nombre_ejecutivo,dia,semana];
table tout {nombre_ejecutivo in E, dia in 1..6, semana in S} OUT "CSV" "error_DescansoDobleDia2.csv":
nombre_ejecutivo, dia, semana, error_DescansoDobleDia2[nombre_ejecutivo,dia,semana];
display obj;
Solve the Model
GLPK must be installed. Since the problem consists of finding a solution and we do not know in advance if one exists, we will run the solver with a time limit of 600 seconds.
glpsol --cuts --fpump --mipgap 0.001 --tmlim 7200 -m "modelo.mod"
Retrieve the Output in R
# Write tables
write.csv(fechas, "fechas.csv", row.names = F)
write.csv(ejecutivos, "ejecutivos.csv", row.names = F)
write.csv(fecha_ejecutivo, "fecha_ejecutivo.csv", row.names = F)
write.csv(semana, "semana.csv", row.names = F)
write.csv(parametros, "parametros.csv", row.names = F)
system('glpsol --cuts --fpump --mipgap 0.001 --tmlim 7200 -m "modelo2.mod"')
solucion = read.csv("solucion.csv")
solucion$semana_dia = paste0("s", solucion$semana, "-d", solucion$dia)
print(dcast(solucion, nombre_ejecutivo ~ semana_dia, value.var = "W"))
print("Errors")
error_DiaLibre = read.csv("error_DiaLibre.csv")
print(paste("error day off:", sum(error_DiaLibre$error_DiaLibre)))
error_DescDobleSem = read.csv("error_DescDobleSem.csv")
print(paste("error double rest:", sum(error_DescDobleSem$error_DescDobleSem)))
error_DescansoDobleDia1 = read.csv("error_DescansoDobleDia1.csv")
print(paste("error double rest day 1:", sum(error_DescansoDobleDia1$error_DescansoDobleDia1)))
error_DescansoDobleDia2 = read.csv("error_DescansoDobleDia2.csv")
print(paste("error double rest day 2:", sum(error_DescansoDobleDia2$error_DescansoDobleDia2)))
error_DomingoAnterior = read.csv("error_DomingoAnterior.csv")
print(paste("error previous Sunday:", sum(error_DomingoAnterior$error_DiaLibre)))
print(paste("objective function:",
sum(error_DiaLibre$error_DiaLibre,
error_DescDobleSem$error_DescDobleSem,
error_DescansoDobleDia1$error_DescansoDobleDia1,
error_DescansoDobleDia1$error_DescansoDobleDia2,
error_DomingoAnterior$error_DiaLibre)))
Example Output
The output for the first week and the first five executives would look like this:
| name_executive | s1-d1 | s1-d2 | s1-d3 | s1-d4 | s1-d5 | s1-d6 | s1-d7 |
|---|---|---|---|---|---|---|---|
| Executive 1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
| Executive 2 | 1 | 1 | 0 | 0 | 1 | 0 | 1 |
| Executive 3 | 0 | 1 | 0 | 0 | 1 | 1 | 1 |
| Executive 4 | 1 | 1 | 1 | 1 | 1 | 0 | 0 |
| Executive 5 | 1 | 0 | 1 | 0 | 0 | 1 | 1 |
I hope this tutorial was helpful. If you have any questions, feel free to contact me via LinkedIn at https://cl.linkedin.com/in/danielfischerm, my email dfischer@ug.uchile.cl, or any of my contact channels.

Leave a Reply