Asignación Agentes Call Center

La asignación se agentes de un Call Center consiste en realizar la programación de turnos para satisfacer la demanda de llamadas a un Call Center. Este es un problema que se compone de dos partes: la primera es la estimación de la demanda y la segunda asignar los turnos de los agentes.

En este articulo nos enfocaremos en la asignación de turnos de los agentes, utilizaremos R para generar los datos de un solver y GLPK para resolver el problema programado en AMPL.

El problema

La dificultad en la programación de turnos está en las reglas de negocio asociadas a este, las cuales afectan directamente el ambiente laboral del Call Center.

Las reglas de negocio que implementaremos en este articulo son de un caso real y son las siguientes:

  1. Cada agente debe tener al menos una vez a la semana 2 días seguidos de descanso.
  2. Los agentes pueden pedir días libres.
  3. Si un agente trabaja un día domingo, el próximo domingo no debe trabajar.
  4. El agente puede especificar días que quiere trabajar, estos pueden ser utilizados como condición inicial para la regla anterior.

El desafío es poder planear esto con al menos 4 semanas de anticipación (más una semana que detalla los turnos del día domingo).

Metodología

Modelaremos el problema utilizando programación lineal en AMPL y lo resolveremos utilizando SIMPLEX con el paquete GLPK, el input para el modelo será generado en R y el output de GLPK sera leído en R y mostrado en pantalla

Parámetros:
  • Nombres de los Ejecutivos.
  • Demanda Diaria de Llamadas.
  • Días libres pedidos por los agentes.
  • Días en que agentes deben trabajar
Output

Turnos de Agentes y reglas de negocio que no se pudieron cumplir.

Implementación:

Generar parámetros del modelo

Primero generaremos los datos de entrada del modelo en R, para hacer esto, leeremos los parámetros de una base de datos en excel la cual tiene el siguiente formato (solo se mostrarán las primeras filas).

Nombres de Ejecutivos:
nombre_ejecutivo
Ejecutivo 1
Ejecutivo 2
Ejecutivo 3
Ejecutivo 4
Ejecutivo 5
Ejecutivo 6
Ejecutivo 7
Demanda diaria de Ejecutivos:
semana dia demanda_ejecutivos
1 1 16
1 2 15
1 3 15
1 4 15
1 5 16
1 6 9
1 7 9
2 1 16
Días Libres Pedidos:
nombre_ejecutivo semana dia
Ejecutivo 1 1 1
Ejecutivo 1 1 2
Ejecutivo 1 1 3
Ejecutivo 4 2 1
Ejecutivo 5 2 2
Ejecutivo 6 2 3
Turnos Obligatorios:
nombre_ejecutivo semana dia
Ejecutivo 1 2 1
Ejecutivo 1 2 2
Ejecutivo 1 2 3
Código en R que lee Excel y genera entrada para GLPK.
options(stringsAsFactors = FALSE)
library(reshape2)
library("xlsx")

#Lectura de Parametros
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

#semanas
semana = data.frame(semana = unique(fechas$semana))

#tabla con parámetros diarios para ejecutivos
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))

#tabla auxiliar con numero de semanas
parametros = data.frame(parametro = c("semanas"),
 valor = max(semana$semana))

#escribir tablas
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)

El Modelo

/*Indices*/
set E ; /*Ejecutivo*/
set S; /*semanas*/
set D:={1 .. 7}; /*Dia de la semana*/
set DS, within D cross S; /*Semana cros dia, para la demanda */
set EDS , within E cross D cross S; /*Ejecutivo dia semana*/
set P;

/*Declarar Parametros*/
param dias_libres{(e,d,s) in EDS} default 0, binary; /*true si el ejecutivo pidio ese dia libre*/
param turno_obligado{(e,d,s) in EDS} default 0, binary; /*true si el ejecutivo va a trabajar ese dia*/
param demanda_ejecutivos{(d,s) in DS} default 0, integer; /*demanda de ejecutivos en el dia*/
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 si el ejecutivo e trabaja el dia d de la semana*/
var I{e in E, d in 1..6, s in S} binary; /*1 si el ejecutivo inicia descanso doble el dia EDS*/
var error_DescDobleSem{e in E, s in S} >= 0; /*numero de veces que no se asgna descanso doble a un agente*/
var error_DomingoAnterior{e in E, s in S} >= 0; /*numero de veces que un agente debe trabajar 2 domingo seguidos*/
var error_DiaLibre{(e,d,s) in EDS} >= 0; /*numero de veces que el agente pidio dia libre y no se le pudo asignar*/
var error_DescansoDobleDia1{e in E, d in 1..6, s in S} >= 0; /*e signo como descanzo doble pero no se pudo dar el dia*/
var error_DescansoDobleDia2{e in E, d in 1..6, s in S} >= 0; /*e signo como descanzo doble pero no se pudo dar el dia*/

/*Funcion obj*/
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];

/*Restricciones*/
/*R1 Satisfacer Demanda*/
s.t. SatDem{(d,s) in DS}: sum{e in E} W[e,d,s] = demanda_ejecutivos[d,s];
/*R2 Cada ejecutivo debetener un al menos un descanso doble a la semana*/
s.t. DescDobleSem{e in E, s in S}: 1 - sum{d in 1..6} I[e,d,s] <= error_DescDobleSem[e,s];
/*R3 Si se le asigna el descanzo, doble el dia sgte tambien es libre (al menos un descanzo de 2 dias a la semana)*/
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 Si le toco descanzo el domingo de la semana anterior, domingo siguiente no trabaja (hay que rellenar ese dato en data)*/
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 Si el ejecutivo pide un dia libre, no trabaja*/
s.t. DiaLibre{(e,d,s) in EDS}: W[e,d,s] - (1- dias_libres[e,d,s]) <= error_DiaLibre[e,d,s];
/*R6 Si tiene un dia obligado, debe trabajar*/
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;

Resolver el modelo

Se debe tener instalado GLPK, como el problema consiste en encontrar una solución, a priori no sabemos si ella existe, por lo que vamos a ejecutar el solver con un tiempo limitado de ejecución de 600 segundos

glpsol --cuts --fpump --mipgap 0.001 --tmlim 7200 -m "modelo.mod"

Recuperamos la salida en R

#escribir tablas
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("Errores")
error_DiaLibre = read.csv("error_DiaLibre.csv")
print(paste("error dia libre:", sum(error_DiaLibre$error_DiaLibre)))

error_DescDobleSem = read.csv("error_DescDobleSem.csv")
print(paste("error descanso doble:", sum(error_DescDobleSem$error_DescDobleSem)))

error_DescansoDobleDia1 = read.csv("error_DescansoDobleDia1.csv")
print(paste("error descanso doble dia 1:", sum(error_DescansoDobleDia1$error_DescansoDobleDia1)))

error_DescansoDobleDia2 = read.csv("error_DescansoDobleDia2.csv")
print(paste("error descanso doble dia 2:", sum(error_DescansoDobleDia2$error_DescansoDobleDia2)))

error_DomingoAnterior = read.csv("error_DomingoAnterior.csv")
print(paste("error domingo anterior:", sum(error_DomingoAnterior$error_DiaLibre)))

print(paste("funcion objetivo:",sum(error_DiaLibre$error_DiaLibre,
 error_DescDobleSem$error_DescDobleSem,
 error_DescansoDobleDia1$error_DescansoDobleDia1,
 error_DescansoDobleDia1$error_DescansoDobleDia2,
 error_DomingoAnterior$error_DiaLibre)))
Ejemplo Output

La salida de la primera semana y los primeros 5 ejecutivos se verá de esta forma:

nombre ejecutivo s1-d1 s1-d2 s1-d3 s1-d4 s1-d5 s1-d6 s1-d7
Ejecutivo 1 0 0 0 1 1 1 0
Ejecutivo 2 1 1 0 0 1 0 1
Ejecutivo 3 0 1 0 0 1 1 1
Ejecutivo 4 1 1 1 1 1 0 0
Ejecutivo 5 1 0 1 0 0 1 1

Espero que este tutorial halla servido, en caso de dudas no dude en contactarme por linked in en https://cl.linkedin.com/in/danielfischerm, mi correo dfischer@ug.uchile.cl o cualquiera de mis medios de contacto.

Print Friendly, PDF & Email