Este informe presenta un análisis de la rotación de empleados en una organización, con el objetivo de estimar la probabilidad de rotación e identificar los factores que la determinan. Se trabaja con una base de datos que incluye variables demográficas, laborales y de percepción (por ejemplo: edad, ingreso, antigüedad, horas extra, satisfacción laboral y equilibrio trabajo–vida). Sobre esta información se realizan procesos de limpieza, análisis univariado y bivariado, y la estimación de un modelo de regresión logística. El desempeño del modelo se evalúa con ROC y AUC y se proponen umbrales de decisión para priorizar intervenciones de retención sobre casos con mayor riesgo.
Para la preparación y limpieza de la base de datos hacemos una inspección inicial de la estructura del dataset.
# Tabla con estructura de los datos
Estructura <- data.frame(
Variable = names(rotacion),
Tipo = sapply(rotacion, function(x) class(x)[1]),
Ejemplo = sapply(rotacion, function(x) paste(utils::head(x, 3), collapse = ", "))
)
knitr::kable(Estructura, caption = "Estructura del dataset original")| Variable | Tipo | Ejemplo | |
|---|---|---|---|
| Rotación | Rotación | character | Si, No, Si |
| Edad | Edad | numeric | 41, 49, 37 |
| Viaje de Negocios | Viaje de Negocios | character | Raramente, Frecuentemente, Raramente |
| Departamento | Departamento | character | Ventas, IyD, IyD |
| Distancia_Casa | Distancia_Casa | numeric | 1, 8, 2 |
| Educación | Educación | numeric | 2, 1, 2 |
| Campo_Educación | Campo_Educación | character | Ciencias, Ciencias, Otra |
| Satisfacción_Ambiental | Satisfacción_Ambiental | numeric | 2, 3, 4 |
| Genero | Genero | character | F, M, M |
| Cargo | Cargo | character | Ejecutivo_Ventas, Investigador_Cientifico, Tecnico_Laboratorio |
| Satisfación_Laboral | Satisfación_Laboral | numeric | 4, 2, 3 |
| Estado_Civil | Estado_Civil | character | Soltero, Casado, Soltero |
| Ingreso_Mensual | Ingreso_Mensual | numeric | 5993, 5130, 2090 |
| Trabajos_Anteriores | Trabajos_Anteriores | numeric | 8, 1, 6 |
| Horas_Extra | Horas_Extra | character | Si, No, Si |
| Porcentaje_aumento_salarial | Porcentaje_aumento_salarial | numeric | 11, 23, 15 |
| Rendimiento_Laboral | Rendimiento_Laboral | numeric | 3, 4, 3 |
| Años_Experiencia | Años_Experiencia | numeric | 8, 10, 7 |
| Capacitaciones | Capacitaciones | numeric | 0, 3, 3 |
| Equilibrio_Trabajo_Vida | Equilibrio_Trabajo_Vida | numeric | 1, 3, 3 |
| Antigüedad | Antigüedad | numeric | 6, 10, 0 |
| Antigüedad_Cargo | Antigüedad_Cargo | numeric | 4, 7, 0 |
| Años_ultima_promoción | Años_ultima_promoción | numeric | 0, 1, 0 |
| Años_acargo_con_mismo_jefe | Años_acargo_con_mismo_jefe | numeric | 5, 7, 0 |
Se realiza una tipificación de las variables del dataset, es decir, se convierte cada columna al tipo de dato adecuado (numéricas, categóricas nominales, categóricas ordinales).
# Variables por tipo
ord_vars <- c("Satisfacción_Ambiental","Satisfación_Laboral",
"Equilibrio_Trabajo_Vida","Rendimiento_Laboral","Educación")
cat_vars <- c("Viaje de Negocios","Departamento","Campo_Educación",
"Genero","Cargo","Estado_Civil","Horas_Extra")
num_vars <- c("Ingreso_Mensual","Distancia_Casa","Edad","Años_Experiencia",
"Antigüedad","Antigüedad_Cargo","Años_acargo_con_mismo_jefe",
"Porcentaje_aumento_salarial","Capacitaciones",
"Trabajos_Anteriores","Años_ultima_promoción")
# Tipificatr las variables
rotacion <- rotacion %>%
mutate(
Rotación = factor(ifelse(Rotación == "Si", 1, 0),
levels = c(0,1)),
# nominales
across(all_of(intersect(cat_vars, names(.))), ~factor(.)),
# ordinales
across(all_of(intersect(ord_vars, names(.))),
~factor(., levels = sort(unique(.)), ordered = TRUE)),
# numéricas
across(all_of(intersect(num_vars, names(.))), ~as.numeric(.))
)
# Tabla con estructura de los datos
Estructura2 <- data.frame(
Variable = names(rotacion),
Tipo = sapply(rotacion, function(x) class(x)[1]),
Ejemplo = sapply(rotacion, function(x) paste(utils::head(x, 3), collapse = ", "))
)
knitr::kable(Estructura2, caption = "Estructura del dataset tipificado")| Variable | Tipo | Ejemplo | |
|---|---|---|---|
| Rotación | Rotación | factor | 1, 0, 1 |
| Edad | Edad | numeric | 41, 49, 37 |
| Viaje de Negocios | Viaje de Negocios | factor | Raramente, Frecuentemente, Raramente |
| Departamento | Departamento | factor | Ventas, IyD, IyD |
| Distancia_Casa | Distancia_Casa | numeric | 1, 8, 2 |
| Educación | Educación | ordered | 2, 1, 2 |
| Campo_Educación | Campo_Educación | factor | Ciencias, Ciencias, Otra |
| Satisfacción_Ambiental | Satisfacción_Ambiental | ordered | 2, 3, 4 |
| Genero | Genero | factor | F, M, M |
| Cargo | Cargo | factor | Ejecutivo_Ventas, Investigador_Cientifico, Tecnico_Laboratorio |
| Satisfación_Laboral | Satisfación_Laboral | ordered | 4, 2, 3 |
| Estado_Civil | Estado_Civil | factor | Soltero, Casado, Soltero |
| Ingreso_Mensual | Ingreso_Mensual | numeric | 5993, 5130, 2090 |
| Trabajos_Anteriores | Trabajos_Anteriores | numeric | 8, 1, 6 |
| Horas_Extra | Horas_Extra | factor | Si, No, Si |
| Porcentaje_aumento_salarial | Porcentaje_aumento_salarial | numeric | 11, 23, 15 |
| Rendimiento_Laboral | Rendimiento_Laboral | ordered | 3, 4, 3 |
| Años_Experiencia | Años_Experiencia | numeric | 8, 10, 7 |
| Capacitaciones | Capacitaciones | numeric | 0, 3, 3 |
| Equilibrio_Trabajo_Vida | Equilibrio_Trabajo_Vida | ordered | 1, 3, 3 |
| Antigüedad | Antigüedad | numeric | 6, 10, 0 |
| Antigüedad_Cargo | Antigüedad_Cargo | numeric | 4, 7, 0 |
| Años_ultima_promoción | Años_ultima_promoción | numeric | 0, 1, 0 |
| Años_acargo_con_mismo_jefe | Años_acargo_con_mismo_jefe | numeric | 5, 7, 0 |
Seguido, verificamos la existencia de duplicados, faltantes y outliers en el dataset:
# Identificar los duplicados
num_duplicados <- sum(duplicated(rotacion))
cat("Número de duplicados:", num_duplicados)## Número de duplicados: 0
# Conteo y % de NAs por columna
na_tbl <- rotacion %>%
dplyr::summarise(dplyr::across(dplyr::everything(), ~ sum(is.na(.)))) %>%
tidyr::pivot_longer(dplyr::everything(), names_to = "Variable", values_to = "NA_n") %>%
dplyr::mutate(Total = nrow(rotacion), NA_pct = round(100 * NA_n / Total, 2)) %>%
dplyr::arrange(dplyr::desc(NA_pct))
knitr::kable(na_tbl, caption = "Valores faltantes por variable")| Variable | NA_n | Total | NA_pct |
|---|---|---|---|
| Rotación | 0 | 1470 | 0 |
| Edad | 0 | 1470 | 0 |
| Viaje de Negocios | 0 | 1470 | 0 |
| Departamento | 0 | 1470 | 0 |
| Distancia_Casa | 0 | 1470 | 0 |
| Educación | 0 | 1470 | 0 |
| Campo_Educación | 0 | 1470 | 0 |
| Satisfacción_Ambiental | 0 | 1470 | 0 |
| Genero | 0 | 1470 | 0 |
| Cargo | 0 | 1470 | 0 |
| Satisfación_Laboral | 0 | 1470 | 0 |
| Estado_Civil | 0 | 1470 | 0 |
| Ingreso_Mensual | 0 | 1470 | 0 |
| Trabajos_Anteriores | 0 | 1470 | 0 |
| Horas_Extra | 0 | 1470 | 0 |
| Porcentaje_aumento_salarial | 0 | 1470 | 0 |
| Rendimiento_Laboral | 0 | 1470 | 0 |
| Años_Experiencia | 0 | 1470 | 0 |
| Capacitaciones | 0 | 1470 | 0 |
| Equilibrio_Trabajo_Vida | 0 | 1470 | 0 |
| Antigüedad | 0 | 1470 | 0 |
| Antigüedad_Cargo | 0 | 1470 | 0 |
| Años_ultima_promoción | 0 | 1470 | 0 |
| Años_acargo_con_mismo_jefe | 0 | 1470 | 0 |
# Función para detectar outliers
detectar_outliers_tabla <- function(df, vars) {
resumen <- data.frame(Variable = character(), N_outliers = integer())
for (var in vars) {
datos <- df[[var]]
Q1 <- quantile(datos, 0.25, na.rm = TRUE)
Q3 <- quantile(datos, 0.75, na.rm = TRUE)
IQR_val <- Q3 - Q1
lim_inf <- Q1 - 1.5 * IQR_val
lim_sup <- Q3 + 1.5 * IQR_val
outliers <- sum(datos < lim_inf | datos > lim_sup, na.rm = TRUE)
resumen <- rbind(resumen, data.frame(Variable = var, N_outliers = outliers))
}
resumen[order(-resumen$N_outliers), ]
}
# Mostrar tabla
tabla_outliers <- detectar_outliers_tabla(rotacion, num_vars)
knitr::kable(tabla_outliers, caption = "Conteo de outliers")| Variable | N_outliers | |
|---|---|---|
| 9 | Capacitaciones | 238 |
| 1 | Ingreso_Mensual | 114 |
| 11 | Años_ultima_promoción | 107 |
| 5 | Antigüedad | 104 |
| 4 | Años_Experiencia | 63 |
| 10 | Trabajos_Anteriores | 52 |
| 6 | Antigüedad_Cargo | 21 |
| 7 | Años_acargo_con_mismo_jefe | 14 |
| 2 | Distancia_Casa | 0 |
| 3 | Edad | 0 |
| 8 | Porcentaje_aumento_salarial | 0 |
# Diagramas de caja interactivos de variables numéricas
# Variables extra para mostrar en el hover
hover_vars <- c("Cargo", "Edad", "Antigüedad")
# Crear gráfico vacío
fig <- plot_ly()
# Boxplot + puntos
for (v in num_vars) {
# Texto de hover para cada fila
hover_text <- apply(rotacion[hover_vars], 1, function(row){
paste(names(row), row, sep=": ", collapse="<br>")
})
# Boxplot
fig <- fig %>%
add_boxplot(y = rotacion[[v]], name = v,
boxpoints = "suspectedoutliers",
marker = list(opacity = 0),
line = list(color = 'grey'),
visible = FALSE,
showlegend = FALSE)
# Scatter con todos los puntos
fig <- fig %>%
add_trace(
type = "scatter",
mode = "markers",
y = rotacion[[v]],
x = rep(v, nrow(rotacion)),
text = hover_text,
hoverinfo = "text+y",
marker = list(size = 6, color = 'coral', opacity = 0.2),
name = paste("Puntos", v),
visible = FALSE,
showlegend = FALSE
)
}
fig <- fig %>% style(visible = TRUE, traces = c(1,2))
# Alternar visibilidad
buttons <- lapply(seq_along(num_vars), function(i){
vis <- rep(FALSE, length(num_vars)*2)
vis[(2*i-1):(2*i)] <- TRUE
list(
method = "restyle",
args = list("visible", vis),
label = num_vars[i]
)
})
# Layout final
fig <- fig %>%
layout(
title = list(
text = paste("Boxplot:", num_vars[1]),
x = 0.5),
yaxis = list(title = ""),
xaxis = list(title = ""),
updatemenus = list(list(
type = "dropdown",
direction = "down",
x = 0.02, y = 1.15,
showactive = TRUE,
buttons = buttons
))
)
figAl analizar los diagramas de caja de las variables con presencia de outliers, se evidencia que las observaciones extremas en Años_ultima_promoción, Antigüedad, Años_Experiencia y Antigüedad_Cargo se asocian principalmente con empleados de mayor edad, lo cual resulta coherente y sugiere que corresponden a trayectorias laborales prolongadas, por lo que no deben considerarse errores. En el caso de Capacitaciones, los valores atípicos corresponden a un máximo de 6 y a valores de 0, ambos plausibles dentro de este contexto, dado que algunos empleados pueden no haber recibido formación adicional mientras que otros pueden haber participado en multiples capacitaciones. Finalmente, los ingresos mensuales atípicamente altos se relacionan con cargos de gerencia y puestos directivos, lo cual es consistente con niveles salariales superiores al promedio. En consecuencia, los outliers identificados se interpretan como datos posibles y representativos de la realidad de la organización, por lo que no se aplican tratamientos de eliminación o transformación en esta etapa del análisis.
Ingreso_Mensual: El salario es un factor clave de permanencia. Quienes tienen ingresos más bajos son más propensos a buscar nuevas y mejores oportunidades laborales en comparación con aquellos con salarios elevados.
Antigüedad: El tiempo en la organización refleja la experiencia y estabilidad del empleado. Una antigüedad baja se asocia con una mayor probabilidad de rotación, mientras que una alta suele indicar una tendencia a permanecer en el cargo.
Distancia_Casa: La proximidad del lugar de trabajo al hogar del empleado influye en su decisión de permanencia. Una distancia mayor implica costos de tiempo y transporte más elevados, lo que puede aumentar el desgaste y la probabilidad de querer buscar cargos que permitan considerar opciones de trabajo hidridas o teletrabajo.Por el contrario, los empleados que viven más cerca suelen tener mayor facilidad para conciliar la vida personal y laboral, lo que fomenta su estabilidad en la organización.
Satisfacción_Laboral: El nivel de satisfacción con el trabajo influye directamente en la rotación. A menor satisfacción, mayor es la probabilidad de que el empleado rote en la organización.
Horas_Extra: Trabajar fuera del horario regular puede generar desgaste fisico y emocional del empleado, afectando su equilibrio vida/trabajo por lo que los empleados que si las realizan tienen mayor probabilidad de rotacion que los que no realizan horas extra.
Equilibrio_Trabajo_Vida: El balance entre la vida personal y laboral es crucial para la retención. Los empleados que perciben un buen equilibrio muestran mayor compromiso y menor intención de rotación. Por el contrario, una puntuación baja en este indicador se asocia con mayores niveles de estrés, insatisfacción y por consiguiente, una mayor probabilidad de rotación.
# Resumen estadístico de variables numéricas
resumen_num <- purrr::map_dfr(num_vars, ~{
x <- rotacion[[.x]]
tibble::tibble(
Variable = .x,
n = sum(!is.na(x)),
Media = mean(x, na.rm = TRUE),
DE = sd(x, na.rm = TRUE),
Mín = min(x, na.rm = TRUE),
Q1 = quantile(x, 0.25, na.rm = TRUE),
Mediana = median(x, na.rm = TRUE),
Q3 = quantile(x, 0.75, na.rm = TRUE),
Máx = max(x, na.rm = TRUE)
)
})
knitr::kable(
resumen_num,
digits = c(0,0,2,2,2,2,2,2,2),
caption = "Resumen estadístico de variables numéricas"
)| Variable | n | Media | DE | Mín | Q1 | Mediana | Q3 | Máx |
|---|---|---|---|---|---|---|---|---|
| Ingreso_Mensual | 1470 | 6502.93 | 4707.96 | 1009 | 2911 | 4919 | 8379 | 19999 |
| Distancia_Casa | 1470 | 9.19 | 8.11 | 1 | 2 | 7 | 14 | 29 |
| Edad | 1470 | 36.92 | 9.14 | 18 | 30 | 36 | 43 | 60 |
| Años_Experiencia | 1470 | 11.28 | 7.78 | 0 | 6 | 10 | 15 | 40 |
| Antigüedad | 1470 | 7.01 | 6.13 | 0 | 3 | 5 | 9 | 40 |
| Antigüedad_Cargo | 1470 | 4.23 | 3.62 | 0 | 2 | 3 | 7 | 18 |
| Años_acargo_con_mismo_jefe | 1470 | 4.12 | 3.57 | 0 | 2 | 3 | 7 | 17 |
| Porcentaje_aumento_salarial | 1470 | 15.21 | 3.66 | 11 | 12 | 14 | 18 | 25 |
| Capacitaciones | 1470 | 2.80 | 1.29 | 0 | 2 | 3 | 3 | 6 |
| Trabajos_Anteriores | 1470 | 2.69 | 2.50 | 0 | 1 | 2 | 4 | 9 |
| Años_ultima_promoción | 1470 | 2.19 | 3.22 | 0 | 0 | 1 | 3 | 15 |
# Selección de variables numéricas principales
num_sel <- rotacion[, c("Ingreso_Mensual","Edad","Antigüedad", "Distancia_Casa")]
melted <- melt(num_sel)
ggplot(melted, aes(x = value)) +
geom_histogram(bins = 30, fill = "lightblue", color = "white") +
facet_wrap(~variable, scales = "free") +
theme_minimal() +
labs(title = "Distribución de variables numéricas seleccionadas") +
theme(
plot.title = element_text(hjust = 0.5)
)El análisis descriptivo de las variables numéricas muestra que los empleados presentan un ingreso mensual promedio cercano a 6.500, aunque con una gran dispersión y clara asimetría hacia la izquierda (evidenciada en el histograma), con una elevada concentración de salarios bajos y medios y unos pocos casos en niveles muy altos, correspondientes a cargos directivos. La edad promedio es de 37 años, con una distribución aproximadamente simétrica entre los 30 y 43, lo que refleja una plantilla en su mayoría adulta joven. La antigüedad en la organización presenta una fuerte concentración en valores bajos (mediana de 5 años), confirmando que la mayor parte de los empleados lleva poco tiempo en la empresa, aunque existen algunos casos de larga permanencia. Los años desde la última promoción y el número de trabajos anteriores evidencian que la mayoría de los empleados ha tenido poca movilidad laboral, mientras que el porcentaje de aumento salarial presenta poca variabilidad mostrando una politica uniforme de incrementos. Finalmente, la distancia desde la vivienda al lugar de trabajo también evidencia asimetría positiva: la mayoría de los empleados vive a menos de 10 km, mientras que solo un grupo reducido recorre distancias mayores a 20 km.
# Funcion para calcular frecuencias
freq_tabla_larga <- function(data, vars){
do.call(rbind, lapply(vars, function(v){
x <- as.character(data[[v]])
tb <- as.data.frame(table(x), stringsAsFactors = FALSE)
names(tb) <- c("Categoría", "Frecuencia")
tb$Variable <- v
tb$`%` <- round(100 * tb$Frecuencia / sum(tb$Frecuencia), 1)
tb[, c("Variable", "Categoría", "Frecuencia", "%")]
}))
}
# Construir tabla para ordinales + nominales
tabla_cat_ord <- freq_tabla_larga(rotacion, c(ord_vars, cat_vars))
tabla <- tabla_cat_ord %>%
select(Variable, Categoría, Frecuencia, `%`) %>%
mutate(
Variable = as.character(Variable),
Categoria_num = suppressWarnings(as.numeric(as.character(Categoría)))
)
# Ordenar por variable
tabla_ord <- tabla %>%
split(.$Variable) %>%
imap_dfr(function(df, var){
if (var %in% ord_vars) {
df %>% arrange(Categoria_num)
} else {
df %>% arrange(desc(Frecuencia), Categoría) # más frecuentes primero
}
}) %>%
select(-Categoria_num) %>%
mutate(
Variable = factor(Variable, levels = c(ord_vars, cat_vars))
) %>%
arrange(Variable)
# Mostrar tabla
kable(
tabla_ord,
caption = "Frecuencias absolutas y relativas (%) por variable",
col.names = c("Variable","Categoría","Frecuencia","%")
)| Variable | Categoría | Frecuencia | % |
|---|---|---|---|
| Satisfacción_Ambiental | 1 | 284 | 19.3 |
| Satisfacción_Ambiental | 2 | 287 | 19.5 |
| Satisfacción_Ambiental | 3 | 453 | 30.8 |
| Satisfacción_Ambiental | 4 | 446 | 30.3 |
| Satisfación_Laboral | 1 | 289 | 19.7 |
| Satisfación_Laboral | 2 | 280 | 19.0 |
| Satisfación_Laboral | 3 | 442 | 30.1 |
| Satisfación_Laboral | 4 | 459 | 31.2 |
| Equilibrio_Trabajo_Vida | 1 | 80 | 5.4 |
| Equilibrio_Trabajo_Vida | 2 | 344 | 23.4 |
| Equilibrio_Trabajo_Vida | 3 | 893 | 60.7 |
| Equilibrio_Trabajo_Vida | 4 | 153 | 10.4 |
| Rendimiento_Laboral | 3 | 1244 | 84.6 |
| Rendimiento_Laboral | 4 | 226 | 15.4 |
| Educación | 1 | 170 | 11.6 |
| Educación | 2 | 282 | 19.2 |
| Educación | 3 | 572 | 38.9 |
| Educación | 4 | 398 | 27.1 |
| Educación | 5 | 48 | 3.3 |
| Viaje de Negocios | Raramente | 1043 | 71.0 |
| Viaje de Negocios | Frecuentemente | 277 | 18.8 |
| Viaje de Negocios | No_Viaja | 150 | 10.2 |
| Departamento | IyD | 961 | 65.4 |
| Departamento | Ventas | 446 | 30.3 |
| Departamento | RH | 63 | 4.3 |
| Campo_Educación | Ciencias | 606 | 41.2 |
| Campo_Educación | Salud | 464 | 31.6 |
| Campo_Educación | Mercadeo | 159 | 10.8 |
| Campo_Educación | Tecnicos | 132 | 9.0 |
| Campo_Educación | Otra | 82 | 5.6 |
| Campo_Educación | Humanidades | 27 | 1.8 |
| Genero | M | 882 | 60.0 |
| Genero | F | 588 | 40.0 |
| Cargo | Ejecutivo_Ventas | 326 | 22.2 |
| Cargo | Investigador_Cientifico | 292 | 19.9 |
| Cargo | Tecnico_Laboratorio | 259 | 17.6 |
| Cargo | Director_Manofactura | 145 | 9.9 |
| Cargo | Representante_Salud | 131 | 8.9 |
| Cargo | Gerente | 102 | 6.9 |
| Cargo | Representante_Ventas | 83 | 5.6 |
| Cargo | Director_Investigación | 80 | 5.4 |
| Cargo | Recursos_Humanos | 52 | 3.5 |
| Estado_Civil | Casado | 673 | 45.8 |
| Estado_Civil | Soltero | 470 | 32.0 |
| Estado_Civil | Divorciado | 327 | 22.2 |
| Horas_Extra | No | 1054 | 71.7 |
| Horas_Extra | Si | 416 | 28.3 |
El análisis de frecuencias revela un perfil claro de la plantilla. En cuanto a la percepcion de los empleados, la mayoría reporta altos niveles de satisfacción laboral y ambiental. Sin embargo, el equilibrio trabajo-vida prevalece la categoría intermedia, con una mayoría significativa del 60,7%.
Respecto a las características profesionales, el rendimiento laboral está altamente concentrado en la categoría 3 (84,6%), mientras que en educación predominan los niveles intermedios/altos (3 y 4).
El análisis de las variables nominales refleja condiciones laborales específicas: la gran mayoría de los empleados viaja raramente por negocios (71%) y no realiza horas extra (71,7%), lo que indica que la sobrecarga laboral no es un problema generalizado en la organización.
Finalmente, la plantilla se caracteriza por una fuerte presencia en el departamento de Investigación y Desarrollo (65,4%) y en el área de Ciencias (41,2%), con predominio del género masculino (60%) y de empleados casados (45,8%).
# Grafico distribución de la variable rotación
ggplot(rotacion, aes(x = Rotación)) +
geom_bar(fill = "lightblue") +
geom_text(stat = "count", aes(label = ..count..), vjust = -0.3) +
labs(
title = "Distribución de la variable Rotación",
x = "Rotación (0 = No, 1 = Sí)",
y = "Frecuencia"
) +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5))Finalmente, se observa que la variable Rotación presenta un marcado desbalance de clases, ya que aproximadamente el 83% de los registros corresponden a empleados que no rotan, mientras que solo una proporción reducida refleja casos de rotación.
# Análisis bivariado para variables numéricas
res_num <- lapply(num_vars, function(v){
f <- as.formula(paste("Rotación ~", v))
m <- glm(f, data = rotacion, family = binomial)
broom::tidy(m)[2, c("term","estimate","p.value")]
}) %>% bind_rows()
# Mostrar tabla
knitr::kable(res_num,
caption = "Regresiones logísticas bivariadas - Variables numéricas",
col.names = c("Variable","Coeficiente","p-valor"))| Variable | Coeficiente | p-valor |
|---|---|---|
| Ingreso_Mensual | -0.0001271 | 0.0000000 |
| Distancia_Casa | 0.0247101 | 0.0029517 |
| Edad | -0.0522544 | 0.0000000 |
| Años_Experiencia | -0.0777307 | 0.0000000 |
| Antigüedad | -0.0807589 | 0.0000004 |
| Antigüedad_Cargo | -0.1462777 | 0.0000000 |
| Años_acargo_con_mismo_jefe | -0.1413767 | 0.0000000 |
| Porcentaje_aumento_salarial | -0.0101238 | 0.6053630 |
| Capacitaciones | -0.1299500 | 0.0228531 |
| Trabajos_Anteriores | 0.0456464 | 0.0959595 |
| Años_ultima_promoción | -0.0297867 | 0.2064513 |
Los coeficientes negativos obtenidos indican que a medida que aumenta el valor de la variable, disminuye la probabilidad de rotación. Entre estas variables se destacan aquellas relacionadas con una mayor estabilidad y permanencia en la organización, como la Edad, los años de experiencia, los años en el cargo con el mismo jefe, la antigüedad en la empresa y la antigüedad en el cargo. Tambien, aspectos económicos como el ingreso mensual y formativos como el número de capacitaciones muestran una relación directamente proporcional con la probabilidad de permanencia de los empleados, mientras que, aspectos como una mayor distancia hasta la casa influyen positivamente en la probabilidad de rotación.
Finalmente, variables como el número de Trabajos_Anteriores, el Porcentaje_aumento_salarial y los Años_ultima_promoción no presentan efectos significativos (p > 0.05), lo que indica que, de manera aislada, no se relacionan de forma clara con la rotación.
En línea con los resultados, las tres hipótesis planteadas inicialmente encuentran respaldo:
El Ingreso_Mensual confirmó ser un factor clave de permanencia.
La Antigüedad demostró asociarse con una menor probabilidad de rotación.
La Distancia_Casa mostró que una mayor proximidad al trabajo reduce la probabilidad de rotación, mientras que una distancia mayor la incrementa.
# Analisis bivariado para variables categóricas
res_cat <- data.frame()
for (v in cat_vars) {
m <- glm(rotacion[[1]] ~ rotacion[[v]], family = binomial)
result <- broom::tidy(m)[-1, c("term", "estimate", "p.value")]
result$variable <- v
res_cat <- rbind(res_cat, result)
}
# Limpiar y presentar resultados
res_cat_clean <- res_cat %>%
mutate(
categoria = gsub("rotacion\\[\\[v\\]\\]", "", term),
categoria = trimws(categoria)
) %>%
select(variable, categoria, estimate, p.value)
# Mostrar tabla
knitr::kable(res_cat_clean,
caption = "Regresiones logisticas bivariadas - Variables categoricas nominales",
col.names = c("Variable", "Categoria", "Coeficiente", "p-valor"),
digits = c(0, 0, 3, 4),
align = c("l", "l", "r", "r"))| Variable | Categoria | Coeficiente | p-valor |
|---|---|---|---|
| Viaje de Negocios | No_Viaja | -1.339 | 0.0001 |
| Viaje de Negocios | Raramente | -0.635 | 0.0001 |
| Departamento | RH | 0.382 | 0.2533 |
| Departamento | Ventas | 0.481 | 0.0013 |
| Campo_Educación | Humanidades | 0.710 | 0.1180 |
| Campo_Educación | Mercadeo | 0.494 | 0.0267 |
| Campo_Educación | Otra | -0.105 | 0.7592 |
| Campo_Educación | Salud | -0.091 | 0.6067 |
| Campo_Educación | Tecnicos | 0.620 | 0.0079 |
| Genero | M | 0.166 | 0.2591 |
| Cargo | Director_Manofactura | 1.061 | 0.1780 |
| Cargo | Ejecutivo_Ventas | 2.112 | 0.0039 |
| Cargo | Gerente | 0.698 | 0.4116 |
| Cargo | Investigador_Cientifico | 2.012 | 0.0061 |
| Cargo | Recursos_Humanos | 2.460 | 0.0018 |
| Cargo | Representante_Salud | 1.057 | 0.1838 |
| Cargo | Representante_Ventas | 3.248 | 0.0000 |
| Cargo | Tecnico_Laboratorio | 2.507 | 0.0006 |
| Estado_Civil | Divorciado | -0.239 | 0.2709 |
| Estado_Civil | Soltero | 0.877 | 0.0000 |
| Horas_Extra | Si | 1.327 | 0.0000 |
# Análisis bivariado para variables ordinales
res_ord <- data.frame()
for (v in ord_vars) {
# Convertir a factor regular (no ordenado) para evitar contrastes polinomiales
temp_var <- factor(rotacion[[v]], ordered = FALSE)
m <- glm(rotacion$Rotación ~ temp_var, family = binomial)
result <- broom::tidy(m)[-1, c("term", "estimate", "p.value")]
result$variable <- v
res_ord <- rbind(res_ord, result)
}
# Limpiar y presentar resultados ordinales
res_ord_clean <- res_ord %>%
# Limpiar el nombre de las categorías
mutate(
categoria = gsub("temp_var", "", term),
categoria = trimws(categoria),
# Crear etiquetas descriptivas
nivel = as.numeric(categoria),
categoria_desc = case_when(
!is.na(nivel) ~ paste("Nivel", nivel),
TRUE ~ categoria
)
) %>%
select(variable, categoria = categoria_desc, estimate, p.value)
# Mostrar tabla
knitr::kable(res_ord_clean,
caption = "Regresiones logísticas bivariadas - Variable categoricas ordinales",
col.names = c("Variable", "Categoría", "Coeficiente", "p-valor"),
digits = c(0, 0, 3, 4),
align = c("l", "l", "r", "r"))| Variable | Categoría | Coeficiente | p-valor |
|---|---|---|---|
| Satisfacción_Ambiental | Nivel 2 | -0.656 | 0.0022 |
| Satisfacción_Ambiental | Nivel 3 | -0.762 | 0.0001 |
| Satisfacción_Ambiental | Nivel 4 | -0.782 | 0.0001 |
| Satisfación_Laboral | Nivel 2 | -0.409 | 0.0555 |
| Satisfación_Laboral | Nivel 3 | -0.403 | 0.0339 |
| Satisfación_Laboral | Nivel 4 | -0.840 | 0.0000 |
| Equilibrio_Trabajo_Vida | Nivel 2 | -0.807 | 0.0041 |
| Equilibrio_Trabajo_Vida | Nivel 3 | -1.009 | 0.0001 |
| Equilibrio_Trabajo_Vida | Nivel 4 | -0.752 | 0.0192 |
| Rendimiento_Laboral | Nivel 4 | 0.022 | 0.9118 |
| Educación | Nivel 2 | -0.188 | 0.4665 |
| Educación | Nivel 3 | -0.063 | 0.7800 |
| Educación | Nivel 4 | -0.268 | 0.2724 |
| Educación | Nivel 5 | -0.651 | 0.2038 |
El análisis bivariado revela patrones significativos en la rotación de personal. En variables categóricas nominales, destacan como factores de riesgo de rotación los empleados solteros, quienes realizan horas extra y ciertos cargos como Representante de Ventas, Tecnicos de Laboratorio y Recursos Humanos. Como factor que influye positivamente en la permanencia sobresalen los empleados que no viajan por negocios (coef. = -1.339, p < 0.001).
En variables ordinales, se observa una relación consistente: mayores niveles de satisfacción ambiental, laboral y equilibrio trabajo-vida se asocian con menor probabilidad de rotación, mostrando en casi todos los casos coeficientes negativos significativos.
En línea con los resultados, las tres hipótesis planteadas inicialmente encuentran respaldo:
La Satisfacción_Laboral confirmó que a mayor nivel de satisfacción, menor probabilidad de rotación.
Las Horas_Extra demostraron que los empleados que las realizan tienen significativamente mayor probabilidad de rotación.
El Equilibrio_Trabajo_Vida mostró que mejores niveles de equilibrio se asocian con una mayor probabilidad de permanencia en el cargo.
# Ajustar nombres de variables para evitar errores
names(rotacion) <- c("Rotacion", "Edad", "Viaje_de_Negocios", "Departamento", "Distancia_Casa",
"Educacion", "Campo_Educacion", "Satisfaccion_Ambiental", "Genero", "Cargo",
"Satisfaccion_Laboral", "Estado_Civil", "Ingreso_Mensual", "Trabajos_Anteriores",
"Horas_Extra", "Porcentaje_aumento_salarial", "Rendimiento_Laboral",
"Años_Experiencia", "Capacitaciones", "Equilibrio_Trabajo_Vida",
"Antiguedad", "Antiguedad_Cargo", "Años_ultima_promocion",
"Años_acargo_con_mismo_jefe")# Division Train/Test
set.seed(123)
id_entrenamiento <- sample(seq_len(nrow(rotacion)), size = 0.7 * nrow(rotacion))
datos_entrenamiento <- rotacion[id_entrenamiento, ]
datos_prueba <- rotacion[-id_entrenamiento, ]
# Convertir variables ordinales a factor regular
datos_entrenamiento <- datos_entrenamiento %>%
dplyr::mutate(
Ingreso_k = Ingreso_Mensual/1000,
Satisfaccion_Laboral = factor(Satisfaccion_Laboral,
levels = sort(unique(Satisfaccion_Laboral)),
ordered = FALSE),
Equilibrio_Trabajo_Vida = factor(Equilibrio_Trabajo_Vida,
levels = sort(unique(Equilibrio_Trabajo_Vida)),
ordered = FALSE)
)
datos_prueba <- datos_prueba %>%
dplyr::mutate(
Ingreso_k = Ingreso_Mensual/1000,
Satisfaccion_Laboral = factor(Satisfaccion_Laboral,
levels = levels(datos_entrenamiento$Satisfaccion_Laboral),
ordered = FALSE),
Equilibrio_Trabajo_Vida = factor(Equilibrio_Trabajo_Vida,
levels = levels(datos_entrenamiento$Equilibrio_Trabajo_Vida),
ordered = FALSE)
)
# Ponderaciones por desbalance (más peso a la clase 1)
y_entrenamiento <- if (is.factor(datos_entrenamiento$Rotacion)) {
as.integer(as.character(datos_entrenamiento$Rotacion))
} else {
as.integer(datos_entrenamiento$Rotacion)
}
n_rotacion <- sum(y_entrenamiento == 1)
n_no_rotacion <- sum(y_entrenamiento == 0)
pesos <- ifelse(y_entrenamiento == 1, n_no_rotacion / n_rotacion, 1)
pesos <- pesos / mean(pesos)
# Ajuste del modelo con ponderación
modelo_final <- glm(Rotacion ~ Horas_Extra + Satisfaccion_Laboral + Equilibrio_Trabajo_Vida +
Ingreso_k + Distancia_Casa + Antiguedad,
data = datos_entrenamiento, family = binomial(), weights = pesos)
# Limpiar y presentar resultados
tabla_coeficientes_clean <- broom::tidy(modelo_final) %>%
dplyr::mutate(
# Limpiar nombres de términos
term_clean = gsub("Satisfaccion_Laboral", "Satisf_Lab_", term),
term_clean = gsub("Equilibrio_Trabajo_Vida", "Eq_Trab_Vida_", term_clean),
# Extraer números y crear etiquetas limpias
nivel = stringr::str_extract(term_clean, "[0-9]+$"),
variable_final = ifelse(!is.na(nivel),
paste0(gsub("_[0-9]+$", "", term_clean), " Nivel ", nivel),
term_clean),
# Calcular OR e IC95%
OR = exp(estimate),
IC95_L = exp(estimate - 1.96 * std.error),
IC95_U = exp(estimate + 1.96 * std.error)
) %>%
dplyr::select(Variable = variable_final, Coeficiente = estimate, OR, IC95_L, IC95_U, p.value)
# Mostrar tabla
knitr::kable(tabla_coeficientes_clean, digits = 2,
caption = "Modelo logistico multivariado con ponderacion")| Variable | Coeficiente | OR | IC95_L | IC95_U | p.value |
|---|---|---|---|---|---|
| (Intercept) | 1.62 | 5.04 | 2.68 | 9.46 | 0.00 |
| Horas_ExtraSi | 1.28 | 3.59 | 2.69 | 4.79 | 0.00 |
| Satisf_Lab Nivel 2 | -0.55 | 0.58 | 0.38 | 0.88 | 0.01 |
| Satisf_Lab Nivel 3 | -0.56 | 0.57 | 0.39 | 0.83 | 0.00 |
| Satisf_Lab Nivel 4 | -0.89 | 0.41 | 0.28 | 0.60 | 0.00 |
| Eq_Trab_Vida Nivel 2 | -0.96 | 0.38 | 0.21 | 0.70 | 0.00 |
| Eq_Trab_Vida Nivel 3 | -1.18 | 0.31 | 0.18 | 0.53 | 0.00 |
| Eq_Trab_Vida Nivel 4 | -0.65 | 0.52 | 0.27 | 0.99 | 0.05 |
| Ingreso_k | -0.14 | 0.87 | 0.84 | 0.91 | 0.00 |
| Distancia_Casa | 0.02 | 1.02 | 1.00 | 1.04 | 0.02 |
| Antiguedad | 0.00 | 1.00 | 0.97 | 1.02 | 0.78 |
El modelo multivariado muestra que hacer horas extra es el principal factor de riesgo: se asocia con un riesgo de rotación aproximadamente 3,59 veces mayor que en quienes no las realizan (p<0,001). En contraste, una mayor satisfacción laboral reduce las odds de forma progresiva y significativa (niveles 2–4: OR 0,57–0,41), y un mejor equilibrio trabajo–vida también protege (niveles 2–3: OR 0,38–0,31, aunque el nivel 4 no sigue la tendencia pero no es significativo, p=0,05). un mayor salario protege frente a la rotación pues por cada $1.000 adicionales, las odds de rotar bajan ~12,6% (OR=0,87; p<0,001). En cambio, vivir más lejos aumenta ligeramente el riesgo pues cada kilómetro extra se asocia con ~2,0% más en las odds de rotación (OR=1,02; p=0,02). Por ultimo, la antigüedad no muestra efecto significativo en la probabilidad de rotacion (p=0,78). En conjunto, los hallazgos confirman de manera general las hipótesis planteadas: mayor carga (horas extra) y mayor distancia aumentan la rotación; mejor salario, satisfacción y equilibrio disminuyen la rotación.
# Probabilidades y variable respuesta
p_test <- predict(modelo_final, newdata = datos_prueba, type = "response")
y_test <- as.numeric(as.character(datos_prueba$Rotacion))
# Calcular y mostrar AUC
roc_obj <- roc(y_test, p_test, quiet = TRUE)
auc_val <- auc(roc_obj)
ci_auc <- ci.auc(roc_obj)
cat(sprintf("AUC = %.3f (IC95%%: %.3f - %.3f)\n", auc_val, ci_auc[1], ci_auc[3]))## AUC = 0.730 (IC95%: 0.665 - 0.795)
# Gráfico
plot(roc_obj, main = sprintf("Curva ROC (AUC = %.3f)", auc_val))
abline(0, 1, lty = 2, col = "gray")La curva ROC muestra un desempeño aceptable: un AUC=0.730 indica que, en promedio, el modelo asigna una probabilidad mayor de rotación al empleado que realmente rota que al que no rota en 73.6% de los pares comparados. El IC95% [0.665–0.795] está claramente por encima de 0.5, por lo que el modelo discrimina mejor que el azar, aunque aún hay margen de mejora.
# Individuo hipotético
new_emp <- data.frame(
Horas_Extra = factor("Si", levels = levels(datos_entrenamiento$Horas_Extra)),
Satisfaccion_Laboral = factor("3", levels = levels(datos_entrenamiento$Satisfaccion_Laboral)),
Equilibrio_Trabajo_Vida = factor("2", levels = levels(datos_entrenamiento$Equilibrio_Trabajo_Vida)),
Ingreso_k = 3.5,
Distancia_Casa = 12,
Antiguedad = 2
)
# Probabilidad de rotación del individuo
p_new <- predict(modelo_final, newdata = new_emp, type = "response")
# Definir corte (umbral óptimo de Youden usando el set de prueba)
p_test <- predict(modelo_final, newdata = datos_prueba, type = "response")
y_test <- if (is.factor(datos_prueba$Rotacion)) {
as.integer(as.character(datos_prueba$Rotacion))
} else {
as.integer(datos_prueba$Rotacion)
}
roc_obj <- roc(y_test, p_test, quiet = TRUE)
thr <- coords(roc_obj, "best", ret = "threshold", best.method = "youden")
# Decisión de intervención
decision <- ifelse(p_new >= thr, "Intervenir", "No Intervenir")
res_pred <- data.frame(
`Prob.rotacion` = round(p_new, 3),
`Umbral.Youden` = round(thr, 3),
`AUC.test` = round(auc_val, 3),
`Decision` = decision
)
# Mostrar Tabla
knitr::kable(
res_pred,
caption = "Prediccion de rotación y decision de intervencion (individuo hipotetico)"
)| Prob.rotacion | threshold | AUC.test | threshold.1 |
|---|---|---|---|
| 0.756 | 0.569 | 0.73 | Intervenir |
Para el caso de este colaborador hipotético, al tener una probabilidad elevada de rotación (0.756) por encima del umbral establecido, se sugeriria intervenir al empleado para investigar los motivos de su insatisfacción laboral para implementar un plan de retención integral que aborde los factores clave identificados: redistribución de tareas para reducir horas extra, implementación de teletrabajo para mitigar la distancia al trabajo, programas de bienestar laboral que mejoren el equilibrio trabajo-vida, y una revisión de la estructura compensacional que considere ajustes salariales, todo ello complementado con oportunidades de capacitación y desarrollo profesional.
Para disminuir la rotación, la estrategia de intervencion a los empleados con probabilidades por encima del umbral definido debe atacar los factores con mayor impacto identificados por el modelo:
Horas extra: redistribuir turnos y cargas, incrementar personal en equipos críticos y fijar límites/alertas de horas para prevenir sobrecarga.
Satisfacción laboral: fortalecer liderazgo y clima laboral con reuniones de equipo frecuentes, capacitaciones e integraciones.
Equilibrio trabajo-vida: ampliar la flexiblibilidad en los horarios con opcion de teletrabajo/híbrido, horarios escalonados y una buena gestión de vacaciones.
Distancia a casa: ofrecer beneficios de movilidad (rutas o auxilios de transporte) o priorizar flexibilidad para quienes viven más lejos.
Ingresos: aplicar ajustes salariales selectivos o bonos de retención para roles y personas con altas responsabilidades, con revisiones periódicas de equidad interna.