# Paquetes requeridos (instalar una sola vez si hace falta):
# install.packages(c("devtools", "dplyr", "tidyr", "ggplot2", "patchwork",
# "kableExtra", "pROC", "car", "broom", "scales", "bookdown"))
# devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
library(paqueteMODELOS)
library(dplyr)
library(tidyr)
library(ggplot2)
library(patchwork)
library(kableExtra)
library(pROC)
library(car)
library(broom)
library(scales)
# Tema gráfico común para todo el informe
theme_set(theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold", size = 12),
panel.grid.minor = element_blank(),
legend.position = "bottom"))
col_rot <- c("No" = "#9aa5b1", "Si" = "#b23a48")
# Funciones auxiliares
tabla <- function(df, caption, digits = 2, ...) {
kbl(df, caption = caption, digits = digits,
format.args = list(big.mark = ".", decimal.mark = ","), ...) |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE, position = "center")
}
asimetria <- function(x) { x <- x[!is.na(x)]; mean((x - mean(x))^3) / sd(x)^3 }
n_atipicos <- function(x) {
q <- quantile(x, c(.25, .75)); r <- IQR(x)
sum(x < q[1] - 1.5 * r | x > q[2] + 1.5 * r)
}
fmt <- function(x, d = 2) format(round(x, d), nsmall = d, big.mark = ".", decimal.mark = ",")
pct <- function(x, d = 1) paste0(fmt(100 * x, d), " %")
pval <- function(p) ifelse(p < 0.001, "< 0,001", fmt(p, 3))
pvt <- function(p) ifelse(p < 0.001, "< 0,001", paste("=", fmt(p, 3))) # para el textoLa rotación de personal genera costos de reclutamiento, inducción y pérdida de conocimiento para cualquier organización. En este informe se analiza la base de datos rotacion (paquete paqueteMODELOS), que contiene información histórica de los empleados de una empresa, con el fin de: (i) caracterizar a los trabajadores, (ii) identificar los factores asociados a la rotación de cargo y (iii) estimar un modelo de regresión logística que permita predecir la probabilidad de que un empleado rote en el próximo periodo, de modo que la gerencia pueda intervenir de forma preventiva.
El documento sigue los pasos propuestos por la gerencia: selección de variables e hipótesis (sección 2), análisis exploratorio y preparación de los datos (secciones 3 y 4), análisis bivariado (sección 5), estimación del modelo (sección 6), evaluación (sección 7), predicción (sección 8) y conclusiones (sección 9).
A partir del conocimiento del negocio y de la literatura sobre rotación laboral, se seleccionan tres variables categóricas y tres cuantitativas. Para cada una se plantea el mecanismo esperado y la hipótesis que se contrastará en las secciones 5 y 6.
Variables categóricas
1. Satisfacción laboral (Satisfación_Laboral). La satisfacción con el trabajo es uno de los predictores clásicos de la intención de abandono: un empleado insatisfecho con sus funciones, su reconocimiento o sus condiciones tiene más incentivos para buscar otro cargo. Aunque se almacena con números (1 a 4), es una variable categórica ordinal según el diccionario de datos.
Hipótesis H1: a mayor satisfacción laboral, menor probabilidad de rotar; los empleados muy satisfechos rotan menos que los muy insatisfechos (coeficiente esperado negativo para los niveles superiores frente a Muy insatisfecho).
2. Estado civil (Estado_Civil). Las personas solteras suelen tener menos cargas familiares y mayor movilidad, por lo que asumen con más facilidad el riesgo de cambiar de trabajo; las personas casadas tienden a privilegiar la estabilidad.
Hipótesis H2: los empleados solteros tienen mayor probabilidad de rotar que los casados (coeficiente esperado positivo para Soltero frente a Casado).
3. Viajes de negocios (Viaje de Negocios). Viajar con frecuencia altera las rutinas personales y familiares y genera cansancio, lo que puede deteriorar la satisfacción con el cargo.
Hipótesis H3: a mayor frecuencia de viajes, mayor probabilidad de rotar; quienes viajan frecuentemente rotan más que quienes no viajan (coeficiente esperado positivo).
Variables cuantitativas
4. Edad (Edad). Los empleados jóvenes están en etapa de exploración profesional y tienen menos arraigo; con la edad aumentan las responsabilidades y la preferencia por la estabilidad.
Hipótesis H4: a mayor edad, menor probabilidad de rotar (coeficiente esperado negativo).
5. Ingreso mensual (Ingreso_Mensual). Un salario bajo reduce el costo de oportunidad de irse y hace más atractivas las ofertas externas.
Hipótesis H5: a mayor ingreso mensual, menor probabilidad de rotar (coeficiente esperado negativo).
6. Distancia a la casa (Distancia_Casa). Un desplazamiento largo consume tiempo y dinero, genera cansancio y reduce el tiempo para la vida personal; un empleado que vive lejos valora más una oferta de trabajo cercana a su vivienda.
Hipótesis H6: a mayor distancia entre la casa y el trabajo, mayor probabilidad de rotar (coeficiente esperado positivo).
## [1] 1470 24
La base contiene 1470 registros (empleados) y 24 variables. Algunos nombres de columna incluyen tildes, espacios o errores de digitación (por ejemplo, Viaje de Negocios o Satisfación_Laboral), lo que dificulta su manejo en el código. Por eso se renombran por posición con nombres estandarizados, sin alterar los datos.
nombres_nuevos <- c("Rotacion", "Edad", "Viaje_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",
"Anios_Experiencia", "Capacitaciones",
"Equilibrio_Trabajo_Vida", "Antiguedad", "Antiguedad_Cargo",
"Anios_Ultima_Promocion", "Anios_Mismo_Jefe")
stopifnot(length(nombres_nuevos) == ncol(datos_crudos))
datos <- datos_crudos
names(datos) <- nombres_nuevosCon base en el diccionario de datos suministrado, las variables se clasifican según su naturaleza (Tabla 1). Las escalas tipo Likert (satisfacción, rendimiento, educación y equilibrio trabajo–vida) están almacenadas como números, pero son variables categóricas ordinales: la distancia entre sus niveles no es necesariamente constante, por lo que en el análisis descriptivo se tratan como factores ordenados.
Las variables como edad, antiguedad del cargo, años ultima promoción y años a cargo con el mismo jefe, son medidas de tiempo y podrían considerarse como variables cuantitativas continuas, sin embargo, se tomó de referencia los números como vienen en la base para detrminar su tipo.
dicc <- tibble::tribble(
~Variable, ~Tipo, ~Descripción,
"Rotacion", "Categórica nominal", "Rotó de cargo (Si / No) — variable respuesta",
"Edad", "Cuantitativa discreta", "Edad en años",
"Viaje_Negocios", "Categórica ordinal", "Frecuencia de viajes: No_Viaja, Raramente, Frecuentemente",
"Departamento", "Categórica nominal", "IyD, RH, Ventas",
"Distancia_Casa", "Cuantitativa continua", "Kilómetros de distancia desde la casa",
"Educacion", "Categórica ordinal", "1 = Primaria … 5 = Posgrado",
"Campo_Educacion", "Categórica ordinal", "Área de formación",
"Satisfaccion_Ambiental", "Categórica ordinal", "1 = Muy insatisfecho … 4 = Muy satisfecho",
"Genero", "Categórica nominal", "F / M",
"Cargo", "Categórica nominal", "Cargo actual",
"Satisfaccion_Laboral", "Categórica ordinal", "1 = Muy insatisfecho … 4 = Muy satisfecho",
"Estado_Civil", "Categórica nominal", "Casado, Divorciado, Soltero",
"Ingreso_Mensual", "Cuantitativa continua", "Ingreso mensual (unidades monetarias)",
"Trabajos_Anteriores", "Cuantitativa discreta", "Trabajos antes de ingresar a la empresa",
"Horas_Extra", "Categórica nominal", "Trabaja horas extra (Si / No)",
"Porcentaje_Aumento_Salarial", "Cuantitativa continua", "Porcentaje del último aumento salarial",
"Rendimiento_Laboral", "Categórica ordinal", "1 = Bajo … 4 = Muy alto",
"Anios_Experiencia", "Cuantitativa discreta", "Años de experiencia laboral total",
"Capacitaciones", "Cuantitativa discreta", "Capacitaciones recibidas en el último año",
"Equilibrio_Trabajo_Vida", "Categórica ordinal", "1 = Muy bajo … 4 = Alto",
"Antiguedad", "Cuantitativa discreta", "Años en la empresa",
"Antiguedad_Cargo", "Cuantitativa discreta", "Años en el cargo actual",
"Anios_Ultima_Promocion", "Cuantitativa discreta", "Años desde la última promoción",
"Anios_Mismo_Jefe", "Cuantitativa discreta", "Años a cargo del mismo jefe"
)
tabla(dicc, caption = "Diccionario y clasificación de las variables de la base `rotacion`.")| Variable | Tipo | Descripción |
|---|---|---|
| Rotacion | Categórica nominal | Rotó de cargo (Si / No) — variable respuesta |
| Edad | Cuantitativa discreta | Edad en años |
| Viaje_Negocios | Categórica ordinal | Frecuencia de viajes: No_Viaja, Raramente, Frecuentemente |
| Departamento | Categórica nominal | IyD, RH, Ventas |
| Distancia_Casa | Cuantitativa continua | Kilómetros de distancia desde la casa |
| Educacion | Categórica ordinal | 1 = Primaria … 5 = Posgrado |
| Campo_Educacion | Categórica ordinal | Área de formación |
| Satisfaccion_Ambiental | Categórica ordinal | 1 = Muy insatisfecho … 4 = Muy satisfecho |
| Genero | Categórica nominal | F / M |
| Cargo | Categórica nominal | Cargo actual |
| Satisfaccion_Laboral | Categórica ordinal | 1 = Muy insatisfecho … 4 = Muy satisfecho |
| Estado_Civil | Categórica nominal | Casado, Divorciado, Soltero |
| Ingreso_Mensual | Cuantitativa continua | Ingreso mensual (unidades monetarias) |
| Trabajos_Anteriores | Cuantitativa discreta | Trabajos antes de ingresar a la empresa |
| Horas_Extra | Categórica nominal | Trabaja horas extra (Si / No) |
| Porcentaje_Aumento_Salarial | Cuantitativa continua | Porcentaje del último aumento salarial |
| Rendimiento_Laboral | Categórica ordinal | 1 = Bajo … 4 = Muy alto |
| Anios_Experiencia | Cuantitativa discreta | Años de experiencia laboral total |
| Capacitaciones | Cuantitativa discreta | Capacitaciones recibidas en el último año |
| Equilibrio_Trabajo_Vida | Categórica ordinal | 1 = Muy bajo … 4 = Alto |
| Antiguedad | Cuantitativa discreta | Años en la empresa |
| Antiguedad_Cargo | Cuantitativa discreta | Años en el cargo actual |
| Anios_Ultima_Promocion | Cuantitativa discreta | Años desde la última promoción |
| Anios_Mismo_Jefe | Cuantitativa discreta | Años a cargo del mismo jefe |
calidad <- tibble(
Indicador = c("Registros", "Variables", "Celdas con datos faltantes (NA)",
"Registros duplicados", "Variables con un único valor"),
Valor = c(nrow(datos), ncol(datos), sum(is.na(datos)),
sum(duplicated(datos)), sum(sapply(datos, function(x) n_distinct(x) == 1)))
)
tabla(calidad, caption = "Resumen de la calidad de la base de datos.", digits = 0)| Indicador | Valor |
|---|---|
| Registros | 1.470 |
| Variables | 24 |
| Celdas con datos faltantes (NA) | 0 |
| Registros duplicados | 0 |
| Variables con un único valor | 0 |
num_vars <- names(datos)[sapply(datos, is.numeric)]
rangos <- tibble(
Variable = num_vars,
Mínimo = sapply(datos[num_vars], min),
Máximo = sapply(datos[num_vars], max),
`Valores distintos` = sapply(datos[num_vars], n_distinct)
)
tabla(rangos, caption = "Rango observado de las variables numéricas (control de consistencia).", digits = 0)| Variable | Mínimo | Máximo | Valores distintos |
|---|---|---|---|
| Edad | 18 | 60 | 43 |
| Distancia_Casa | 1 | 29 | 29 |
| Educacion | 1 | 5 | 5 |
| Satisfaccion_Ambiental | 1 | 4 | 4 |
| Satisfaccion_Laboral | 1 | 4 | 4 |
| Ingreso_Mensual | 1.009 | 19.999 | 1.349 |
| Trabajos_Anteriores | 0 | 9 | 10 |
| Porcentaje_Aumento_Salarial | 11 | 25 | 15 |
| Rendimiento_Laboral | 3 | 4 | 2 |
| Anios_Experiencia | 0 | 40 | 40 |
| Capacitaciones | 0 | 6 | 7 |
| Equilibrio_Trabajo_Vida | 1 | 4 | 4 |
| Antiguedad | 0 | 40 | 37 |
| Antiguedad_Cargo | 0 | 18 | 19 |
| Anios_Ultima_Promocion | 0 | 15 | 16 |
| Anios_Mismo_Jefe | 0 | 17 | 18 |
Como muestran las Tablas 2 y 3, la base no presenta datos faltantes ni registros duplicados, y todos los valores se encuentran dentro de rangos plausibles: las escalas Likert toman valores entre 1 y 4 (o 1 y 5 en Educación), la edad está entre 18 y 60 años y no hay ingresos negativos. Dos observaciones de consistencia merecen atención:
coherencia <- tibble(
Regla = c("Antigüedad en el cargo ≤ Antigüedad en la empresa",
"Antigüedad en la empresa ≤ Años de experiencia",
"Años desde la última promoción ≤ Antigüedad en la empresa",
"Años con el mismo jefe ≤ Antigüedad en la empresa"),
`Registros que incumplen` = c(
sum(datos$Antiguedad_Cargo > datos$Antiguedad),
sum(datos$Antiguedad > datos$Anios_Experiencia),
sum(datos$Anios_Ultima_Promocion > datos$Antiguedad),
sum(datos$Anios_Mismo_Jefe > datos$Antiguedad))
)
tabla(coherencia, caption = "Reglas de coherencia lógica entre variables temporales.", digits = 0)| Regla | Registros que incumplen |
|---|---|
| Antigüedad en el cargo ≤ Antigüedad en la empresa | 0 |
| Antigüedad en la empresa ≤ Años de experiencia | 0 |
| Años desde la última promoción ≤ Antigüedad en la empresa | 0 |
| Años con el mismo jefe ≤ Antigüedad en la empresa | 0 |
Para el análisis se realizan las siguientes transformaciones:
y con la codificación solicitada (y = 1 si rotó, y = 0 si no rotó) y se conserva Rotacion como factor para tablas y gráficos.Sat_Laboral, una versión no ordenada de la satisfacción laboral con Muy insatisfecho como referencia, de modo que cada nivel tenga su propio coeficiente (con un factor ordenado, R usaría contrastes polinomiales, más difíciles de interpretar).Ingreso_Miles (ingreso / 1.000) para que el coeficiente del modelo se interprete por cada mil unidades monetarias y no por una unidad.lik4 <- c("Muy insatisfecho", "Insatisfecho", "Satisfecho", "Muy satisfecho")
datos <- datos |>
mutate(
y = ifelse(Rotacion == "Si", 1L, 0L),
Rotacion = factor(Rotacion, levels = c("No", "Si")),
Horas_Extra = factor(Horas_Extra, levels = c("No", "Si")),
Estado_Civil = factor(Estado_Civil, levels = c("Casado", "Divorciado", "Soltero")),
Viaje_Negocios = factor(Viaje_Negocios, levels = c("No_Viaja", "Raramente", "Frecuentemente")),
Departamento = factor(Departamento),
Campo_Educacion = factor(Campo_Educacion),
Genero = factor(Genero),
Cargo = factor(Cargo),
Educacion = factor(Educacion, levels = 1:5, ordered = TRUE,
labels = c("Primaria", "Secundaria", "Técnico/Tecnólogo", "Pregrado", "Posgrado")),
Satisfaccion_Ambiental = factor(Satisfaccion_Ambiental, levels = 1:4, labels = lik4, ordered = TRUE),
Satisfaccion_Laboral = factor(Satisfaccion_Laboral, levels = 1:4, labels = lik4, ordered = TRUE),
Rendimiento_Laboral = factor(Rendimiento_Laboral, levels = 1:4, ordered = TRUE,
labels = c("Bajo", "Medio", "Alto", "Muy alto")),
Equilibrio_Trabajo_Vida = factor(Equilibrio_Trabajo_Vida, levels = 1:4, ordered = TRUE,
labels = c("Muy bajo", "Bajo", "Medio", "Alto")),
Ingreso_Miles = Ingreso_Mensual / 1000,
Sat_Laboral = factor(as.character(Satisfaccion_Laboral), levels = lik4)
)
vars_cuanti <- c("Edad", "Distancia_Casa", "Ingreso_Mensual", "Trabajos_Anteriores",
"Porcentaje_Aumento_Salarial", "Anios_Experiencia", "Capacitaciones",
"Antiguedad", "Antiguedad_Cargo", "Anios_Ultima_Promocion", "Anios_Mismo_Jefe")
vars_cuali <- c("Viaje_Negocios", "Departamento", "Educacion", "Campo_Educacion",
"Satisfaccion_Ambiental", "Genero", "Cargo", "Satisfaccion_Laboral",
"Estado_Civil", "Horas_Extra", "Rendimiento_Laboral", "Equilibrio_Trabajo_Vida")
vars_sel_cuali <- c("Sat_Laboral", "Estado_Civil", "Viaje_Negocios")
vars_sel_cuanti <- c("Edad", "Ingreso_Mensual", "Distancia_Casa")
glimpse(datos)## Rows: 1.470
## Columns: 27
## $ Rotacion <fct> Si, No, Si, No, No, No, No, No, No, No, No…
## $ Edad <dbl> 41, 49, 37, 33, 27, 32, 59, 30, 38, 36, 35…
## $ Viaje_Negocios <fct> Raramente, Frecuentemente, Raramente, Frec…
## $ Departamento <fct> Ventas, IyD, IyD, IyD, IyD, IyD, IyD, IyD,…
## $ Distancia_Casa <dbl> 1, 8, 2, 3, 2, 2, 3, 24, 23, 27, 16, 15, 2…
## $ Educacion <ord> Secundaria, Primaria, Secundaria, Pregrado…
## $ Campo_Educacion <fct> Ciencias, Ciencias, Otra, Ciencias, Salud,…
## $ Satisfaccion_Ambiental <ord> Insatisfecho, Satisfecho, Muy satisfecho, …
## $ Genero <fct> F, M, M, F, M, M, F, M, M, M, M, F, M, M, …
## $ Cargo <fct> Ejecutivo_Ventas, Investigador_Cientifico,…
## $ Satisfaccion_Laboral <ord> Muy satisfecho, Insatisfecho, Satisfecho, …
## $ Estado_Civil <fct> Soltero, Casado, Soltero, Casado, Casado, …
## $ Ingreso_Mensual <dbl> 5993, 5130, 2090, 2909, 3468, 3068, 2670, …
## $ Trabajos_Anteriores <dbl> 8, 1, 6, 1, 9, 0, 4, 1, 0, 6, 0, 0, 1, 0, …
## $ Horas_Extra <fct> Si, No, Si, Si, No, No, Si, No, No, No, No…
## $ Porcentaje_Aumento_Salarial <dbl> 11, 23, 15, 11, 12, 13, 20, 22, 21, 13, 13…
## $ Rendimiento_Laboral <ord> Alto, Muy alto, Alto, Alto, Alto, Alto, Mu…
## $ Anios_Experiencia <dbl> 8, 10, 7, 8, 6, 8, 12, 1, 10, 17, 6, 10, 5…
## $ Capacitaciones <dbl> 0, 3, 3, 3, 3, 2, 3, 2, 2, 3, 5, 3, 1, 2, …
## $ Equilibrio_Trabajo_Vida <ord> Muy bajo, Medio, Medio, Medio, Medio, Bajo…
## $ Antiguedad <dbl> 6, 10, 0, 8, 2, 7, 1, 1, 9, 7, 5, 9, 5, 2,…
## $ Antiguedad_Cargo <dbl> 4, 7, 0, 7, 2, 7, 0, 0, 7, 7, 4, 5, 2, 2, …
## $ Anios_Ultima_Promocion <dbl> 0, 1, 0, 3, 2, 3, 0, 0, 1, 7, 0, 0, 4, 1, …
## $ Anios_Mismo_Jefe <dbl> 5, 7, 0, 0, 2, 6, 0, 0, 8, 7, 3, 8, 3, 2, …
## $ y <int> 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
## $ Ingreso_Miles <dbl> 5,993, 5,130, 2,090, 2,909, 3,468, 3,068, …
## $ Sat_Laboral <fct> Muy satisfecho, Insatisfecho, Satisfecho, …
tab_rot <- datos |> count(Rotacion, name = "Frecuencia") |>
mutate(`Porcentaje (%)` = 100 * Frecuencia / sum(Frecuencia))
tabla(tab_rot, caption = "Distribución de la variable respuesta Rotación.", digits = 1)| Rotacion | Frecuencia | Porcentaje (%) |
|---|---|---|
| No | 1.233 | 83,9 |
| Si | 237 | 16,1 |
ggplot(tab_rot, aes(x = Rotacion, y = Frecuencia, fill = Rotacion)) +
geom_col(width = 0.55, show.legend = FALSE) +
geom_text(aes(label = paste0(Frecuencia, " (", fmt(`Porcentaje (%)`, 1), " %)")),
vjust = -0.4, size = 4) +
scale_fill_manual(values = col_rot) +
scale_y_continuous(expand = expansion(mult = c(0, 0.12))) +
labs(x = "¿Rotó de cargo?", y = "Número de empleados")Figura 1: Distribución de la rotación de cargo.
Interpretación. De los 1470 empleados, 237 rotaron de cargo, es decir, el 16,1 % (Tabla 5 y Figura 1). Por cada empleado que rota hay aproximadamente 5,2 que permanecen. Se trata de una variable desbalanceada: la clase de interés (Si) es minoritaria. Esto tiene dos implicaciones para el modelado: (i) un modelo que prediga siempre “No rota” acertaría el 83,9 % de las veces sin aportar información, por lo que la exactitud (accuracy) no es una buena métrica y se usará la curva ROC y el AUC; y (ii) el punto de corte habitual de 0,5 será inadecuado para clasificar, por lo que se definirá un corte acorde con la prevalencia (sección 8).
desc <- lapply(vars_cuanti, function(v) {
x <- datos[[v]]
tibble(Variable = v, Media = mean(x), DE = sd(x), Mín = min(x),
Q1 = quantile(x, .25), Mediana = median(x), Q3 = quantile(x, .75),
Máx = max(x), `CV (%)` = 100 * sd(x) / mean(x),
Asimetría = asimetria(x), Atípicos = n_atipicos(x))
}) |> bind_rows()
tabla(desc, caption = "Estadísticos descriptivos de las variables cuantitativas (atípicos según el criterio de 1,5 × RIC).")| Variable | Media | DE | Mín | Q1 | Mediana | Q3 | Máx | CV (%) | Asimetría | Atípicos |
|---|---|---|---|---|---|---|---|---|---|---|
| Edad | 36,92 | 9,14 | 18 | 30 | 36 | 43 | 60 | 24,74 | 0,41 | 0 |
| Distancia_Casa | 9,19 | 8,11 | 1 | 2 | 7 | 14 | 29 | 88,19 | 0,96 | 0 |
| Ingreso_Mensual | 6.502,93 | 4.707,96 | 1.009 | 2.911 | 4.919 | 8.379 | 19.999 | 72,40 | 1,37 | 114 |
| Trabajos_Anteriores | 2,69 | 2,50 | 0 | 1 | 2 | 4 | 9 | 92,75 | 1,02 | 52 |
| Porcentaje_Aumento_Salarial | 15,21 | 3,66 | 11 | 12 | 14 | 18 | 25 | 24,06 | 0,82 | 0 |
| Anios_Experiencia | 11,28 | 7,78 | 0 | 6 | 10 | 15 | 40 | 68,98 | 1,11 | 63 |
| Capacitaciones | 2,80 | 1,29 | 0 | 2 | 3 | 3 | 6 | 46,06 | 0,55 | 238 |
| Antiguedad | 7,01 | 6,13 | 0 | 3 | 5 | 9 | 40 | 87,42 | 1,76 | 104 |
| Antiguedad_Cargo | 4,23 | 3,62 | 0 | 2 | 3 | 7 | 18 | 85,67 | 0,92 | 21 |
| Anios_Ultima_Promocion | 2,19 | 3,22 | 0 | 0 | 1 | 3 | 15 | 147,29 | 1,98 | 107 |
| Anios_Mismo_Jefe | 4,12 | 3,57 | 0 | 2 | 3 | 7 | 17 | 86,54 | 0,83 | 14 |
datos |>
select(all_of(vars_cuanti)) |>
pivot_longer(everything(), names_to = "Variable", values_to = "Valor") |>
ggplot(aes(x = Valor)) +
geom_histogram(bins = 25, fill = "#1f3b57", color = "white", alpha = 0.85) +
facet_wrap(~ Variable, scales = "free", ncol = 3) +
labs(x = NULL, y = "Frecuencia")Figura 2: Histogramas de las variables cuantitativas.
datos |>
select(all_of(vars_cuanti)) |>
mutate(across(everything(), ~ as.numeric(scale(.x)))) |>
pivot_longer(everything(), names_to = "Variable", values_to = "z") |>
ggplot(aes(x = reorder(Variable, z, FUN = median), y = z)) +
geom_boxplot(fill = "#d6e0ea", outlier.color = "#b23a48", outlier.size = 1) +
coord_flip() +
labs(x = NULL, y = "Valor estandarizado (puntaje z)")Figura 3: Diagramas de caja de las variables cuantitativas (valores estandarizados).
Interpretación. La Tabla 6 y las Figuras 2 y 3 muestran que:
Tratamiento de atípicos. Los valores atípicos detectados (principalmente en ingreso y variables de tiempo) corresponden a situaciones reales y plausibles (directivos con altos salarios, empleados con largas trayectorias), no a errores de digitación. Por ello no se eliminan ni se imputan: hacerlo distorsionaría la realidad de la empresa. La regresión logística no exige normalidad de las covariables, de modo que la asimetría no invalida el modelo.
desc_cuali <- lapply(vars_cuali, function(v) {
datos |> count(Categoría = .data[[v]], name = "n") |>
mutate(Variable = v, `%` = 100 * n / sum(n), Categoría = as.character(Categoría))
}) |> bind_rows() |> select(Variable, Categoría, n, `%`)
tabla(desc_cuali, caption = "Distribución de frecuencias de las variables cualitativas.", digits = 1) |>
collapse_rows(columns = 1, valign = "top") |>
scroll_box(height = "450px")| Variable | Categoría | n | % |
|---|---|---|---|
| Viaje_Negocios | No_Viaja | 150 | 10,2 |
| Raramente | 1.043 | 71,0 | |
| Frecuentemente | 277 | 18,8 | |
| Departamento | IyD | 961 | 65,4 |
| RH | 63 | 4,3 | |
| Ventas | 446 | 30,3 | |
| Educacion | Primaria | 170 | 11,6 |
| Secundaria | 282 | 19,2 | |
| Técnico/Tecnólogo | 572 | 38,9 | |
| Pregrado | 398 | 27,1 | |
| Posgrado | 48 | 3,3 | |
| Campo_Educacion | Ciencias | 606 | 41,2 |
| Humanidades | 27 | 1,8 | |
| Mercadeo | 159 | 10,8 | |
| Otra | 82 | 5,6 | |
| Salud | 464 | 31,6 | |
| Tecnicos | 132 | 9,0 | |
| Satisfaccion_Ambiental | Muy insatisfecho | 284 | 19,3 |
| Insatisfecho | 287 | 19,5 | |
| Satisfecho | 453 | 30,8 | |
| Muy satisfecho | 446 | 30,3 | |
| Genero | F | 588 | 40,0 |
| M | 882 | 60,0 | |
| Cargo | Director_Investigación | 80 | 5,4 |
| Director_Manofactura | 145 | 9,9 | |
| Ejecutivo_Ventas | 326 | 22,2 | |
| Gerente | 102 | 6,9 | |
| Investigador_Cientifico | 292 | 19,9 | |
| Recursos_Humanos | 52 | 3,5 | |
| Representante_Salud | 131 | 8,9 | |
| Representante_Ventas | 83 | 5,6 | |
| Tecnico_Laboratorio | 259 | 17,6 | |
| Satisfaccion_Laboral | Muy insatisfecho | 289 | 19,7 |
| Insatisfecho | 280 | 19,0 | |
| Satisfecho | 442 | 30,1 | |
| Muy satisfecho | 459 | 31,2 | |
| Estado_Civil | Casado | 673 | 45,8 |
| Divorciado | 327 | 22,2 | |
| Soltero | 470 | 32,0 | |
| Horas_Extra | No | 1.054 | 71,7 |
| Si | 416 | 28,3 | |
| Rendimiento_Laboral | Alto | 1.244 | 84,6 |
| Muy alto | 226 | 15,4 | |
| Equilibrio_Trabajo_Vida | Muy bajo | 80 | 5,4 |
| Bajo | 344 | 23,4 | |
| Medio | 893 | 60,7 | |
| Alto | 153 | 10,4 |
graf_barra <- function(v) {
datos |> count(cat = .data[[v]]) |> mutate(p = n / sum(n)) |>
ggplot(aes(x = cat, y = p)) +
geom_col(fill = "#1f3b57", alpha = 0.85, width = 0.7) +
geom_text(aes(label = percent(p, accuracy = 1, decimal.mark = ",")), hjust = -0.1, size = 3) +
scale_y_continuous(labels = percent, expand = expansion(mult = c(0, 0.25))) +
coord_flip() + labs(title = v, x = NULL, y = NULL) +
theme(axis.text.x = element_blank(), panel.grid.major = element_blank())
}
wrap_plots(lapply(vars_cuali, graf_barra), ncol = 3)Figura 4: Distribución porcentual de las variables cualitativas.
Interpretación. De la Tabla 7 y la Figura 4 se destaca que:
Antes de modelar conviene revisar si las covariables cuantitativas están fuertemente correlacionadas entre sí, pues la multicolinealidad infla los errores estándar y dificulta la interpretación de los coeficientes.
mc <- cor(datos[vars_cuanti])
as.data.frame(as.table(mc)) |>
ggplot(aes(Var1, Var2, fill = Freq)) +
geom_tile(color = "white") +
geom_text(aes(label = fmt(Freq, 2)), size = 2.8) +
scale_fill_gradient2(low = "#2c7bb6", mid = "white", high = "#b23a48",
limits = c(-1, 1), name = "r") +
labs(x = NULL, y = NULL) +
theme(axis.text.x = element_text(angle = 45, hjust = 1))Figura 5: Matriz de correlación de Pearson entre las variables cuantitativas.
Interpretación. La Figura 5 muestra correlaciones altas entre las variables de tiempo: antigüedad en la empresa con antigüedad en el cargo (r = 0,76) y con años con el mismo jefe (r = 0,77), y experiencia con ingreso (r = 0,77). Por esta razón no se incluyen simultáneamente varias variables de tiempo en el modelo. Entre las tres cuantitativas seleccionadas, la correlación más alta es la de edad con ingreso (r = 0,50), moderada; la distancia a la casa prácticamente no se correlaciona con ninguna de las dos (r = 0,00 con la edad y r = -0,02 con el ingreso). La colinealidad se verificará con el factor de inflación de varianza (VIF) en la sección 6.
En esta sección se estudia la relación de cada variable con la rotación (y = 1 si rota, y = 0 si no rota). Para las variables cualitativas se usa la prueba ji-cuadrado de independencia y la V de Cramér como medida de intensidad; para las cuantitativas se ajusta una regresión logística simple, cuyo coeficiente indica la dirección del efecto (signo) y su significancia.
biv_cuali <- lapply(vars_cuali, function(v) {
tb <- table(datos[[v]], datos$Rotacion)
tb <- tb[rowSums(tb) > 0, , drop = FALSE]
pr <- suppressWarnings(chisq.test(tb))
tibble(Variable = v,
`Ji-cuadrado` = unname(pr$statistic), gl = unname(pr$parameter),
`Valor p` = pval(pr$p.value),
`V de Cramér` = sqrt(unname(pr$statistic) / (sum(tb) * (min(dim(tb)) - 1))),
p = pr$p.value)
}) |> bind_rows() |> arrange(p) |>
mutate(Conclusión = ifelse(p < 0.05, "Asociada", "No asociada")) |> select(-p)
tabla(biv_cuali, caption = "Prueba ji-cuadrado de independencia entre cada variable cualitativa y la rotación.")| Variable | Ji-cuadrado | gl | Valor p | V de Cramér | Conclusión |
|---|---|---|---|---|---|
| Horas_Extra | 87,56 | 1 | < 0,001 | 0,24 | Asociada |
| Cargo | 86,19 | 8 | < 0,001 | 0,24 | Asociada |
| Estado_Civil | 46,16 | 2 | < 0,001 | 0,18 | Asociada |
| Viaje_Negocios | 24,18 | 2 | < 0,001 | 0,13 | Asociada |
| Satisfaccion_Ambiental | 22,50 | 3 | < 0,001 | 0,12 | Asociada |
| Satisfaccion_Laboral | 17,51 | 3 | < 0,001 | 0,11 | Asociada |
| Equilibrio_Trabajo_Vida | 16,33 | 3 | < 0,001 | 0,11 | Asociada |
| Departamento | 10,80 | 2 | 0,005 | 0,09 | Asociada |
| Campo_Educacion | 16,02 | 5 | 0,007 | 0,10 | Asociada |
| Genero | 1,12 | 1 | 0,291 | 0,03 | No asociada |
| Educacion | 3,07 | 4 | 0,546 | 0,05 | No asociada |
| Rendimiento_Laboral | 0,00 | 1 | 0,990 | 0,00 | No asociada |
biv_cuanti <- lapply(vars_cuanti, function(v) {
m <- glm(as.formula(paste("y ~", v)), family = binomial, data = datos)
s <- summary(m)$coefficients[2, ]
tibble(Variable = v,
`Media (No rota)` = mean(datos[[v]][datos$y == 0]),
`Media (Sí rota)` = mean(datos[[v]][datos$y == 1]),
`β` = s[1], `Signo` = ifelse(s[1] > 0, "+", "−"),
`Valor p` = pval(s[4]), p = s[4])
}) |> bind_rows() |> arrange(p) |>
mutate(Conclusión = ifelse(p < 0.05, "Asociada", "No asociada")) |> select(-p)
tabla(biv_cuanti, caption = "Regresión logística simple de la rotación sobre cada variable cuantitativa.", digits = 4)| Variable | Media (No rota) | Media (Sí rota) | β | Signo | Valor p | Conclusión |
|---|---|---|---|---|---|---|
| Anios_Experiencia | 11,8629 | 8,2447 | -0,0777 | − | < 0,001 | Asociada |
| Antiguedad_Cargo | 4,4842 | 2,9030 | -0,1463 | − | < 0,001 | Asociada |
| Edad | 37,5620 | 33,6076 | -0,0523 | − | < 0,001 | Asociada |
| Ingreso_Mensual | 6.832,7397 | 4.787,0928 | -0,0001 | − | < 0,001 | Asociada |
| Anios_Mismo_Jefe | 4,3674 | 2,8523 | -0,1414 | − | < 0,001 | Asociada |
| Antiguedad | 7,3690 | 5,1308 | -0,0808 | − | < 0,001 | Asociada |
| Distancia_Casa | 8,9157 | 10,6329 | 0,0247 |
|
0,003 | Asociada |
| Capacitaciones | 2,8329 | 2,6245 | -0,1299 | − | 0,023 | Asociada |
| Trabajos_Anteriores | 2,6456 | 2,9409 | 0,0456 |
|
0,096 | No asociada |
| Anios_Ultima_Promocion | 2,2344 | 1,9451 | -0,0298 | − | 0,206 | No asociada |
| Porcentaje_Aumento_Salarial | 15,2311 | 15,0970 | -0,0101 | − | 0,605 | No asociada |
Las Tablas 8 y 9 muestran que la mayoría de las variables se asocian significativamente con la rotación (p < 0,05). Entre las cualitativas, las de mayor intensidad de asociación (mayor V de Cramér) son Horas_Extra, Cargo, Estado_Civil. Entre las cuantitativas significativas, tienen coeficiente negativo (a mayor valor, menor probabilidad de rotar) Anios_Experiencia, Antiguedad_Cargo, Edad, Ingreso_Mensual, Anios_Mismo_Jefe, Antiguedad, Capacitaciones, y coeficiente positivo Distancia_Casa. En cambio, Genero, Educacion, Rendimiento_Laboral, Trabajos_Anteriores, Anios_Ultima_Promocion, Porcentaje_Aumento_Salarial no muestran asociación significativa con la rotación al 5 %.
tasas <- lapply(vars_sel_cuali, function(v) {
datos |> group_by(Categoría = .data[[v]]) |>
summarise(n = n(), Rotan = sum(y), `Tasa de rotación (%)` = 100 * mean(y), .groups = "drop") |>
mutate(Variable = v, Categoría = as.character(Categoría))
}) |> bind_rows() |> select(Variable, everything())
tabla(tasas, caption = "Tasa de rotación según las variables categóricas seleccionadas.", digits = 1) |>
collapse_rows(columns = 1, valign = "top")| Variable | Categoría | n | Rotan | Tasa de rotación (%) |
|---|---|---|---|---|
| Sat_Laboral | Muy insatisfecho | 289 | 66 | 22,8 |
| Insatisfecho | 280 | 46 | 16,4 | |
| Satisfecho | 442 | 73 | 16,5 | |
| Muy satisfecho | 459 | 52 | 11,3 | |
| Estado_Civil | Casado | 673 | 84 | 12,5 |
| Divorciado | 327 | 33 | 10,1 | |
| Soltero | 470 | 120 | 25,5 | |
| Viaje_Negocios | No_Viaja | 150 | 12 | 8,0 |
| Raramente | 1.043 | 156 | 15,0 | |
| Frecuentemente | 277 | 69 | 24,9 |
graf_tasa <- function(v, titulo) {
datos |> group_by(cat = .data[[v]]) |>
summarise(tasa = mean(y), n = n(), .groups = "drop") |>
ggplot(aes(x = cat, y = tasa)) +
geom_col(fill = col_rot["Si"], width = 0.6, alpha = 0.9) +
geom_hline(yintercept = p_rot, linetype = "dashed") +
geom_text(aes(label = percent(tasa, accuracy = 0.1, decimal.mark = ",")), vjust = -0.4, size = 3.5) +
scale_y_continuous(labels = percent, limits = c(0, 0.4)) +
scale_x_discrete(labels = function(x) gsub(" ", "\n", x)) +
labs(title = titulo, x = NULL, y = "Tasa de rotación")
}
graf_tasa("Sat_Laboral", "Satisfacción laboral") +
graf_tasa("Estado_Civil", "Estado civil") +
graf_tasa("Viaje_Negocios", "Viajes de negocios") +
plot_layout(widths = c(1.4, 1, 1))Figura 6: Proporción de empleados que rotan según las variables categóricas seleccionadas. La línea punteada indica la tasa global de rotación.
or_cuali <- lapply(vars_sel_cuali, function(v) {
m <- glm(as.formula(paste("y ~", v)), family = binomial, data = datos)
tidy(m, conf.int = TRUE) |> filter(term != "(Intercept)") |>
mutate(Variable = v)
}) |> bind_rows() |>
transmute(Variable, Término = term, `β` = estimate, `Signo` = ifelse(estimate > 0, "+", "−"),
OR = exp(estimate), `IC 95 % OR` = paste0("[", fmt(exp(conf.low)), " ; ", fmt(exp(conf.high)), "]"),
`Valor p` = pval(p.value))
tabla(or_cuali, caption = "Regresión logística simple para las variables categóricas seleccionadas (categorías de referencia: Muy insatisfecho, Casado y No_Viaja).", digits = 3)| Variable | Término | β | Signo | OR | IC 95 % OR | Valor p |
|---|---|---|---|---|---|---|
| Sat_Laboral | Sat_LaboralInsatisfecho | -0,409 | − | 0,664 | [0,44 ; 1,01] | 0,055 |
| Sat_Laboral | Sat_LaboralSatisfecho | -0,403 | − | 0,668 | [0,46 ; 0,97] | 0,034 |
| Sat_Laboral | Sat_LaboralMuy satisfecho | -0,840 | − | 0,432 | [0,29 ; 0,64] | < 0,001 |
| Estado_Civil | Estado_CivilDivorciado | -0,239 | − | 0,787 | [0,51 ; 1,19] | 0,271 |
| Estado_Civil | Estado_CivilSoltero | 0,877 |
|
2,404 | [1,77 ; 3,28] | < 0,001 |
| Viaje_Negocios | Viaje_NegociosRaramente | 0,704 |
|
2,023 | [1,14 ; 3,93] | 0,025 |
| Viaje_Negocios | Viaje_NegociosFrecuentemente | 1,339 |
|
3,815 | [2,06 ; 7,64] | < 0,001 |
graf_box <- function(v, titulo) {
ggplot(datos, aes(x = Rotacion, y = .data[[v]], fill = Rotacion)) +
geom_boxplot(width = 0.55, show.legend = FALSE, outlier.size = 0.8) +
stat_summary(fun = mean, geom = "point", shape = 23, size = 2.5, fill = "white") +
scale_fill_manual(values = col_rot) +
labs(title = titulo, x = "¿Rotó?", y = NULL)
}
graf_box("Edad", "Edad (años)") +
graf_box("Ingreso_Mensual", "Ingreso mensual") +
graf_box("Distancia_Casa", "Distancia a la casa (km)")Figura 7: Distribución de las variables cuantitativas seleccionadas según la rotación.
or_cuanti <- lapply(c("Edad", "Ingreso_Miles", "Distancia_Casa"), function(v) {
m <- glm(as.formula(paste("y ~", v)), family = binomial, data = datos)
tidy(m, conf.int = TRUE) |> filter(term != "(Intercept)")
}) |> bind_rows() |>
transmute(Variable = term, `β` = estimate, `Signo` = ifelse(estimate > 0, "+", "−"),
OR = exp(estimate), `IC 95 % OR` = paste0("[", fmt(exp(conf.low), 3), " ; ", fmt(exp(conf.high), 3), "]"),
`Valor p` = pval(p.value))
tabla(or_cuanti, caption = "Regresión logística simple para las variables cuantitativas seleccionadas (ingreso en miles).", digits = 3)| Variable | β | Signo | OR | IC 95 % OR | Valor p |
|---|---|---|---|---|---|
| Edad | -0,052 | − | 0,949 | [0,933 ; 0,965] | < 0,001 |
| Ingreso_Miles | -0,127 | − | 0,881 | [0,842 ; 0,917] | < 0,001 |
| Distancia_Casa | 0,025 |
|
1,025 | [1,008 ; 1,042] | 0,003 |
g <- function(var, cat) mean(datos$y[datos[[var]] == cat])
b <- function(v) coef(glm(as.formula(paste("y ~", v)), family = binomial, data = datos))[2]
contraste <- tibble(
Hipótesis = c("H1: Muy satisfecho vs. muy insatisfecho (−)", "H2: Soltero vs. casado (+)",
"H3: Viaja frecuentemente vs. no viaja (+)", "H4: Edad (−)",
"H5: Ingreso mensual (−)", "H6: Distancia a la casa (+)"),
`Evidencia bivariada` = c(
paste0("Tasa ", pct(g("Sat_Laboral", "Muy satisfecho")), " vs. ", pct(g("Sat_Laboral", "Muy insatisfecho"))),
paste0("Tasa ", pct(g("Estado_Civil", "Soltero")), " vs. ", pct(g("Estado_Civil", "Casado"))),
paste0("Tasa ", pct(g("Viaje_Negocios", "Frecuentemente")), " vs. ", pct(g("Viaje_Negocios", "No_Viaja"))),
paste0("β = ", fmt(b("Edad"), 4)),
paste0("β = ", fmt(b("Ingreso_Miles"), 4), " (por mil)"),
paste0("β = ", fmt(b("Distancia_Casa"), 4))),
`Signo observado` = c(
sign(or_cuali$`β`[or_cuali$Término == "Sat_LaboralMuy satisfecho"]),
sign(or_cuali$`β`[or_cuali$Término == "Estado_CivilSoltero"]),
sign(or_cuali$`β`[or_cuali$Término == "Viaje_NegociosFrecuentemente"]),
sign(b("Edad")), sign(b("Ingreso_Miles")), sign(b("Distancia_Casa"))),
`Signo esperado` = c(-1, 1, 1, -1, -1, 1)
) |>
mutate(Resultado = ifelse(`Signo observado` == `Signo esperado`, "Se confirma", "No se confirma"),
`Signo observado` = ifelse(`Signo observado` > 0, "+", "−"),
`Signo esperado` = ifelse(`Signo esperado` > 0, "+", "−"))
tabla(contraste, caption = "Contraste entre las hipótesis planteadas y los resultados del análisis bivariado.")| Hipótesis | Evidencia bivariada | Signo observado | Signo esperado | Resultado |
|---|---|---|---|---|
| H1: Muy satisfecho vs. muy insatisfecho (−) | Tasa 11,3 % vs. 22,8 % | − | − | Se confirma |
| H2: Soltero vs. casado (+) | Tasa 25,5 % vs. 12,5 % |
|
|
Se confirma |
| H3: Viaja frecuentemente vs. no viaja (+) | Tasa 24,9 % vs. 8,0 % |
|
|
Se confirma |
| H4: Edad (−) | β = -0,0523 | − | − | Se confirma |
| H5: Ingreso mensual (−) | β = -0,1271 (por mil) | − | − | Se confirma |
| H6: Distancia a la casa (+) | β = 0,0247 |
|
|
Se confirma |
Interpretación. Los resultados bivariados (Tablas 10 a 13 y Figuras 6 y 7) confirman las seis hipótesis en cuanto al signo esperado, y todas las asociaciones son estadísticamente significativas:
Estas son relaciones marginales: no controlan por las demás variables. Por ejemplo, edad e ingreso están relacionadas entre sí, y parte del efecto de cada una puede deberse a las otras. El modelo multivariado de la siguiente sección permite aislar el efecto de cada variable manteniendo constantes las demás.
Se estima un modelo de regresión logística en el que la probabilidad de rotar, \(\pi_i = P(y_i = 1)\), se relaciona con las seis covariables mediante la función logit:
\[\begin{equation} \begin{split} \log\left(\frac{\pi_i}{1-\pi_i}\right) = {} & \beta_0 + \beta_1\,\text{SatInsat}_i + \beta_2\,\text{SatSat}_i + \beta_3\,\text{SatMuySat}_i + \beta_4\,\text{Divorciado}_i + \beta_5\,\text{Soltero}_i \\ & + \beta_6\,\text{ViajeRaro}_i + \beta_7\,\text{ViajeFrec}_i + \beta_8\,\text{Edad}_i + \beta_9\,\text{Ingreso}_i + \beta_{10}\,\text{Distancia}_i \end{split} \tag{1} \end{equation}\]
donde las variables de satisfacción, estado civil y viaje son indicadoras (0/1) de cada categoría frente a su referencia (Muy insatisfecho, Casado y No_Viaja), el ingreso se expresa en miles y la distancia en kilómetros. En la Ecuación (1), \(e^{\beta_j}\) es la razón de odds (OR): cuántas veces cambian las odds de rotar ante un aumento de una unidad en la covariable (o al pasar de la categoría de referencia a la categoría indicada), manteniendo las demás constantes.
Para evaluar el poder predictivo de forma honesta, la base se divide aleatoriamente en un conjunto de entrenamiento (70 %), con el que se estima el modelo, y uno de prueba (30 %), que el modelo no “ve” durante la estimación y se usa para la evaluación. La partición es estratificada por la variable respuesta para conservar la misma proporción de rotación en ambos conjuntos.
set.seed(2026)
idx_train <- datos |> mutate(fila = row_number()) |>
group_by(y) |> slice_sample(prop = 0.7) |> pull(fila)
train <- datos[idx_train, ]
test <- datos[-idx_train, ]
tibble(Conjunto = c("Entrenamiento", "Prueba", "Total"),
Registros = c(nrow(train), nrow(test), nrow(datos)),
`Rotan` = c(sum(train$y), sum(test$y), sum(datos$y)),
`Tasa de rotación (%)` = 100 * c(mean(train$y), mean(test$y), mean(datos$y))) |>
tabla(caption = "Tamaño y tasa de rotación de los conjuntos de entrenamiento y prueba.", digits = 1)| Conjunto | Registros | Rotan | Tasa de rotación (%) |
|---|---|---|---|
| Entrenamiento | 1.028 | 165 | 16,1 |
| Prueba | 442 | 72 | 16,3 |
| Total | 1.470 | 237 | 16,1 |
modelo <- glm(y ~ Sat_Laboral + Estado_Civil + Viaje_Negocios +
Edad + Ingreso_Miles + Distancia_Casa,
family = binomial(link = "logit"), data = train)
summary(modelo)##
## Call:
## glm(formula = y ~ Sat_Laboral + Estado_Civil + Viaje_Negocios +
## Edad + Ingreso_Miles + Distancia_Casa, family = binomial(link = "logit"),
## data = train)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0,90352 0,55487 -1,628 0,103449
## Sat_LaboralInsatisfecho -0,35596 0,26773 -1,330 0,183664
## Sat_LaboralSatisfecho -0,45064 0,24140 -1,867 0,061936 .
## Sat_LaboralMuy satisfecho -1,07174 0,26177 -4,094 0,0000424 ***
## Estado_CivilDivorciado -0,42722 0,28290 -1,510 0,130998
## Estado_CivilSoltero 0,77156 0,19606 3,935 0,0000831 ***
## Viaje_NegociosRaramente 0,59777 0,34767 1,719 0,085548 .
## Viaje_NegociosFrecuentemente 1,28168 0,37655 3,404 0,000665 ***
## Edad -0,02908 0,01212 -2,400 0,016385 *
## Ingreso_Miles -0,08795 0,02957 -2,974 0,002940 **
## Distancia_Casa 0,03596 0,01070 3,361 0,000775 ***
## ---
## Signif. codes: 0 '***' 0,001 '**' 0,01 '*' 0,05 '.' 0,1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 905,68 on 1027 degrees of freedom
## Residual deviance: 800,54 on 1017 degrees of freedom
## AIC: 822,54
##
## Number of Fisher Scoring iterations: 5
coefs <- tidy(modelo, conf.int = TRUE) |>
transmute(Término = term, `β` = estimate, `Error est.` = std.error, `z` = statistic,
`Valor p` = pval(p.value),
Sig. = case_when(p.value < 0.001 ~ "***", p.value < 0.01 ~ "**",
p.value < 0.05 ~ "*", p.value < 0.1 ~ ".", TRUE ~ ""),
OR = exp(estimate),
`IC 95 % OR` = paste0("[", fmt(exp(conf.low), 3), " ; ", fmt(exp(conf.high), 3), "]"))
tabla(coefs, caption = "Coeficientes estimados del modelo logístico, razones de odds (OR) e intervalos de confianza al 95 %. Significancia: *** p < 0,001; ** p < 0,01; * p < 0,05; . p < 0,1.", digits = 3)| Término | β | Error est. | z | Valor p | Sig. | OR | IC 95 % OR |
|---|---|---|---|---|---|---|---|
| (Intercept) | -0,904 | 0,555 | -1,628 | 0,103 | 0,405 | [0,134 ; 1,185] | |
| Sat_LaboralInsatisfecho | -0,356 | 0,268 | -1,330 | 0,184 | 0,700 | [0,412 ; 1,181] | |
| Sat_LaboralSatisfecho | -0,451 | 0,241 | -1,867 | 0,062 | . | 0,637 | [0,397 ; 1,024] |
| Sat_LaboralMuy satisfecho | -1,072 | 0,262 | -4,094 | < 0,001 | *** | 0,342 | [0,204 ; 0,570] |
| Estado_CivilDivorciado | -0,427 | 0,283 | -1,510 | 0,131 | 0,652 | [0,366 ; 1,116] | |
| Estado_CivilSoltero | 0,772 | 0,196 | 3,935 | < 0,001 | *** | 2,163 | [1,475 ; 3,185] |
| Viaje_NegociosRaramente | 0,598 | 0,348 | 1,719 | 0,086 | . | 1,818 | [0,956 ; 3,779] |
| Viaje_NegociosFrecuentemente | 1,282 | 0,377 | 3,404 | < 0,001 | *** | 3,603 | [1,778 ; 7,866] |
| Edad | -0,029 | 0,012 | -2,400 | 0,016 |
|
0,971 | [0,948 ; 0,994] |
| Ingreso_Miles | -0,088 | 0,030 | -2,974 | 0,003 | ** | 0,916 | [0,862 ; 0,969] |
| Distancia_Casa | 0,036 | 0,011 | 3,361 | < 0,001 | *** | 1,037 | [1,015 ; 1,059] |
lr <- drop1(modelo, test = "LRT")
lr_tab <- tibble(Variable = rownames(lr)[-1], gl = lr$Df[-1],
`Desvianza al excluir` = lr$Deviance[-1], LRT = lr$LRT[-1],
`Valor p` = pval(lr$`Pr(>Chi)`[-1]))
tabla(lr_tab, caption = "Prueba de razón de verosimilitud (LRT) para la significancia conjunta de cada variable.", digits = 2)| Variable | gl | Desvianza al excluir | LRT | Valor p |
|---|---|---|---|---|
| Sat_Laboral | 3 | 818,70 | 18,16 | < 0,001 |
| Estado_Civil | 2 | 826,10 | 25,55 | < 0,001 |
| Viaje_Negocios | 2 | 816,34 | 15,79 | < 0,001 |
| Edad | 1 | 806,56 | 6,02 | 0,014 |
| Ingreso_Miles | 1 | 810,37 | 9,83 | 0,002 |
| Distancia_Casa | 1 | 811,57 | 11,03 | < 0,001 |
m_nulo <- glm(y ~ 1, family = binomial, data = train)
chi_g <- modelo$null.deviance - modelo$deviance
gl_g <- modelo$df.null - modelo$df.residual
p_g <- pchisq(chi_g, gl_g, lower.tail = FALSE)
r2_mcf <- 1 - as.numeric(logLik(modelo)) / as.numeric(logLik(m_nulo))v <- vif(modelo)
tabla(tibble(Variable = rownames(v), GVIF = v[, 1], gl = v[, 2], `GVIF^(1/(2·gl))` = v[, 3]),
caption = "Factor de inflación de varianza generalizado (diagnóstico de multicolinealidad).")| Variable | GVIF | gl | GVIF^(1/(2·gl)) |
|---|---|---|---|
| Sat_Laboral | 1,02 | 3 | 1,00 |
| Estado_Civil | 1,04 | 2 | 1,01 |
| Viaje_Negocios | 1,03 | 2 | 1,01 |
| Edad | 1,26 | 1 | 1,12 |
| Ingreso_Miles | 1,23 | 1 | 1,11 |
| Distancia_Casa | 1,03 | 1 | 1,01 |
tidy(modelo, conf.int = TRUE, exponentiate = TRUE) |>
filter(term != "(Intercept)") |>
mutate(Significativo = ifelse(p.value < 0.05, "p < 0,05", "p ≥ 0,05")) |>
ggplot(aes(x = estimate, y = reorder(term, estimate), color = Significativo)) +
geom_vline(xintercept = 1, linetype = "dashed") +
geom_errorbarh(aes(xmin = conf.low, xmax = conf.high), height = 0.2) +
geom_point(size = 2.8) +
scale_x_log10() +
scale_color_manual(values = c("p < 0,05" = "#b23a48", "p ≥ 0,05" = "#9aa5b1")) +
labs(x = "Razón de odds (OR)", y = NULL, color = NULL)Figura 8: Razones de odds e intervalos de confianza al 95 % del modelo logístico (escala logarítmica).
Significancia global. El modelo es globalmente significativo: la reducción de la devianza (medida de desajuste) respecto al modelo nulo es \(\chi^2\) = 105,1 con 10 grados de libertad (p < 0,001), es decir, las seis variables en conjunto explican la rotación mejor que un modelo sin covariables. El pseudo-R² de McFadden es 0,116; en regresión logística este indicador suele ser mucho menor que el R² de la regresión lineal, y valores entre 0,2 y 0,4 se consideran un ajuste muy bueno, por lo que el valor obtenido refleja un ajuste razonable para un modelo con solo seis variables. Los valores de GVIF (Tabla 17) son cercanos a 1, lo que descarta problemas de multicolinealidad.
Interpretación de cada coeficiente (Tabla 15 y Figura 8), manteniendo constantes las demás variables:
Los signos del modelo multivariado coinciden con los del análisis bivariado y con las hipótesis. Controlando por las demás variables, resultan significativos al 5 %: estar muy satisfecho, ser soltero, viajar frecuentemente, edad, ingreso mensual y distancia a la casa. No alcanzan significancia al 5 %: estar insatisfecho, estar satisfecho, ser divorciado y viajar raramente. Las magnitudes de edad e ingreso son menores que en el análisis simple, lo que indica que parte de su efecto marginal era compartido: ambas están correlacionadas (los empleados mayores también ganan más), de modo que al incluirlas juntas cada una pierde parte de la fuerza que mostraba por separado, aunque las dos siguen siendo significativas, lo que indica que cada una aporta información propia.
La curva ROC grafica la sensibilidad (proporción de empleados que rotan y que el modelo identifica correctamente) contra 1 − especificidad (proporción de empleados que no rotan y que el modelo marca erróneamente) para todos los posibles puntos de corte. El AUC (área bajo la curva) resume la capacidad de discriminación: es la probabilidad de que el modelo asigne mayor riesgo a un empleado que rota que a uno que no rota, elegidos al azar. Un AUC de 0,5 equivale a lanzar una moneda y uno de 1 a una clasificación perfecta.
train$prob <- predict(modelo, newdata = train, type = "response")
test$prob <- predict(modelo, newdata = test, type = "response")
roc_train <- roc(train$y, train$prob, levels = c(0, 1), direction = "<", quiet = TRUE)
roc_test <- roc(test$y, test$prob, levels = c(0, 1), direction = "<", quiet = TRUE)
ic_test <- ci.auc(roc_test)
auc_tab <- tibble(Conjunto = c("Entrenamiento", "Prueba"),
AUC = c(auc(roc_train), auc(roc_test)),
`IC 95 %` = c(paste0("[", fmt(ci.auc(roc_train)[1], 3), " ; ", fmt(ci.auc(roc_train)[3], 3), "]"),
paste0("[", fmt(ic_test[1], 3), " ; ", fmt(ic_test[3], 3), "]")))
tabla(auc_tab, caption = "Área bajo la curva ROC (AUC) en los conjuntos de entrenamiento y prueba.", digits = 3)| Conjunto | AUC | IC 95 % |
|---|---|---|
| Entrenamiento | 0,732 | [0,687 ; 0,776] |
| Prueba | 0,716 | [0,649 ; 0,782] |
ggroc(list(Entrenamiento = roc_train, Prueba = roc_test), legacy.axes = TRUE, linewidth = 1) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "grey50") +
annotate("text", x = 0.62, y = 0.15, hjust = 0,
label = paste0("AUC prueba = ", fmt(auc(roc_test), 3),
"\nAUC entrenamiento = ", fmt(auc(roc_train), 3))) +
scale_color_manual(values = c(Entrenamiento = "#9aa5b1", Prueba = "#1f3b57")) +
coord_equal() +
labs(x = "1 − Especificidad (tasa de falsos positivos)",
y = "Sensibilidad (tasa de verdaderos positivos)", color = NULL)Figura 9: Curvas ROC del modelo logístico en los conjuntos de entrenamiento y prueba.
Interpretación. El AUC en el conjunto de prueba es 0,716 (IC 95 %: 0,649 – 0,782), lo que indica una capacidad de discriminación aceptable según la escala de Hosmer y Lemeshow (0,7–0,8 aceptable; 0,8–0,9 excelente; > 0,9 sobresaliente). En otras palabras, si se toma al azar un empleado que rotó y otro que no, el modelo asigna mayor probabilidad de rotación al primero en el 72 % de los casos. El intervalo de confianza no incluye 0,5, de modo que el modelo es claramente mejor que el azar. La diferencia entre el AUC de entrenamiento (0,732) y el de prueba es de 0,016, lo que sugiere que no hay sobreajuste y que el modelo generaliza a empleados nuevos.
El AUC es moderado porque el modelo usa solo seis variables por exigencia de la actividad. En particular, no incluye las horas extra, que en el análisis bivariado fue la variable con mayor asociación con la rotación (Tabla 8); incorporarla en una versión ampliada mejoraría la discriminación.
Para convertir la probabilidad estimada en una decisión (intervenir o no) se necesita un punto de corte: si la probabilidad de un empleado es mayor o igual al corte, se clasifica como “rota” y se interviene. El corte convencional de 0,5 se conserva como referencia, pero no es adecuado aquí: como solo el 16,1 % de los empleados rota, el modelo rara vez asigna probabilidades superiores a 0,5 y dejaría sin detectar a la mayoría de quienes van a rotar.
Elegir el corte implica un balance entre dos errores:
Por esta asimetría se busca un corte que privilegie la sensibilidad (detectar a quienes rotan) sin sacrificar demasiado la especificidad. Se comparan cuatro criterios, todos calculados en el conjunto de entrenamiento y evaluados en el de prueba, para no elegir el corte con los mismos datos con que se evalúa:
clasif <- function(df, c) {
pred <- factor(ifelse(df$prob >= c, "Rota", "No rota"), levels = c("No rota", "Rota"))
real <- factor(ifelse(df$y == 1, "Rota", "No rota"), levels = c("No rota", "Rota"))
table(Predicho = pred, Real = real)
}
metricas <- function(tb) {
VP <- tb["Rota", "Rota"]; VN <- tb["No rota", "No rota"]
FP <- tb["Rota", "No rota"]; FN <- tb["No rota", "Rota"]
c(Exactitud = (VP + VN) / sum(tb), Sensibilidad = VP / (VP + FN),
Especificidad = VN / (VN + FP), `Valor predictivo positivo` = VP / (VP + FP),
Intervenidos = VP + FP, `Rotaciones detectadas` = VP, `Rotaciones no detectadas` = FN)
}
# Cortes candidatos (calculados con los datos de entrenamiento)
c_youden <- coords(roc_train, "best", best.method = "youden", ret = "threshold")[1, 1]
c_topleft <- coords(roc_train, "best", best.method = "closest.topleft", ret = "threshold")[1, 1]
todos <- coords(roc_train, "all", ret = c("threshold", "sensitivity", "specificity"))
todos <- todos[is.finite(todos$threshold), ]
c_equil <- todos$threshold[which.min(abs(todos$sensitivity - todos$specificity))]
cortes <- c("Convencional (0,5)" = 0.5,
"Índice de Youden" = c_youden,
"Más cercano a (0, 1)" = c_topleft,
"Equilibrio sens. = esp." = c_equil)
eval_cortes <- function(df) {
t(sapply(cortes, function(c) metricas(clasif(df, c))))
}
comp_train <- eval_cortes(train)
comp <- eval_cortes(test)
tabla(data.frame(Criterio = names(cortes), Corte = cortes, comp_train[, 2:4],
check.names = FALSE, row.names = NULL),
caption = "Cortes candidatos y su desempeño en el conjunto de entrenamiento (donde se eligen).", digits = 3)| Criterio | Corte | Sensibilidad | Especificidad | Valor predictivo positivo |
|---|---|---|---|---|
| Convencional (0,5) | 0,500 | 0,073 | 0,995 | 0,750 |
| Índice de Youden | 0,232 | 0,570 | 0,823 | 0,381 |
| Más cercano a (0, 1) | 0,185 | 0,642 | 0,723 | 0,307 |
| Equilibrio sens. = esp. | 0,161 | 0,661 | 0,662 | 0,272 |
curva <- data.frame(corte = seq(0.02, 0.7, by = 0.005)) |>
rowwise() |>
mutate(m = list(metricas(clasif(train, corte)))) |>
mutate(Sensibilidad = m[["Sensibilidad"]], Especificidad = m[["Especificidad"]],
`Valor predictivo positivo` = m[["Valor predictivo positivo"]]) |>
ungroup() |> select(-m) |>
pivot_longer(-corte, names_to = "Métrica", values_to = "Valor")
ref <- data.frame(Criterio = factor(names(cortes), levels = names(cortes)), corte = cortes)
ggplot(curva, aes(corte, Valor, color = Métrica)) +
geom_line(linewidth = 1, na.rm = TRUE) +
geom_vline(data = ref, aes(xintercept = corte, linetype = Criterio), color = "grey30") +
scale_color_manual(values = c(Sensibilidad = "#b23a48", Especificidad = "#1f3b57",
`Valor predictivo positivo` = "#d9a441")) +
scale_linetype_manual(values = c("solid", "dashed", "dotdash", "dotted")) +
scale_y_continuous(labels = percent, limits = c(0, 1)) +
labs(x = "Punto de corte", y = NULL, color = NULL, linetype = NULL) +
guides(color = guide_legend(nrow = 1, order = 1), linetype = guide_legend(nrow = 1, order = 2)) +
theme(legend.box = "vertical", legend.spacing.y = unit(2, "pt"))Figura 10: Sensibilidad, especificidad y valor predictivo positivo según el punto de corte (conjunto de entrenamiento). Las líneas verticales señalan los cortes candidatos.
tabla(data.frame(Criterio = names(cortes), Corte = cortes, comp,
check.names = FALSE, row.names = NULL),
caption = paste0("Desempeño de los cortes candidatos en el conjunto de prueba (", nrow(test),
" empleados, de los cuales ", sum(test$y), " rotaron)."), digits = 3) |>
row_spec(4, bold = TRUE, background = "#eef3f7")| Criterio | Corte | Exactitud | Sensibilidad | Especificidad | Valor predictivo positivo | Intervenidos | Rotaciones detectadas | Rotaciones no detectadas |
|---|---|---|---|---|---|---|---|---|
| Convencional (0,5) | 0,500 | 0,848 | 0,097 | 0,995 | 0,778 | 9 | 7 | 65 |
| Índice de Youden | 0,232 | 0,760 | 0,486 | 0,814 | 0,337 | 104 | 35 | 37 |
| Más cercano a (0, 1) | 0,185 | 0,681 | 0,611 | 0,695 | 0,280 | 157 | 44 | 28 |
| Equilibrio sens. = esp. | 0,161 | 0,627 | 0,639 | 0,624 | 0,249 | 185 | 46 | 26 |
pts <- data.frame(Criterio = factor(names(cortes), levels = names(cortes)),
fpr = 1 - comp[, "Especificidad"], tpr = comp[, "Sensibilidad"])
ggroc(roc_test, legacy.axes = TRUE, linewidth = 1, color = "#1f3b57") +
geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "grey50") +
geom_point(data = pts, aes(x = fpr, y = tpr, shape = Criterio, fill = Criterio),
inherit.aes = FALSE, size = 3.5) +
scale_shape_manual(values = c(21, 22, 24, 23)) +
scale_fill_manual(values = c("#9aa5b1", "#d9a441", "#5b8db8", "#b23a48")) +
coord_equal() +
labs(x = "1 − Especificidad", y = "Sensibilidad", shape = NULL, fill = NULL) +
guides(shape = guide_legend(nrow = 2), fill = guide_legend(nrow = 2))Figura 11: Ubicación de los cortes candidatos sobre la curva ROC del conjunto de prueba.
c_opt <- c_equil
mc_05 <- as.data.frame.matrix(clasif(test, 0.5))
mc_opt <- as.data.frame.matrix(clasif(test, c_opt))
tabla(cbind(`Predicho \\ Real` = rownames(mc_05), mc_05, mc_opt),
caption = paste0("Matrices de confusión en el conjunto de prueba con el corte de 0,5 y con el corte óptimo (",
fmt(c_opt, 3), ")."), digits = 0, row.names = FALSE) |>
add_header_above(c(" " = 1, "Corte 0,5" = 2, "Corte óptimo" = 2))| Predicho Real | No rota | Rota | No rota | Rota |
|---|---|---|---|---|
| No rota | 368 | 65 | 231 | 26 |
| Rota | 2 | 7 | 139 | 46 |
Interpretación. La Figura 10 muestra el balance entre los dos errores: al bajar el corte, el modelo detecta a más empleados que rotan (sube la sensibilidad), pero también marca a más empleados que no iban a rotar (baja la especificidad). Con el corte de 0,5 el modelo casi nunca predice rotación: en el conjunto de prueba detecta solo el 9,7 % de quienes rotaron (7 de 72), aunque su exactitud sea alta (84,8 %); esa exactitud es engañosa, porque se logra clasificando a casi todos como “no rota” (Tabla 20).
Los cortes alternativos mejoran mucho la detección. El índice de Youden (0,232) detecta el 48,6 %, el punto más cercano a (0, 1) (0,185) el 61,1 %, y el corte de equilibrio (0,161) el 63,9 %, con una especificidad de 62,4 %.
Se elige como corte óptimo 0,161, correspondiente al equilibrio entre sensibilidad y especificidad, por tres razones:
El precio de esta mayor detección es un valor predictivo positivo bajo (24,9 %): de cada 10 empleados intervenidos, unos 2,5 habrían rotado efectivamente. Dado el bajo costo de una intervención preventiva frente al costo de reemplazar a un empleado, este balance es razonable. Si la empresa tuviera recursos limitados para intervenir, podría usar el corte de Youden, que interviene a menos personas (104) a cambio de detectar menos rotaciones.
Regla de decisión: si la probabilidad estimada de rotación de un empleado es mayor o igual a 0,161, se recomienda intervenir; si es menor, se mantiene el seguimiento habitual. Como referencia, con el corte convencional de 0,5 solo se intervendría a quienes tienen una probabilidad muy alta de rotar.
Se calcula la probabilidad de rotación de dos empleados hipotéticos con perfiles contrastantes.
nuevos <- data.frame(
Perfil = c("Empleado A", "Empleado B"),
Sat_Laboral = factor(c("Muy insatisfecho", "Muy satisfecho"), levels = lik4),
Estado_Civil = factor(c("Soltero", "Casado"), levels = levels(datos$Estado_Civil)),
Viaje_Negocios = factor(c("Frecuentemente", "Raramente"), levels = levels(datos$Viaje_Negocios)),
Edad = c(27, 42),
Ingreso_Mensual = c(2800, 7500),
Distancia_Casa = c(20, 3)
) |> mutate(Ingreso_Miles = Ingreso_Mensual / 1000)
pr <- predict(modelo, newdata = nuevos, type = "link", se.fit = TRUE)
nuevos <- nuevos |>
mutate(Probabilidad = plogis(pr$fit),
`IC 95 %` = paste0("[", fmt(plogis(pr$fit - 1.96 * pr$se.fit), 3), " ; ",
fmt(plogis(pr$fit + 1.96 * pr$se.fit), 3), "]"),
`Decisión (corte 0,5)` = ifelse(Probabilidad >= 0.5, "Intervenir", "No intervenir"),
`Decisión (corte óptimo)` = ifelse(Probabilidad >= c_opt, "Intervenir", "No intervenir"))
tabla(nuevos |> select(-Ingreso_Miles),
caption = paste0("Probabilidad estimada de rotación para dos empleados hipotéticos (corte = ", fmt(c_opt, 3), ")."), digits = 3)| Perfil | Sat_Laboral | Estado_Civil | Viaje_Negocios | Edad | Ingreso_Mensual | Distancia_Casa | Probabilidad | IC 95 % | Decisión (corte 0,5) | Decisión (corte óptimo) |
|---|---|---|---|---|---|---|---|---|---|---|
| Empleado A | Muy insatisfecho | Soltero | Frecuentemente | 27 | 2.800 | 20 | 0,698 | [0,559 ; 0,808] | Intervenir | Intervenir |
| Empleado B | Muy satisfecho | Casado | Raramente | 42 | 7.500 | 3 | 0,041 | [0,025 ; 0,068] | No intervenir | No intervenir |
Interpretación. El empleado A —27 años, soltero, muy insatisfecho con su trabajo, viaja frecuentemente, gana 2.800 y vive a 20 km de la empresa— tiene una probabilidad estimada de rotar de 69,8 %, muy por encima del corte de 0,161, por lo que se recomienda intervenir de inmediato. La intervención debería atacar los factores modificables de su perfil: una conversación con su jefe para identificar las causas de su insatisfacción (funciones, reconocimiento, plan de carrera), reducir o compensar los viajes, revisar su salario frente al mercado y ofrecerle flexibilidad de horario o días de teletrabajo para aliviar el desplazamiento.
El empleado B —42 años, casado, muy satisfecho, viaja raramente, gana 7.500 y vive a 3 km— tiene una probabilidad de 4,1 %, por debajo del corte, de modo que no requiere intervención y basta el seguimiento habitual.
Situación de la empresa. El 16,1 % de los empleados rotó de cargo. La base de datos es de buena calidad (sin faltantes, duplicados ni inconsistencias), y los valores atípicos encontrados corresponden a casos reales, por lo que se conservaron.
Factores determinantes. En el análisis bivariado las seis variables seleccionadas se asocian significativamente con la rotación, con los signos planteados en las hipótesis. En el modelo multivariado, que controla por todas a la vez, se mantienen como significativos al 5 % . Los factores que aumentan el riesgo son viajar frecuentemente (OR ≈ 3,60 frente a no viajar), ser soltero (OR ≈ 2,16 frente a casado) y vivir lejos del trabajo (cada 10 km aumentan las odds en un 43,3 %). Los factores que lo reducen son una alta satisfacción laboral (los muy satisfechos tienen odds 65,8 % menores que los muy insatisfechos), una mayor edad y un mayor ingreso. El perfil de mayor riesgo es el de un empleado joven, soltero, insatisfecho con su trabajo, con salario bajo, que vive lejos y viaja con frecuencia.
Poder predictivo. El modelo tiene una capacidad de discriminación aceptable (AUC = 0,716 en datos no usados para estimarlo) y no muestra sobreajuste. Con el corte de 0,161 permite identificar a cerca del 64 % de los empleados que rotarán, lo que lo hace útil como herramienta de alerta temprana.
Estrategia para disminuir la rotación. Con base en las variables significativas, se recomienda a la gerencia:
Limitaciones y trabajo futuro. El modelo se restringe a seis variables por diseño de la actividad. Variables como las horas extra (la de mayor asociación bivariada), la antigüedad en el cargo o la satisfacción ambiental, que también mostraron asociación con la rotación, podrían incorporarse en una versión ampliada para mejorar la predicción. Además, al tratarse de datos observacionales, los resultados indican asociación y no necesariamente causalidad; las intervenciones propuestas deberían evaluarse midiendo su efecto en la rotación posterior.
Hosmer, D. W., Lemeshow, S. y Sturdivant, R. X. (2013). Applied Logistic Regression (3.ª ed.). Wiley.
James, G., Witten, D., Hastie, T. y Tibshirani, R. (2021). An Introduction to Statistical Learning with Applications in R (2.ª ed.). Springer.
Weiers, R. M. (2006). Introducción a la estadística para negocios (5.ª ed.). Thomson.
Xie, Y., Allaire, J. J. y Grolemund, G. (2018). R Markdown: The Definitive Guide. Chapman & Hall/CRC.