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

1 Introducción

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

2 Selección de variables e hipótesis

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

3 Análisis exploratorio y preparación de los datos

3.1 Carga y estructura de la base

data("rotacion")
datos_crudos <- as.data.frame(rotacion)
dim(datos_crudos)
## [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_nuevos

3.2 Clasificación de las variables

Con 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`.")
Tabla 1: 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

3.3 Calidad de los datos: faltantes, duplicados y consistencia

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)
Tabla 2: Resumen de la calidad de la base de datos.
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)
Tabla 3: Rango observado de las variables numéricas (control de consistencia).
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:

  • Rendimiento laboral solo toma los valores 3 y 4 (alto y muy alto), aunque el diccionario define cuatro niveles. Su poca variabilidad limita su capacidad para explicar la rotación.
  • Se verifica la coherencia temporal entre variables: la antigüedad en el cargo no debe superar la antigüedad en la empresa, ni esta los años de experiencia.
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)
Tabla 4: Reglas de coherencia lógica entre variables temporales.
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

3.4 Transformación y codificación

Para el análisis se realizan las siguientes transformaciones:

  1. Variable respuesta: se crea 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.
  2. Variables nominales: se convierten a factor y se fija una categoría de referencia con sentido interpretativo: Casado en estado civil y No_Viaja en viajes de negocios.
  3. Variables ordinales: se convierten en factores ordenados con las etiquetas del diccionario de datos. Para el modelo se crea además 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).
  4. Escala del ingreso: se crea 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, …

4 Análisis univariado

4.1 Variable respuesta: rotación

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)
Tabla 5: Distribución de la variable respuesta Rotación.
Rotacion Frecuencia Porcentaje (%)
No 1.233 83,9
Si 237 16,1
p_rot <- mean(datos$y)
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")
Distribución de la rotación de cargo.

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

4.2 Variables cuantitativas

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).")
Tabla 6: 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")
Histogramas de las variables cuantitativas.

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)")
Diagramas de caja de las variables cuantitativas (valores estandarizados).

Figura 3: Diagramas de caja de las variables cuantitativas (valores estandarizados).

Interpretación. La Tabla 6 y las Figuras 2 y 3 muestran que:

  • La edad promedio es de 36,9 años (DE = 9,1), con una distribución aproximadamente simétrica y sin valores atípicos; la mitad de los empleados tiene entre 30 y 43 años.
  • El ingreso mensual presenta asimetría positiva marcada (asimetría = 1,37): la media (6.503) supera ampliamente a la mediana (4.919) por la presencia de un grupo reducido de salarios altos, propios de cargos directivos. Su coeficiente de variación (72,4 %) indica alta heterogeneidad salarial.
  • Las variables de tiempo (antigüedad, antigüedad en el cargo, años con el mismo jefe, años desde la última promoción y experiencia) son asimétricas a la derecha: la mayoría de los empleados tiene pocos años en la empresa y en el cargo, y unos pocos tienen trayectorias muy largas. La antigüedad en el cargo tiene una mediana de 3 años.
  • La distancia a la casa también es asimétrica: la mitad de los empleados vive a 7 km o menos.

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.

4.3 Variables cualitativas

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")
Tabla 7: Distribución de frecuencias de las variables cualitativas.
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)
Distribución porcentual de las variables cualitativas.

Figura 4: Distribución porcentual de las variables cualitativas.

Interpretación. De la Tabla 7 y la Figura 4 se destaca que:

  • En satisfacción laboral, el 38,7 % de los empleados se declara insatisfecho o muy insatisfecho (19,7 % muy insatisfecho), una proporción considerable que señala un frente de mejora; el 31,2 % se declara muy satisfecho.
  • El 28,3 % de los empleados trabaja horas extra.
  • Por estado civil, predominan los casados (45,8 %), seguidos por solteros (32,0 %) y divorciados (22,2 %).
  • En viajes de negocios, la mayoría viaja raramente (71,0 %); el 18,8 % viaja con frecuencia.
  • La empresa está concentrada en el departamento de Investigación y Desarrollo (65,4 %) y en cargos de ventas e investigación. Hay más hombres (60,0 %) que mujeres.
  • La satisfacción ambiental muestra un patrón similar: el 38,8 % de los empleados está insatisfecho o muy insatisfecho con el ambiente de trabajo.
  • Algunas categorías son poco frecuentes (por ejemplo, Humanidades en campo de educación o RH en departamento); esto se tiene en cuenta al interpretar las tasas de rotación de esos grupos, que se calculan sobre pocos casos.

4.4 Correlación entre variables cuantitativas

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))
Matriz de correlación de Pearson entre las variables cuantitativas.

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.

5 Análisis bivariado

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.

5.1 Todas las variables frente a la rotación

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.")
Tabla 8: 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)
Tabla 9: Regresión logística simple de la rotación sobre cada variable cuantitativa.
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 %.

5.2 Variables seleccionadas

5.2.1 Variables categóricas

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")
Tabla 10: Tasa de rotación según las variables categóricas seleccionadas.
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))
Proporción de empleados que rotan según las variables categóricas seleccionadas. La línea punteada indica la tasa global de rotación.

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)
Tabla 11: Regresión logística simple para las variables categóricas seleccionadas (categorías de referencia: Muy insatisfecho, Casado y No_Viaja).
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

5.2.2 Variables cuantitativas

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)")
Distribución de las variables cuantitativas seleccionadas según la rotación.

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)
Tabla 12: Regresión logística simple para las variables cuantitativas seleccionadas (ingreso en miles).
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

5.3 Contraste con las hipótesis

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.")
Tabla 13: 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:

  • Satisfacción laboral (H1, −): la tasa de rotación es de 22,8 % entre los muy insatisfechos, 16,4 % entre los insatisfechos, 16,5 % entre los satisfechos y 11,3 % entre los muy satisfechos. La relación es decreciente, aunque no estrictamente: los dos niveles intermedios tienen tasas casi iguales, y la diferencia más marcada está entre los extremos. Las odds de rotar de un empleado muy satisfecho son 0,43 veces las de uno muy insatisfecho, es decir, 56,8 % menores.
  • Estado civil (H2, +): los solteros rotan en un 25,5 %, aproximadamente el doble que los casados (12,5 %). Los divorciados (10,1 %) no se diferencian significativamente de los casados.
  • Viajes de negocios (H3, +): la tasa de rotación crece con la frecuencia de viaje: 8,0 % en quienes no viajan, 15,0 % en quienes viajan raramente y 24,9 % en quienes viajan con frecuencia, un patrón de tipo dosis–respuesta.
  • Edad (H4, −): los empleados que rotan son en promedio más jóvenes (33,6 vs. 37,6 años). Cada año adicional de edad reduce las odds de rotar en un 5,1 %.
  • Ingreso (H5, −): quienes rotan ganan en promedio 4.787, frente a 6.833 de quienes permanecen. Cada mil unidades adicionales de ingreso reducen las odds de rotar en un 11,9 %.
  • Distancia a la casa (H6, +): quienes rotan viven en promedio a 10,6 km, frente a 8,9 km de quienes permanecen. Cada kilómetro adicional aumenta las odds de rotar en un 2,5 %; diez kilómetros más las aumentan en un 28,0 %.

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.

6 Estimación del modelo

6.1 Especificación

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.

6.2 Partición en entrenamiento y prueba

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)
Tabla 14: Tamaño y tasa de rotación de los conjuntos de entrenamiento y prueba.
Conjunto Registros Rotan Tasa de rotación (%)
Entrenamiento 1.028 165 16,1
Prueba 442 72 16,3
Total 1.470 237 16,1

6.3 Estimación

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)
Tabla 15: 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.
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)
Tabla 16: Prueba de razón de verosimilitud (LRT) para la significancia conjunta de cada variable.
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).")
Tabla 17: 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)
Razones de odds e intervalos de confianza al 95 % del modelo logístico (escala logarítmica).

Figura 8: Razones de odds e intervalos de confianza al 95 % del modelo logístico (escala logarítmica).

6.4 Interpretación de los coeficientes y su significancia

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:

  • Satisfacción laboral (referencia: Muy insatisfecho): los tres coeficientes son negativos, es decir, cualquier nivel de satisfacción superior reduce el riesgo frente a estar muy insatisfecho. El efecto es claro para los empleados muy satisfechos (β = -1,072, p < 0,001, estadísticamente significativo al 5 %): sus odds de rotar son 0,34 veces las de los muy insatisfechos, es decir, un 65,8 % menores. Para los niveles Satisfecho (OR = 0,64, p = 0,062) e Insatisfecho (OR = 0,70, p = 0,184) el efecto va en la misma dirección pero es significativo solo al 10 % (evidencia débil) y no significativo, respectivamente. La prueba LRT (Tabla 16) confirma que la satisfacción laboral, como variable, es significativa en conjunto.
  • Estado civil: ser soltero (β = 0,772, p < 0,001) multiplica las odds de rotar por 2,16 frente a ser casado. Ser divorciado (p = 0,131) no difiere significativamente de ser casado. La prueba LRT confirma que el estado civil, como variable, es significativo.
  • Viajes de negocios: quienes viajan frecuentemente (β = 1,282, p < 0,001) tienen odds 3,60 veces mayores que quienes no viajan. Viajar raramente (OR = 1,82, p = 0,086) también aumenta el riesgo, aunque sin alcanzar significancia al 5 %.
  • Edad (β = -0,0291, p = 0,016): signo negativo, estadísticamente significativo al 5 %. Cada año adicional de edad reduce las odds de rotar en un 2,9 %; por ejemplo, diez años más de edad las reducen en un 25,2 %.
  • Ingreso mensual (β = -0,0879 por cada mil, p = 0,003): signo negativo, estadísticamente significativo al 5 %. Cada mil unidades adicionales de salario reducen las odds de rotar en un 8,4 %.
  • Distancia a la casa (β = 0,0360, p < 0,001): signo positivo, estadísticamente significativo al 5 %. Cada kilómetro adicional aumenta las odds de rotar en un 3,7 %; un empleado que vive 10 km más lejos que otro, con las demás características iguales, tiene odds de rotar un 43,3 % mayores.
  • Intercepto (β = -0,904): corresponde al log-odds de un empleado de referencia (muy insatisfecho, casado, que no viaja) con edad, ingreso y distancia iguales a cero; no tiene interpretación práctica directa, pero es necesario para calcular las probabilidades.

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.

7 Evaluación del poder predictivo

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)
Tabla 18: Área bajo la curva ROC (AUC) en los conjuntos de entrenamiento y prueba.
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)
Curvas ROC del modelo logístico en los conjuntos de entrenamiento y prueba.

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.

8 Predicción y punto de corte

8.1 Búsqueda del punto de corte óptimo

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:

  • Falso negativo: no intervenir a un empleado que sí rota. Es el error más costoso para la empresa, porque implica perder talento y asumir los costos de reemplazo (reclutamiento, selección, inducción y curva de aprendizaje).
  • Falso positivo: intervenir a un empleado que no iba a rotar. Su costo es bajo: una conversación de seguimiento, un plan de carrera o un ajuste de condiciones que, además, puede mejorar la satisfacción de ese empleado.

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:

  1. Corte convencional (0,5).
  2. Índice de Youden: maximiza sensibilidad + especificidad − 1.
  3. Punto más cercano a (0, 1): el punto de la curva ROC más cercano a la esquina superior izquierda, que representa una clasificación perfecta.
  4. Equilibrio sensibilidad = especificidad: el corte en el que el modelo acierta en la misma proporción con quienes rotan y con quienes no rotan.
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)
Tabla 19: Cortes candidatos y su desempeño en el conjunto de entrenamiento (donde se eligen).
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"))
Sensibilidad, especificidad y valor predictivo positivo según el punto de corte (conjunto de entrenamiento). Las líneas verticales señalan los cortes candidatos.

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")
Tabla 20: Desempeño de los cortes candidatos en el conjunto de prueba (442 empleados, de los cuales 72 rotaron).
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))
Ubicación de los cortes candidatos sobre la curva ROC del conjunto de prueba.

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))
Tabla 21: Matrices de confusión en el conjunto de prueba con el corte de 0,5 y con el corte óptimo (0,161).
Corte 0,5
Corte óptimo
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:

  1. Ofrece la mayor sensibilidad entre los criterios evaluados (63,9 % en el conjunto de prueba, es decir, 46 de 72 rotaciones detectadas, frente a 35 con Youden y 7 con 0,5; Tabla 21) sin que la especificidad caiga a niveles que impliquen intervenir a buena parte de la planta. Esto es coherente con la asimetría de costos: no detectar una rotación es más costoso que intervenir de más.
  2. Mantiene una especificidad aceptable (62,4 %): no interviene indiscriminadamente a toda la planta, sino a 185 de los 442 empleados del conjunto de prueba (41,9 %).
  3. Es un corte estable: su valor coincide prácticamente con la tasa de rotación de la empresa (16,1 %), un criterio habitual cuando la clase de interés es minoritaria, y su desempeño en prueba es muy similar al de entrenamiento (Tablas 19 y 20), lo que indica que no está sobreajustado.

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.

8.2 Predicción para empleados hipotéticos

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)
Tabla 22: Probabilidad estimada de rotación para dos empleados hipotéticos (corte = 0,161).
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.

9 Conclusiones

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

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

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

  4. Estrategia para disminuir la rotación. Con base en las variables significativas, se recomienda a la gerencia:

    • Mejorar la satisfacción laboral: aplicar encuestas periódicas de clima y satisfacción, actuar sobre sus resultados (reconocimiento, claridad de funciones, liderazgo de los jefes, oportunidades de crecimiento) y hacer seguimiento individual a quienes se declaran insatisfechos, que en esta empresa son una proporción considerable.
    • Racionalizar los viajes de negocios: sustituir viajes por reuniones virtuales cuando sea posible, rotar los viajes entre los miembros del equipo y ofrecer días de descanso compensatorio tras viajes largos.
    • Revisar la política salarial: hacer estudios de equidad interna y competitividad externa, priorizando los rangos salariales bajos, donde la rotación es mayor; complementar con incentivos por permanencia.
    • Reducir el costo del desplazamiento: ofrecer modalidades de trabajo híbrido o teletrabajo, horarios flexibles que eviten las horas pico, auxilios o rutas de transporte, y considerar la cercanía a la vivienda en los procesos de selección y traslado.
    • Atender al talento joven y soltero: ofrecer oportunidades de crecimiento, formación y movilidad interna, de modo que el deseo de nuevos retos se canalice dentro de la empresa y no fuera de ella.
    • Implementar un sistema de alerta temprana: aplicar periódicamente el modelo a toda la planta, marcar a los empleados con probabilidad ≥ 0,161 y asignarles un plan de intervención individual con su jefe y recursos humanos.
  5. 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.

Referencias

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.