Samuel Suarez, Sebastian Barbosa, Samuel Rodriguez
En el ámbito de People Analytics, la rotación de personal representa un desafío crítico para las organizaciones. Este fenómeno no solo implica costos directos asociados a la contratación y capacitación de nuevos empleados, sino también pérdida de conocimiento organizacional y disminución en la moral del equipo.
El presente análisis busca identificar los factores que influyen en la rotación dentro de una organización, utilizando técnicas analíticas para predecir riesgos y proponer estrategias de retención efectivas. Este enfoque permite a la empresa actuar de manera proactiva, optimizando sus recursos humanos y mejorando su competitividad en el mercado.
El conjunto de datos analizado contiene información sobre 628 empleados, con una tasa de rotación del 8.3%. Se seleccionaron variables clave basadas en su potencial relación teórica con la rotación:
library(readxl)
library(ggplot2)
library(dplyr)
##
## Adjuntando el paquete: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(tidyr)
library(knitr)
datos <- read_excel("variables.xlsx")
## New names:
## • `` -> `...4`
## • `` -> `...6`
horas_extras <- datos %>%
count(`Horas extras`) %>%
mutate(porcentaje = n/sum(n)*100)
ggplot(horas_extras, aes(x = `Horas extras`, y = n, fill = `Horas extras`)) +
geom_bar(stat = "identity") +
geom_text(aes(label = paste0(round(porcentaje, 1), "%")), vjust = -0.5) +
labs(title = "Distribución de Horas Extras",
x = "Horas Extras", y = "Frecuencia") +
theme_minimal()
departamento <- datos %>%
count(Departamento) %>%
mutate(porcentaje = n/sum(n)*100) %>%
arrange(desc(n))
ggplot(departamento, aes(x = reorder(Departamento, -n), y = n, fill = Departamento)) +
geom_bar(stat = "identity") +
geom_text(aes(label = paste0(round(porcentaje, 1), "%")), vjust = -0.5) +
labs(title = "Distribución por Departamento",
x = "Departamento", y = "Frecuencia") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
tabla_horas_rotacion <- table(datos$`Horas extras`, datos$Rotación)
kable(tabla_horas_rotacion, caption = "Tabla de contingencia: Horas Extras vs. Rotación")
| No | Si | |
|---|---|---|
| No | 944 | 110 |
| Si | 289 | 127 |
ggplot(datos, aes(x = `Horas extras`, fill = Rotación)) +
geom_bar(position = "fill") +
labs(title = "Proporción de Rotación por Horas Extras",
x = "Horas Extras", y = "Proporción") +
scale_y_continuous(labels = scales::percent) +
theme_minimal()
# Cargar librerías necesarias
library(readxl)
library(caret)
## Cargando paquete requerido: lattice
library(ggplot2)
library(pROC)
## Type 'citation("pROC")' for a citation.
##
## Adjuntando el paquete: 'pROC'
## The following objects are masked from 'package:stats':
##
## cov, smooth, var
library(dplyr)
# 1. Cargar y preparar los datos
datos <- read_excel("variables.xlsx")
## New names:
## • `` -> `...4`
## • `` -> `...6`
# Limpiar nombres de columnas (opcional)
colnames(datos) <- make.names(colnames(datos))
# Ver estructura de los datos
str(datos)
## tibble [1,470 × 9] (S3: tbl_df/tbl/data.frame)
## $ Horas.extras : chr [1:1470] "Si" "No" "Si" "Si" ...
## $ Departamento : chr [1:1470] "Ventas" "IyD" "IyD" "IyD" ...
## $ Cargo : chr [1:1470] "Ejecutivo_Ventas" "Investigador_Cientifico" "Tecnico_Laboratorio" "Investigador_Cientifico" ...
## $ ...4 : logi [1:1470] NA NA NA NA NA NA ...
## $ Rotación : chr [1:1470] "Si" "No" "Si" "No" ...
## $ ...6 : logi [1:1470] NA NA NA NA NA NA ...
## $ Años.con.el.mismo.jefe: num [1:1470] 5 7 0 0 2 6 0 0 8 7 ...
## $ Rendimiento.laboral : num [1:1470] 3 4 3 3 3 3 4 4 4 3 ...
## $ Ingreso.mensual : num [1:1470] 5993 5130 2090 2909 3468 ...
# Seleccionar variables relevantes
datos_modelo <- datos %>%
select(Rotación, Departamento, Cargo, Ingreso.mensual, Años.con.el.mismo.jefe)
# Renombrar columnas para mayor claridad
colnames(datos_modelo) <- c("Rotacion", "Departamento", "Cargo", "Ingreso_Mensual", "Anios_con_mismo_jefe")
# Convertir Rotación a factor (Si/No)
datos_modelo$Rotacion <- factor(datos_modelo$Rotacion, levels = c("Si", "No"))
# Convertir variables categóricas a factores
datos_modelo$Departamento <- as.factor(datos_modelo$Departamento)
datos_modelo$Cargo <- as.factor(datos_modelo$Cargo)
# Verificar valores faltantes
datos_modelo <- na.omit(datos_modelo)
set.seed(123) # Para reproducibilidad
indices_entrenamiento <- createDataPartition(datos_modelo$Rotacion, p = 0.7, list = FALSE)
datos_entrenamiento <- datos_modelo[indices_entrenamiento, ]
datos_prueba <- datos_modelo[-indices_entrenamiento, ]
preprocesamiento <- preProcess(datos_entrenamiento,
method = c("center", "scale", "nzv", "medianImpute"))
# Aplicar preprocesamiento a ambos conjuntos
datos_entrenamiento_proc <- predict(preprocesamiento, datos_entrenamiento)
datos_prueba_proc <- predict(preprocesamiento, datos_prueba)
modelo <- train(
Rotacion ~ .,
data = datos_entrenamiento_proc,
method = "glm",
family = "binomial",
trControl = trainControl(method = "cv", number = 5)
)
# Ver resumen del modelo
summary(modelo)
##
## Call:
## NULL
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 3.6973 0.8704 4.248 2.16e-05 ***
## DepartamentoRH 12.2140 510.7887 0.024 0.98092
## DepartamentoVentas -1.0297 1.2598 -0.817 0.41374
## CargoDirector_Manofactura -1.2298 0.9271 -1.326 0.18468
## CargoEjecutivo_Ventas -1.0222 1.5295 -0.668 0.50392
## CargoGerente 0.2166 1.2484 0.173 0.86228
## CargoInvestigador_Cientifico -2.1544 0.9819 -2.194 0.02822 *
## CargoRecursos_Humanos -14.6060 510.7898 -0.029 0.97719
## CargoRepresentante_Salud -1.1240 0.9242 -1.216 0.22389
## CargoRepresentante_Ventas -2.3251 1.6099 -1.444 0.14868
## CargoTecnico_Laboratorio -2.5122 0.9767 -2.572 0.01010 *
## Ingreso_Mensual -0.2338 0.2301 -1.016 0.30970
## Anios_con_mismo_jefe 0.3456 0.1097 3.150 0.00163 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 909.69 on 1029 degrees of freedom
## Residual deviance: 836.19 on 1017 degrees of freedom
## AIC: 862.19
##
## Number of Fisher Scoring iterations: 14
predicciones <- predict(modelo, newdata = datos_prueba_proc)
probabilidades <- predict(modelo, newdata = datos_prueba_proc, type = "prob")[, "Si"]
# Matriz de confusión
confusionMatrix(predicciones, datos_prueba_proc$Rotacion, positive = "Si")
## Confusion Matrix and Statistics
##
## Reference
## Prediction Si No
## Si 0 0
## No 71 369
##
## Accuracy : 0.8386
## 95% CI : (0.8009, 0.8718)
## No Information Rate : 0.8386
## P-Value [Acc > NIR] : 0.5316
##
## Kappa : 0
##
## Mcnemar's Test P-Value : <2e-16
##
## Sensitivity : 0.0000
## Specificity : 1.0000
## Pos Pred Value : NaN
## Neg Pred Value : 0.8386
## Prevalence : 0.1614
## Detection Rate : 0.0000
## Detection Prevalence : 0.0000
## Balanced Accuracy : 0.5000
##
## 'Positive' Class : Si
##
# Curva ROC y AUC
roc_curve <- roc(datos_prueba_proc$Rotacion, probabilidades)
## Setting levels: control = Si, case = No
## Setting direction: controls > cases
plot(roc_curve, main = "Curva ROC")
auc(roc_curve)
## Area under the curve: 0.7104
umbrales <- seq(0.1, 0.9, by = 0.05)
metricas <- data.frame()
for (umbral in umbrales) {
preds <- ifelse(probabilidades > umbral, "Si", "No")
preds <- factor(preds, levels = c("Si", "No"))
cm <- confusionMatrix(preds, datos_prueba_proc$Rotacion, positive = "Si")
metricas <- rbind(metricas, data.frame(
Umbral = umbral,
Precision = cm$byClass["Precision"],
Recall = cm$byClass["Recall"],
F1 = cm$byClass["F1"]
))
}
# Graficar métricas por umbral
ggplot(metricas, aes(x = Umbral)) +
geom_line(aes(y = Precision, color = "Precisión")) +
geom_line(aes(y = Recall, color = "Recall")) +
geom_line(aes(y = F1, color = "F1-score")) +
labs(title = "Métricas por Umbral de Corte", y = "Valor") +
scale_color_manual(values = c("Precisión" = "blue", "Recall" = "red", "F1-score" = "green")) +
theme_minimal()
## Warning: Removed 9 rows containing missing values or values outside the scale range
## (`geom_line()`).
## Removed 9 rows containing missing values or values outside the scale range
## (`geom_line()`).
# Seleccionar el umbral con mayor F1-score
mejor_umbral <- metricas$Umbral[which.max(metricas$F1)]
cat("Mejor umbral para maximizar F1-score:", mejor_umbral, "\n")
## Mejor umbral para maximizar F1-score: 0.2
predicciones_finales <- ifelse(probabilidades > mejor_umbral, "Si", "No")
predicciones_finales <- factor(predicciones_finales, levels = c("Si", "No"))
# Matriz de confusión final
cm_final <- confusionMatrix(predicciones_finales, datos_prueba_proc$Rotacion, positive = "Si")
print(cm_final)
## Confusion Matrix and Statistics
##
## Reference
## Prediction Si No
## Si 39 91
## No 32 278
##
## Accuracy : 0.7205
## 95% CI : (0.676, 0.7619)
## No Information Rate : 0.8386
## P-Value [Acc > NIR] : 1
##
## Kappa : 0.2266
##
## Mcnemar's Test P-Value : 1.698e-07
##
## Sensitivity : 0.54930
## Specificity : 0.75339
## Pos Pred Value : 0.30000
## Neg Pred Value : 0.89677
## Prevalence : 0.16136
## Detection Rate : 0.08864
## Detection Prevalence : 0.29545
## Balanced Accuracy : 0.65134
##
## 'Positive' Class : Si
##
# 9. Importancia de variables
importancia <- varImp(modelo)
plot(importancia, main = "Importancia de Variables")
# 10. Simular escenarios de predicción
escenarios <- data.frame(
Departamento = factor(c("IyD", "Ventas", "RH"), levels = levels(datos_modelo$Departamento)),
Cargo = factor(c("Investigador_Cientifico", "Ejecutivo_Ventas", "Recursos_Humanos"),
levels = levels(datos_modelo$Cargo)),
Ingreso_Mensual = c(6000, 4000, 2500),
Anios_con_mismo_jefe = c(5, 2, 1)
)
# Aplicar preprocesamiento a los escenarios
escenarios_proc <- predict(preprocesamiento, escenarios)
# Predecir probabilidades
prob_escenarios <- predict(modelo, newdata = escenarios_proc, type = "prob")
# Mostrar resultados
resultados_escenarios <- cbind(escenarios, Prob_Rotacion = prob_escenarios$Si)
print(resultados_escenarios)
## Departamento Cargo Ingreso_Mensual Anios_con_mismo_jefe
## 1 IyD Investigador_Cientifico 6000 5
## 2 Ventas Ejecutivo_Ventas 4000 2
## 3 RH Recursos_Humanos 2500 1
## Prob_Rotacion
## 1 0.1610743
## 2 0.1734398
## 3 0.2316853
Un modelo predictivo de rotación permite a la empresa ser más eficiente en la gestión del talento al identificar empleados con alto riesgo de salida y aplicar estrategias de retención. Algunos beneficios clave incluyen:
Implementar un modelo predictivo de rotación con IA proporciona una ventaja competitiva, asegurando una mejor administración del capital humano y mejorando la estabilidad organizacional.
“Basado en un análisis de rotación de personal donde los principales hallazgos son: 1) Representantes de Ventas tienen 20.8% de rotación, 2) RH tiene 28.6% de rotación pero con solo 7 casos, 3) Menores ingresos correlacionan con mayor rotación, 4) Menos años con mismo jefe se asocia a mayor rotación. Genera 5 recomendaciones estratégicas detalladas para reducir la rotación, considerando estos hallazgos y buenas prácticas en gestión de personas.”
# Ajustar umbral de corte para múltiples métricas
umbrales <- seq(0.1, 0.9, by = 0.05)
resultados <- data.frame()
for (umbral in umbrales) {
preds <- ifelse(probabilidades > umbral, "Si", "No")
preds <- factor(preds, levels = c("Si", "No"))
cm <- confusionMatrix(preds, datos_prueba_proc$Rotacion, positive = "Si")
# Extraer métricas
precision <- cm$byClass["Precision"]
recall <- cm$byClass["Recall"]
f1 <- cm$byClass["F1"]
sensibilidad <- cm$byClass["Sensitivity"]
especificidad <- cm$byClass["Specificity"]
desempeno <- cm$overall["Accuracy"]
# Índice de Youden: J = Sensibilidad + Especificidad - 1
youden <- sensibilidad + especificidad - 1
# Métrica compuesta: promedio de Precision, Recall y F1
metrica_compuesta <- mean(c(precision, recall, f1))
resultados <- rbind(resultados, data.frame(
Umbral = umbral,
Precision = precision,
Recall = recall,
F1 = f1,
Sensibilidad = sensibilidad,
Especificidad = especificidad,
Desempeño = desempeno,
Youden = youden,
Metrica_Compuesta = metrica_compuesta
))
}
# Mejor umbral según métrica compuesta
mejor_umbral_compuesto <- resultados$Umbral[which.max(resultados$Metrica_Compuesta)]
print(paste("Mejor umbral según la métrica compuesta:", mejor_umbral_compuesto))
## [1] "Mejor umbral según la métrica compuesta: 0.1"
# Mejor umbral según el índice de Youden
mejor_umbral_youden <- resultados$Umbral[which.max(resultados$Youden)]
print(paste("Mejor umbral según el índice de Youden:", mejor_umbral_youden))
## [1] "Mejor umbral según el índice de Youden: 0.2"
# Mejor umbral según el desempeño (Accuracy)
mejor_umbral_desempeno <- resultados$Umbral[which.max(resultados$Desempeño)]
print(paste("Mejor umbral según desempeño (Accuracy):", mejor_umbral_desempeno))
## [1] "Mejor umbral según desempeño (Accuracy): 0.5"