library(tidyverse)
library(caret)
library(randomForest)
library(xgboost)
library(ROCR)
library(corrplot)
library(gridExtra)
library(pROC)
library(readr)
library(pander)
library(dplyr)
Cargar base de datos
HR = read_csv("HR_comma_sep (1).csv")
## Rows: 14999 Columns: 10
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (2): Department, salary
## dbl (8): satisfaction_level, last_evaluation, number_project, average_montly...
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
HR = data.frame(HR)
Cambiar los nombres columna de la data
colnames(HR) <- c("nivel_satisfaccion", "ultima_evaluacion", "numero_proyecto","promedio_horas_mes","gasto_tiempo_empresa","accidente_trabajo","izquierda", "promocion_ultimos_5años", "departamento", "salario")
pander(head(HR,5))
| nivel_satisfaccion | ultima_evaluacion | numero_proyecto | promedio_horas_mes |
|---|---|---|---|
| 0.38 | 0.53 | 2 | 157 |
| 0.8 | 0.86 | 5 | 262 |
| 0.11 | 0.88 | 7 | 272 |
| 0.72 | 0.87 | 5 | 223 |
| 0.37 | 0.52 | 2 | 159 |
| gasto_tiempo_empresa | accidente_trabajo | izquierda | promocion_ultimos_5años |
|---|---|---|---|
| 3 | 0 | 1 | 0 |
| 6 | 0 | 1 | 0 |
| 4 | 0 | 1 | 0 |
| 5 | 0 | 1 | 0 |
| 3 | 0 | 1 | 0 |
| departamento | salario |
|---|---|
| sales | low |
| sales | medium |
| sales | medium |
| sales | low |
| sales | low |
Cambiar datos numericos a factor
HR$accidente_trabajo = as.factor(HR$accidente_trabajo)
HR$izquierda = as.factor(HR$izquierda)
HR$promocion_ultimos_5años = as.factor(HR$promocion_ultimos_5años)
HR$departamento = as.factor(HR$departamento)
HR$salario = as.factor(HR$salario)
identificacion de varibales y resumen de las mismas
str(HR)
## 'data.frame': 14999 obs. of 10 variables:
## $ nivel_satisfaccion : num 0.38 0.8 0.11 0.72 0.37 0.41 0.1 0.92 0.89 0.42 ...
## $ ultima_evaluacion : num 0.53 0.86 0.88 0.87 0.52 0.5 0.77 0.85 1 0.53 ...
## $ numero_proyecto : num 2 5 7 5 2 2 6 5 5 2 ...
## $ promedio_horas_mes : num 157 262 272 223 159 153 247 259 224 142 ...
## $ gasto_tiempo_empresa : num 3 6 4 5 3 3 4 5 5 3 ...
## $ accidente_trabajo : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
## $ izquierda : Factor w/ 2 levels "0","1": 2 2 2 2 2 2 2 2 2 2 ...
## $ promocion_ultimos_5años: Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
## $ departamento : Factor w/ 10 levels "accounting","hr",..: 8 8 8 8 8 8 8 8 8 8 ...
## $ salario : Factor w/ 3 levels "high","low","medium": 2 3 3 2 2 2 2 2 2 2 ...
summary(HR)
## nivel_satisfaccion ultima_evaluacion numero_proyecto promedio_horas_mes
## Min. :0.0900 Min. :0.3600 Min. :2.000 Min. : 96.0
## 1st Qu.:0.4400 1st Qu.:0.5600 1st Qu.:3.000 1st Qu.:156.0
## Median :0.6400 Median :0.7200 Median :4.000 Median :200.0
## Mean :0.6128 Mean :0.7161 Mean :3.803 Mean :201.1
## 3rd Qu.:0.8200 3rd Qu.:0.8700 3rd Qu.:5.000 3rd Qu.:245.0
## Max. :1.0000 Max. :1.0000 Max. :7.000 Max. :310.0
##
## gasto_tiempo_empresa accidente_trabajo izquierda promocion_ultimos_5años
## Min. : 2.000 0:12830 0:11428 0:14680
## 1st Qu.: 3.000 1: 2169 1: 3571 1: 319
## Median : 3.000
## Mean : 3.498
## 3rd Qu.: 4.000
## Max. :10.000
##
## departamento salario
## sales :4140 high :1237
## technical :2720 low :7316
## support :2229 medium:6446
## IT :1227
## product_mng: 902
## marketing : 858
## (Other) :2923
Porcentaje y número de empleados que dejaron o no la empresa
pander(table(HR$izquierda))
| 0 | 1 |
|---|---|
| 11428 | 3571 |
pander(prop.table(table(HR$izquierda)) * 100)
| 0 | 1 |
|---|---|
| 76.19 | 23.81 |
Descripción de empleados que dejaron o no la empresa por diferentes grupos
HR %>%
group_by(izquierda) %>%
summarise(
n = n(),
satisfaccion_media = mean(nivel_satisfaccion),
evaluacion_media = mean(ultima_evaluacion),
proyectos_medio = mean(numero_proyecto),
horas_media = mean(promedio_horas_mes),
años_medio = mean(gasto_tiempo_empresa)
)
## # A tibble: 2 × 7
## izquierda n satisfaccion_media evaluacion_media proyectos_medio
## <fct> <int> <dbl> <dbl> <dbl>
## 1 0 11428 0.667 0.715 3.79
## 2 1 3571 0.440 0.718 3.86
## # ℹ 2 more variables: horas_media <dbl>, años_medio <dbl>
Análisis de correlación
HR_num <- HR %>%
select(nivel_satisfaccion, ultima_evaluacion, numero_proyecto,
promedio_horas_mes, gasto_tiempo_empresa) %>%
mutate(izquierda_num = as.numeric(as.character(HR$izquierda)))
correlacion <- cor(HR_num)
pander(print(correlacion))
nivel_satisfaccion ultima_evaluacion numero_proyecto
nivel_satisfaccion 1.00000000 0.10502121 -0.14296959 ultima_evaluacion 0.10502121 1.00000000 0.34933259 numero_proyecto -0.14296959 0.34933259 1.00000000 promedio_horas_mes -0.02004811 0.33974180 0.41721063 gasto_tiempo_empresa -0.10086607 0.13159072 0.19678589 izquierda_num -0.38837498 0.00656712 0.02378719 promedio_horas_mes gasto_tiempo_empresa izquierda_num nivel_satisfaccion -0.02004811 -0.1008661 -0.38837498 ultima_evaluacion 0.33974180 0.1315907 0.00656712 numero_proyecto 0.41721063 0.1967859 0.02378719 promedio_horas_mes 1.00000000 0.1277549 0.07128718 gasto_tiempo_empresa 0.12775491 1.0000000 0.14482217 izquierda_num 0.07128718 0.1448222 1.00000000
| nivel_satisfaccion | ultima_evaluacion | |
|---|---|---|
| nivel_satisfaccion | 1 | 0.105 |
| ultima_evaluacion | 0.105 | 1 |
| numero_proyecto | -0.143 | 0.3493 |
| promedio_horas_mes | -0.02005 | 0.3397 |
| gasto_tiempo_empresa | -0.1009 | 0.1316 |
| izquierda_num | -0.3884 | 0.006567 |
| numero_proyecto | promedio_horas_mes | |
|---|---|---|
| nivel_satisfaccion | -0.143 | -0.02005 |
| ultima_evaluacion | 0.3493 | 0.3397 |
| numero_proyecto | 1 | 0.4172 |
| promedio_horas_mes | 0.4172 | 1 |
| gasto_tiempo_empresa | 0.1968 | 0.1278 |
| izquierda_num | 0.02379 | 0.07129 |
| gasto_tiempo_empresa | izquierda_num | |
|---|---|---|
| nivel_satisfaccion | -0.1009 | -0.3884 |
| ultima_evaluacion | 0.1316 | 0.006567 |
| numero_proyecto | 0.1968 | 0.02379 |
| promedio_horas_mes | 0.1278 | 0.07129 |
| gasto_tiempo_empresa | 1 | 0.1448 |
| izquierda_num | 0.1448 | 1 |
p1 = ggplot(HR, aes(x = izquierda, y = nivel_satisfaccion, fill = izquierda)) +
geom_boxplot() +
labs(title = "Nivel de Satisfacción vs Retención",
x = "Dejó la empresa", y = "Satisfacción") +
theme_minimal() +
scale_fill_manual(values = c("0" = "#2ecc71", "1" = "#e74c3c"))
p2 <- ggplot(HR, aes(x = numero_proyecto, y = promedio_horas_mes, color = izquierda)) +
geom_point(alpha = 0.3) +
labs(title = "Horas Trabajadas vs Número de Proyectos",
x = "Número de Proyectos", y = "Horas Promedio/Mes") +
theme_minimal() +
scale_color_manual(values = c("0" = "#2ecc71", "1" = "#e74c3c"))
p3 <- HR %>%
group_by(departamento, izquierda) %>%
summarise(n = n()) %>%
mutate(prop = n / sum(n)) %>%
filter(izquierda == "1") %>%
ggplot(aes(x = reorder(departamento, prop), y = prop, fill = departamento)) +
geom_bar(stat = "identity") +
coord_flip() +
labs(title = "Tasa de Rotación por Departamento",
x = "Departamento", y = "Proporción que dejó la empresa") +
theme_minimal() +
theme(legend.position = "none")
## `summarise()` has grouped output by 'departamento'. You can override using the
## `.groups` argument.
corrplot(cor(HR_num), method = "color", type = "upper",
tl.col = "black", tl.srt = 45,
title = "Matriz de Correlación")
p4 <- ggplot(HR, aes(x = nivel_satisfaccion, y = ultima_evaluacion, color = izquierda)) +
geom_point(alpha = 0.4) +
labs(title = "Satisfacción vs Última Evaluación",
x = "Nivel de Satisfacción", y = "Última Evaluación") +
theme_minimal() +
scale_color_manual(values = c("0" = "#2ecc71", "1" = "#e74c3c"))
Satisfaccíon según empleados que dejaron o no la empresa
print(p1)
Horas trabajadas segun número de proyectos a los que contribuye el
empleado
print(p2)
Tasa de rotación por departamento
print(p3)
Satisfacción vs Evaluación
print(p4)
Semilla
set.seed(260126)
Variables dummy
HR_model = HR %>%
mutate(
izquierda_num = as.numeric(as.character(izquierda)),
accidente_num = as.numeric(as.character(accidente_trabajo)),
promocion_num = as.numeric(as.character(promocion_ultimos_5años))
)
dummies = model.matrix(~ departamento + salario - 1, data = HR_model)
HR_final = cbind(HR_model %>%
select(nivel_satisfaccion, ultima_evaluacion,
numero_proyecto, promedio_horas_mes,
gasto_tiempo_empresa, accidente_num,
promocion_num, izquierda_num),
dummies)
Train y Test 70/30
indice_train = createDataPartition(HR_final$izquierda_num, p = 0.7, list = FALSE)
train = HR_final[indice_train, ]
test = HR_final[-indice_train, ]
X_train = train %>% select(-izquierda_num)
y_train = train$izquierda_num
X_test = test %>% select(-izquierda_num)
y_test = test$izquierda_num
Modelos
Logistico
LM = glm(izquierda_num ~ ., data = train, family = binomial)
PL = predict(LM, test, type = "response")
Random Forest
MRF = randomForest(as.factor(izquierda_num) ~ .,
data = train,
ntree = 500,
importance = TRUE)
PRF = predict(MRF, test, type = "prob")[,2]
XGBoost
dtrain = xgb.DMatrix(data = as.matrix(X_train), label = y_train)
dtest = xgb.DMatrix(data = as.matrix(X_test), label = y_test)
params = list(
objective = "binary:logistic",
eval_metric = "auc",
max_depth = 6,
eta = 0.1,
subsample = 0.8,
colsample_bytree = 0.8
)
XGB = xgb.train(
params = params,
data = dtrain,
nrounds = 100,
watchlist = list(train = dtrain, test = dtest),
early_stopping_rounds = 10,
verbose = 0
)
## Warning in throw_err_or_depr_msg("Parameter '", match_old, "' has been renamed
## to '", : Parameter 'watchlist' has been renamed to 'evals'. This warning will
## become an error in a future version.
PXGB <- predict(XGB, dtest)
Cálculo de Métricas
evaluar_modelo <- function(y_true, y_pred, umbral = 0.5) {
y_pred_class <- ifelse(y_pred > umbral, 1, 0)
conf_matrix <- table(Predicho = y_pred_class, Real = y_true)
accuracy <- sum(diag(conf_matrix)) / sum(conf_matrix)
# Calcular precisión, recall, F1 - CORREGIDO
TP <- sum(y_pred_class == 1 & y_true == 1)
FP <- sum(y_pred_class == 1 & y_true == 0)
FN <- sum(y_pred_class == 0 & y_true == 1)
precision <- ifelse((TP + FP) > 0, TP / (TP + FP), 0)
recall <- ifelse((TP + FN) > 0, TP / (TP + FN), 0)
f1 <- ifelse((precision + recall) > 0,
2 * (precision * recall) / (precision + recall),
0)
# AUC
roc_obj <- roc(y_true, y_pred, quiet = TRUE)
auc_val <- as.numeric(auc(roc_obj))
# RETORNAR COMO DATA.FRAME en lugar de lista
return(data.frame(
accuracy = accuracy,
precision = precision,
recall = recall,
f1 = f1,
auc = auc_val
))
}
Evaluación de Modelos
eval_log <- evaluar_modelo(y_test, PL)
print(eval_log)
## accuracy precision recall f1 auc
## 1 0.7939542 0.6125 0.3660131 0.4582116 0.8210045
eval_rf <- evaluar_modelo(y_test, PRF)
print(eval_rf)
## accuracy precision recall f1 auc
## 1 0.9873305 0.9931907 0.9533147 0.9728442 0.9938217
eval_xgb <- evaluar_modelo(y_test, PXGB)
print(eval_xgb)
## accuracy precision recall f1 auc
## 1 0.9826628 0.986288 0.9402428 0.9627151 0.9941078
Comparación de Modelos
comparacion <- data.frame(
Modelo = c("Regresión Logística", "Random Forest", "XGBoost"),
Accuracy = c(eval_log$accuracy, eval_rf$accuracy, eval_xgb$accuracy),
Precision = c(eval_log$precision, eval_rf$precision, eval_xgb$precision),
Recall = c(eval_log$recall, eval_rf$recall, eval_xgb$recall),
F1 = c(eval_log$f1, eval_rf$f1, eval_xgb$f1),
AUC = c(eval_log$auc, eval_rf$auc, eval_xgb$auc)
)
pander(print(comparacion))
Modelo Accuracy Precision Recall F1 AUC
1 Regresión Logística 0.7939542 0.6125000 0.3660131 0.4582116 0.8210045 2 Random Forest 0.9873305 0.9931907 0.9533147 0.9728442 0.9938217 3 XGBoost 0.9826628 0.9862880 0.9402428 0.9627151 0.9941078
| Modelo | Accuracy | Precision | Recall | F1 | AUC |
|---|---|---|---|---|---|
| Regresión Logística | 0.794 | 0.6125 | 0.366 | 0.4582 | 0.821 |
| Random Forest | 0.9873 | 0.9932 | 0.9533 | 0.9728 | 0.9938 |
| XGBoost | 0.9827 | 0.9863 | 0.9402 | 0.9627 | 0.9941 |
Curvas ROC
roc_log <- roc(y_test, PL, quiet = TRUE)
roc_rf <- roc(y_test, PRF, quiet = TRUE)
roc_xgb <- roc(y_test, PXGB, quiet = TRUE)
plot(roc_log, col = "blue", main = "Comparación Curvas ROC")
lines(roc_rf, col = "green")
lines(roc_xgb, col = "red")
legend("bottomright",
legend = c(paste0("Reg. Logística (AUC=", round(auc(roc_log), 3), ")"),
paste0("Random Forest (AUC=", round(auc(roc_rf), 3), ")"),
paste0("XGBoost (AUC=", round(auc(roc_xgb), 3), ")")),
col = c("blue", "green", "red"), lty = 1)
Sobreajuste
PL_train = predict(LM, train, type = "response")
PRF_train = predict(MRF, train, type = "prob")[,2]
PXGB_train = predict(XGB, dtrain)
eval_log_train = evaluar_modelo(y_train, PL_train)
eval_rf_train = evaluar_modelo(y_train, PRF_train)
eval_xgb_train = evaluar_modelo(y_train, PXGB_train)
overfitting = data.frame(
Modelo = c("Regresión Logística", "Random Forest", "XGBoost"),
AUC_Train = c(eval_log_train$auc,
eval_rf_train$auc,
eval_xgb_train$auc),
AUC_Test = c(eval_log$auc,
eval_rf$auc,
eval_xgb$auc)
)
overfitting$Gap_AUC = overfitting$AUC_Train - overfitting$AUC_Test
pander(overfitting)
| Modelo | AUC_Train | AUC_Test | Gap_AUC |
|---|---|---|---|
| Regresión Logística | 0.8206 | 0.821 | -0.0004401 |
| Random Forest | 1 | 0.9938 | 0.006141 |
| XGBoost | 0.998 | 0.9941 | 0.003859 |
La evaluación del sobreajuste se realizó comparando el AUC en los conjuntos de entrenamiento y prueba. Los resultados indican que ninguno de los modelos presenta sobreajuste significativo. La regresión logística muestra una estabilidad sobresaliente, mientras que Random Forest y XGBoost alcanzan un desempeño predictivo superior sin comprometer la capacidad de generalización.
El modelo XGBoost es el modelo con la mejor alternativa predictiva con un AUC superior a 0.99 y un bajo sobreajuste, demostrando que puede capturar relaciones complejas y no lineales entre variables, manejar datos categóricos, y generalizar predicciones confiables que permita a RRHH anticipar y prevenir la rotación de empleados mediante estrategias de retención basadas en evidencia.
A partir de los resultados del modelo XGBoost, propongo una estrategia de negocio basada en la gestión del riesgo, utilizando las probabilidades estimadas como un mecanismo de priorización. El modelo permitiria identificar de manera anticipada los casos con mayor probabilidad de ocurrencia del evento, facilitando una asignación más eficiente de recursos y enfocando los esfuerzos donde el impacto potencial es mayor.
La gestión más que todo se orienta a intervenir de forma temprana en los casos de alto riesgo, realizar seguimiento y acciones preventivas en los casos de riesgo medio, y mantener la estrategia actual en los casos de bajo riesgo. Con este enfoque nos permitiria optimizar costos, reducir decisiones reactivas y mejorar los resultados operativos, y asi la analítica predictiva nos da ese soporte clave en la toma de decisiones del negocio.
set.seed(260126)
experimento = data.frame(
Probabilidad = PXGB,
Clase_Real = y_test
)
# Asignación aleatoria 50% Control / 50% Tratamiento
experimento$Grupo = sample(
c("Control", "Tratamiento"),
size = nrow(experimento),
replace = TRUE
)
experimento <- experimento %>%
mutate(
Segmento = case_when(
Probabilidad > 0.80 ~ "Alto",
Probabilidad >= 0.50 ~ "Medio",
TRUE ~ "Bajo"
)
)
set.seed(260126)
experimento <- experimento %>%
mutate(
Evento_Simulado = case_when(
Grupo == "Tratamiento" & Segmento == "Alto" ~
rbinom(n(), 1, Probabilidad * 0.7),
TRUE ~ Clase_Real
)
)
alto_riesgo <- experimento %>%
filter(Segmento == "Alto")
resultado_alto <- alto_riesgo %>%
group_by(Grupo) %>%
summarise(
Tasa_Evento = mean(Evento_Simulado),
Casos = n()
)
resultado_alto
## # A tibble: 2 × 3
## Grupo Tasa_Evento Casos
## <chr> <dbl> <int>
## 1 Control 0.992 487
## 2 Tratamiento 0.661 504
prop.test(
x = c(
sum(alto_riesgo$Evento_Simulado[alto_riesgo$Grupo == "Control"]),
sum(alto_riesgo$Evento_Simulado[alto_riesgo$Grupo == "Tratamiento"])
),
n = c(
sum(alto_riesgo$Grupo == "Control"),
sum(alto_riesgo$Grupo == "Tratamiento")
)
)
##
## 2-sample test for equality of proportions with continuity correction
##
## data: c(sum(alto_riesgo$Evento_Simulado[alto_riesgo$Grupo == "Control"]), sum(alto_riesgo$Evento_Simulado[alto_riesgo$Grupo == "Tratamiento"])) out of c(sum(alto_riesgo$Grupo == "Control"), sum(alto_riesgo$Grupo == "Tratamiento"))
## X-squared = 184.43, df = 1, p-value < 2.2e-16
## alternative hypothesis: two.sided
## 95 percent confidence interval:
## 0.2869479 0.3751964
## sample estimates:
## prop 1 prop 2
## 0.9917864 0.6607143
ggplot(resultado_alto, aes(x = Grupo, y = Tasa_Evento, fill = Grupo)) +
geom_col() +
labs(
title = "Impacto de la Intervención en Alto Riesgo",
y = "Tasa del Evento"
) +
theme_minimal()
Viendo este experimento se muestra que, en el segmento de empleados clasificados como de alto riesgo, la aplicación de una intervención reduce significativamente la tasa de deserción. Mientras que en el grupo de control la tasa de salida es cercana al 99%, en el grupo tratado esta se reduce a aproximadamente 66%. La prueba de igualdad de proporciones confirma que esta diferencia es estadísticamente significativa, con una reducción estimada entre 28% y 38%.
Lo que evidencia que el modelo no solo predice correctamente el riesgo de salida, sino que además permite implementar acciones efectivas para mitigar la deserción de empleados.
Los beneficios del modelo pueden medirse a través de la reducción de la tasa de rotación en el segmento de empleados de alto riesgo, comparando los resultados entre los grupos de control y tratamiento.
Adicionalmente, el modelo genera valor al permitir una asignación eficiente de los recursos, enfocando las intervenciones únicamente en los casos con mayor probabilidad de salida.
Presentación en power point.
Como siguientes pasos, se propone:
Implementar el modelo en un entorno productivo y automatizar su actualización periódica.
Incorporar nuevas variables de desempeño.
Monitorear el desempeño del modelo en el tiempo y recalibrarlo si es necesario.