Samuel Suarez, Sebastian Barbosa, Samuel Rodriguez

1 Introducción al Problema

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.

2 Descripción de Datos y Variables Seleccionadas

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:

2.1 Variables categóricas:

  • Horas Extra: Se hipotetizó que los empleados que trabajan horas extras tendrían mayor rotación debido al estrés acumulado.
  • Departamento: Se esperaba que departamentos con mayor presión (como Ventas) mostraran mayor rotación.
  • Cargo: Se planteó que posiciones intermedias con alta responsabilidad pero baja compensación tendrían mayor rotación.

2.2 Variables cuantitativas:

  • Años con el mismo jefe: Se hipotetizó que relaciones prolongadas sin crecimiento aumentarían la rotación.
  • Ingreso Mensual: Se esperaba que salarios bajos correlacionaran con mayor rotación.
  • Rendimiento Laboral: Se planteó una relación en forma de U, donde tanto bajo como alto rendimiento aumentarían la rotación.

3 Exploración de Datos

3.1 Análisis Univariado

  • Horas Extra: 38.7% de empleados realizan horas extras.
  • Departamento: 83.6% pertenece a IyD, 15.3% a Ventas y solo 1.1% a RH.
  • Cargos: Técnicos de Laboratorio (41.6%) e Investigadores Científicos (34.4%) son los más comunes.
  • Años con mismo jefe: Mediana de 3 años, sugiriendo que la mitad de los empleados llevan 3 años o menos con su jefe actual.
  • Ingresos: Distribución desigual con mediana en $4,559, indicando disparidades salariales.

3.2 Análisis Bivariado

  • Horas Extra: No mostró relación significativa con rotación (7.8% vs 8.6%).
  • Departamento: RH mostró mayor rotación (28.6%) pero con muestra pequeña (7 casos).
  • Cargo: Representantes de Ventas tuvieron mayor rotación (20.8%).
  • Años con jefe: Empleados que rotaron tenían menos años con su jefe (media 3.12 vs 3.89).
  • Ingresos: Rotación significativamente mayor en empleados con menores ingresos (mediana $3,394 vs $4,680).

4 Análisis de Datos con R

4.1 Carga de Librerías y Datos

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`

4.2 Análisis Univariado

4.2.1 Variables Cualitativas

4.2.1.1 Horas Extras

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

4.2.1.2 Departamento

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

4.3 Análisis Bivariado

4.3.1 Horas Extras vs. Rotación

tabla_horas_rotacion <- table(datos$`Horas extras`, datos$Rotación)
kable(tabla_horas_rotacion, caption = "Tabla de contingencia: Horas Extras vs. Rotación")
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()

5 MODELO DE PREDICCIÓN DE ROTACIÓN

# 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 ...

6 Preprocesamiento

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

7 Dividir los datos en entrenamiento (70%) y prueba (30%)

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, ]

8 Preparar el preprocesamiento (one-hot encoding y escalado)

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)

9 Entrenar el modelo de regresión logística

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

10 Evaluar el modelo en datos de prueba

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

11 Ajustar umbral de corte para maximizar F1-score

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

12 Evaluar con el mejor umbral

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

13 Escenarios de Empleados con Riesgos de Rotación

13.1 Ejecutivo de Ventas con Alta Carga Laboral y Baja Compensación

  • Perfil: Un empleado con 3 años en la empresa, alta carga de trabajo, salario por debajo del promedio de la industria y sin oportunidades claras de crecimiento.
  • Riesgos: Estrés, agotamiento, desmotivación y atracción por ofertas laborales con mejores condiciones.
  • Impacto: Alta probabilidad de renuncia y difícil reemplazo debido a la curva de aprendizaje del puesto.

13.2 Investigador Científico con Estancamiento Profesional

  • Perfil: Trabaja en el área de I+D, tiene más de 5 años en la compañía con el mismo jefe, sin cambios significativos en su rol ni promociones.
  • Riesgos: Falta de motivación, percepción de estancamiento y búsqueda de nuevos retos en otras empresas.
  • Impacto: Pérdida de talento valioso y costos elevados en contratación y capacitación de un reemplazo.

13.3 Empleado de Recursos Humanos con Baja Satisfacción Salarial

  • Perfil: Lleva 2 años en la empresa, maneja tareas operativas y estratégicas, pero su salario no es competitivo con el mercado.
  • Riesgos: Insatisfacción laboral, disminución del compromiso y alta probabilidad de aceptar ofertas con mejor retribución.
  • Impacto: Disrupción en procesos internos de contratación y retención de personal.

14 Beneficios de un Modelo Predictivo de Rotación

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:

  • Reducción de Costos en Contratación: Al disminuir la rotación, se evitan gastos asociados con la selección, contratación y capacitación de nuevos empleados.
  • Optimización del Clima Laboral: Al abordar factores de desmotivación antes de que generen renuncias, se mejora la moral y productividad del equipo.
  • Planificación de Recursos Humanos: Permite anticipar necesidades de personal y desarrollar planes de sucesión para puestos críticos.
  • Aumento de la Retención de Talento Clave: Al identificar patrones de rotación, se pueden implementar estrategias personalizadas como aumentos salariales, oportunidades de crecimiento y beneficios adicionales.

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.

15 Recomendaciones con IA Generativa

15.0.1 Prompt utilizado para generar recomendaciones:

“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.”

15.0.2 Recomendaciones generadas:

  1. Programa de Retención para Representantes de Ventas: Implementar mentorías, establecer metas realistas y crear un plan de desarrollo profesional.
  2. Revisión Salarial Competitiva: Análisis de mercado para ajustar salarios, especialmente en posiciones con alta rotación.
  3. Capacitación en Liderazgo: Programas de formación para jefes en gestión de equipos y desarrollo de talento.
  4. Encuestas de Clima Laboral: Especialmente en RH para identificar causas de alta rotación.
  5. Plan de Carrera Transparente: Comunicar claramente los caminos de promoción y los criterios para avanzar.

16 Evaluación del Modelo y Aplicación Interactiva

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