Introducción

Este informe presenta un análisis de la rotación de empleados en una organización, con el objetivo de estimar la probabilidad de rotación e identificar los factores que la determinan. Se trabaja con una base de datos que incluye variables demográficas, laborales y de percepción (por ejemplo: edad, ingreso, antigüedad, horas extra, satisfacción laboral y equilibrio trabajo–vida). Sobre esta información se realizan procesos de limpieza, análisis univariado y bivariado, y la estimación de un modelo de regresión logística. El desempeño del modelo se evalúa con ROC y AUC y se proponen umbrales de decisión para priorizar intervenciones de retención sobre casos con mayor riesgo.

# Instalar y cargar el paquete
if (!require("paqueteMODELOS")) {
  remotes::install_github("centromagis/paqueteMODELOS")
}

# Cargar datos
data("rotacion")

Preprocesamiento de los datos

Para la preparación y limpieza de la base de datos hacemos una inspección inicial de la estructura del dataset.

# Tabla con estructura de los datos
Estructura <- data.frame(
  Variable = names(rotacion),
  Tipo = sapply(rotacion, function(x) class(x)[1]),
  Ejemplo = sapply(rotacion, function(x) paste(utils::head(x, 3), collapse = ", "))
)

knitr::kable(Estructura, caption = "Estructura del dataset original")
Estructura del dataset original
Variable Tipo Ejemplo
Rotación Rotación character Si, No, Si
Edad Edad numeric 41, 49, 37
Viaje de Negocios Viaje de Negocios character Raramente, Frecuentemente, Raramente
Departamento Departamento character Ventas, IyD, IyD
Distancia_Casa Distancia_Casa numeric 1, 8, 2
Educación Educación numeric 2, 1, 2
Campo_Educación Campo_Educación character Ciencias, Ciencias, Otra
Satisfacción_Ambiental Satisfacción_Ambiental numeric 2, 3, 4
Genero Genero character F, M, M
Cargo Cargo character Ejecutivo_Ventas, Investigador_Cientifico, Tecnico_Laboratorio
Satisfación_Laboral Satisfación_Laboral numeric 4, 2, 3
Estado_Civil Estado_Civil character Soltero, Casado, Soltero
Ingreso_Mensual Ingreso_Mensual numeric 5993, 5130, 2090
Trabajos_Anteriores Trabajos_Anteriores numeric 8, 1, 6
Horas_Extra Horas_Extra character Si, No, Si
Porcentaje_aumento_salarial Porcentaje_aumento_salarial numeric 11, 23, 15
Rendimiento_Laboral Rendimiento_Laboral numeric 3, 4, 3
Años_Experiencia Años_Experiencia numeric 8, 10, 7
Capacitaciones Capacitaciones numeric 0, 3, 3
Equilibrio_Trabajo_Vida Equilibrio_Trabajo_Vida numeric 1, 3, 3
Antigüedad Antigüedad numeric 6, 10, 0
Antigüedad_Cargo Antigüedad_Cargo numeric 4, 7, 0
Años_ultima_promoción Años_ultima_promoción numeric 0, 1, 0
Años_acargo_con_mismo_jefe Años_acargo_con_mismo_jefe numeric 5, 7, 0

Se realiza una tipificación de las variables del dataset, es decir, se convierte cada columna al tipo de dato adecuado (numéricas, categóricas nominales, categóricas ordinales).

# Variables por tipo
ord_vars  <- c("Satisfacción_Ambiental","Satisfación_Laboral",
               "Equilibrio_Trabajo_Vida","Rendimiento_Laboral","Educación")

cat_vars  <- c("Viaje de Negocios","Departamento","Campo_Educación",
               "Genero","Cargo","Estado_Civil","Horas_Extra")

num_vars <- c("Ingreso_Mensual","Distancia_Casa","Edad","Años_Experiencia",
               "Antigüedad","Antigüedad_Cargo","Años_acargo_con_mismo_jefe",
               "Porcentaje_aumento_salarial","Capacitaciones",
               "Trabajos_Anteriores","Años_ultima_promoción")
 
# Tipificatr las variables
rotacion <- rotacion %>%
  mutate(
    Rotación = factor(ifelse(Rotación == "Si", 1, 0),
                      levels = c(0,1)),
    # nominales
    across(all_of(intersect(cat_vars, names(.))), ~factor(.)),
    # ordinales
    across(all_of(intersect(ord_vars, names(.))),
           ~factor(., levels = sort(unique(.)), ordered = TRUE)),
    # numéricas
    across(all_of(intersect(num_vars, names(.))), ~as.numeric(.))
  )

# Tabla con estructura de los datos
Estructura2 <- data.frame(
  Variable = names(rotacion),
  Tipo = sapply(rotacion, function(x) class(x)[1]),
  Ejemplo = sapply(rotacion, function(x) paste(utils::head(x, 3), collapse = ", "))
)

knitr::kable(Estructura2, caption = "Estructura del dataset tipificado")
Estructura del dataset tipificado
Variable Tipo Ejemplo
Rotación Rotación factor 1, 0, 1
Edad Edad numeric 41, 49, 37
Viaje de Negocios Viaje de Negocios factor Raramente, Frecuentemente, Raramente
Departamento Departamento factor Ventas, IyD, IyD
Distancia_Casa Distancia_Casa numeric 1, 8, 2
Educación Educación ordered 2, 1, 2
Campo_Educación Campo_Educación factor Ciencias, Ciencias, Otra
Satisfacción_Ambiental Satisfacción_Ambiental ordered 2, 3, 4
Genero Genero factor F, M, M
Cargo Cargo factor Ejecutivo_Ventas, Investigador_Cientifico, Tecnico_Laboratorio
Satisfación_Laboral Satisfación_Laboral ordered 4, 2, 3
Estado_Civil Estado_Civil factor Soltero, Casado, Soltero
Ingreso_Mensual Ingreso_Mensual numeric 5993, 5130, 2090
Trabajos_Anteriores Trabajos_Anteriores numeric 8, 1, 6
Horas_Extra Horas_Extra factor Si, No, Si
Porcentaje_aumento_salarial Porcentaje_aumento_salarial numeric 11, 23, 15
Rendimiento_Laboral Rendimiento_Laboral ordered 3, 4, 3
Años_Experiencia Años_Experiencia numeric 8, 10, 7
Capacitaciones Capacitaciones numeric 0, 3, 3
Equilibrio_Trabajo_Vida Equilibrio_Trabajo_Vida ordered 1, 3, 3
Antigüedad Antigüedad numeric 6, 10, 0
Antigüedad_Cargo Antigüedad_Cargo numeric 4, 7, 0
Años_ultima_promoción Años_ultima_promoción numeric 0, 1, 0
Años_acargo_con_mismo_jefe Años_acargo_con_mismo_jefe numeric 5, 7, 0

Seguido, verificamos la existencia de duplicados, faltantes y outliers en el dataset:

# Identificar los duplicados
num_duplicados <- sum(duplicated(rotacion))
cat("Número de duplicados:", num_duplicados)
## Número de duplicados: 0
# Conteo y % de NAs por columna
na_tbl <- rotacion %>%
  dplyr::summarise(dplyr::across(dplyr::everything(), ~ sum(is.na(.)))) %>%
  tidyr::pivot_longer(dplyr::everything(), names_to = "Variable", values_to = "NA_n") %>%
  dplyr::mutate(Total = nrow(rotacion), NA_pct = round(100 * NA_n / Total, 2)) %>%
  dplyr::arrange(dplyr::desc(NA_pct))

knitr::kable(na_tbl, caption = "Valores faltantes por variable")
Valores faltantes por variable
Variable NA_n Total NA_pct
Rotación 0 1470 0
Edad 0 1470 0
Viaje de Negocios 0 1470 0
Departamento 0 1470 0
Distancia_Casa 0 1470 0
Educación 0 1470 0
Campo_Educación 0 1470 0
Satisfacción_Ambiental 0 1470 0
Genero 0 1470 0
Cargo 0 1470 0
Satisfación_Laboral 0 1470 0
Estado_Civil 0 1470 0
Ingreso_Mensual 0 1470 0
Trabajos_Anteriores 0 1470 0
Horas_Extra 0 1470 0
Porcentaje_aumento_salarial 0 1470 0
Rendimiento_Laboral 0 1470 0
Años_Experiencia 0 1470 0
Capacitaciones 0 1470 0
Equilibrio_Trabajo_Vida 0 1470 0
Antigüedad 0 1470 0
Antigüedad_Cargo 0 1470 0
Años_ultima_promoción 0 1470 0
Años_acargo_con_mismo_jefe 0 1470 0
# Función para detectar outliers
detectar_outliers_tabla <- function(df, vars) {
  resumen <- data.frame(Variable = character(), N_outliers = integer())
  
  for (var in vars) {
    datos <- df[[var]]
    Q1 <- quantile(datos, 0.25, na.rm = TRUE)
    Q3 <- quantile(datos, 0.75, na.rm = TRUE)
    IQR_val <- Q3 - Q1
    lim_inf <- Q1 - 1.5 * IQR_val
    lim_sup <- Q3 + 1.5 * IQR_val
    
    outliers <- sum(datos < lim_inf | datos > lim_sup, na.rm = TRUE)
    
    resumen <- rbind(resumen, data.frame(Variable = var, N_outliers = outliers))
  }
  
  resumen[order(-resumen$N_outliers), ]
}

# Mostrar tabla
tabla_outliers <- detectar_outliers_tabla(rotacion, num_vars)

knitr::kable(tabla_outliers, caption = "Conteo de outliers")
Conteo de outliers
Variable N_outliers
9 Capacitaciones 238
1 Ingreso_Mensual 114
11 Años_ultima_promoción 107
5 Antigüedad 104
4 Años_Experiencia 63
10 Trabajos_Anteriores 52
6 Antigüedad_Cargo 21
7 Años_acargo_con_mismo_jefe 14
2 Distancia_Casa 0
3 Edad 0
8 Porcentaje_aumento_salarial 0
# Diagramas de caja interactivos de variables numéricas

# Variables extra para mostrar en el hover
hover_vars <- c("Cargo", "Edad", "Antigüedad")

# Crear gráfico vacío
fig <- plot_ly()

# Boxplot + puntos
for (v in num_vars) {
  # Texto de hover para cada fila
  hover_text <- apply(rotacion[hover_vars], 1, function(row){
    paste(names(row), row, sep=": ", collapse="<br>")
  })
  
  # Boxplot
  fig <- fig %>%
    add_boxplot(y = rotacion[[v]], name = v,
                boxpoints = "suspectedoutliers",
                marker = list(opacity = 0),
                line = list(color = 'grey'),
                visible = FALSE,
                showlegend = FALSE)
  
  # Scatter con todos los puntos 
  fig <- fig %>%
    add_trace(
      type = "scatter",
      mode = "markers",
      y = rotacion[[v]],
      x = rep(v, nrow(rotacion)),  
      text = hover_text,
      hoverinfo = "text+y",
      marker = list(size = 6, color = 'coral', opacity = 0.2),
      name = paste("Puntos", v),
      visible = FALSE,
      showlegend = FALSE
    )
}

fig <- fig %>% style(visible = TRUE, traces = c(1,2))

# Alternar visibilidad
buttons <- lapply(seq_along(num_vars), function(i){
  vis <- rep(FALSE, length(num_vars)*2)
  vis[(2*i-1):(2*i)] <- TRUE
  list(
    method = "restyle",
    args = list("visible", vis),
    label = num_vars[i]
  )
})

# Layout final
fig <- fig %>%
  layout(
    title = list(
      text = paste("Boxplot:", num_vars[1]),
      x = 0.5),
    yaxis = list(title = ""),
    xaxis = list(title = ""),
    updatemenus = list(list(
      type = "dropdown",
      direction = "down",
      x = 0.02, y = 1.15,
      showactive = TRUE,
      buttons = buttons
    ))
  )

fig

Al analizar los diagramas de caja de las variables con presencia de outliers, se evidencia que las observaciones extremas en Años_ultima_promoción, Antigüedad, Años_Experiencia y Antigüedad_Cargo se asocian principalmente con empleados de mayor edad, lo cual resulta coherente y sugiere que corresponden a trayectorias laborales prolongadas, por lo que no deben considerarse errores. En el caso de Capacitaciones, los valores atípicos corresponden a un máximo de 6 y a valores de 0, ambos plausibles dentro de este contexto, dado que algunos empleados pueden no haber recibido formación adicional mientras que otros pueden haber participado en multiples capacitaciones. Finalmente, los ingresos mensuales atípicamente altos se relacionan con cargos de gerencia y puestos directivos, lo cual es consistente con niveles salariales superiores al promedio. En consecuencia, los outliers identificados se interpretan como datos posibles y representativos de la realidad de la organización, por lo que no se aplican tratamientos de eliminación o transformación en esta etapa del análisis.

Selección de variables

Variables Cuantitavas

  1. Ingreso_Mensual: El salario es un factor clave de permanencia. Quienes tienen ingresos más bajos son más propensos a buscar nuevas y mejores oportunidades laborales en comparación con aquellos con salarios elevados.

  2. Antigüedad: El tiempo en la organización refleja la experiencia y estabilidad del empleado. Una antigüedad baja se asocia con una mayor probabilidad de rotación, mientras que una alta suele indicar una tendencia a permanecer en el cargo.

  3. Distancia_Casa: La proximidad del lugar de trabajo al hogar del empleado influye en su decisión de permanencia. Una distancia mayor implica costos de tiempo y transporte más elevados, lo que puede aumentar el desgaste y la probabilidad de querer buscar cargos que permitan considerar opciones de trabajo hidridas o teletrabajo.Por el contrario, los empleados que viven más cerca suelen tener mayor facilidad para conciliar la vida personal y laboral, lo que fomenta su estabilidad en la organización.

Variables Cualitativas

  1. Satisfacción_Laboral: El nivel de satisfacción con el trabajo influye directamente en la rotación. A menor satisfacción, mayor es la probabilidad de que el empleado rote en la organización.

  2. Horas_Extra: Trabajar fuera del horario regular puede generar desgaste fisico y emocional del empleado, afectando su equilibrio vida/trabajo por lo que los empleados que si las realizan tienen mayor probabilidad de rotacion que los que no realizan horas extra.

  3. Equilibrio_Trabajo_Vida: El balance entre la vida personal y laboral es crucial para la retención. Los empleados que perciben un buen equilibrio muestran mayor compromiso y menor intención de rotación. Por el contrario, una puntuación baja en este indicador se asocia con mayores niveles de estrés, insatisfacción y por consiguiente, una mayor probabilidad de rotación.

Análisis univariado

# Resumen estadístico de variables numéricas
resumen_num <- purrr::map_dfr(num_vars, ~{
  x <- rotacion[[.x]]
  tibble::tibble(
    Variable = .x,
    n        = sum(!is.na(x)),
    Media    = mean(x, na.rm = TRUE),
    DE       = sd(x, na.rm = TRUE),
    Mín      = min(x, na.rm = TRUE),
    Q1       = quantile(x, 0.25, na.rm = TRUE),
    Mediana  = median(x, na.rm = TRUE),
    Q3       = quantile(x, 0.75, na.rm = TRUE),
    Máx      = max(x, na.rm = TRUE)
  )
})

knitr::kable(
  resumen_num,
  digits = c(0,0,2,2,2,2,2,2,2),
  caption = "Resumen estadístico de variables numéricas"
)
Resumen estadístico de variables numéricas
Variable n Media DE Mín Q1 Mediana Q3 Máx
Ingreso_Mensual 1470 6502.93 4707.96 1009 2911 4919 8379 19999
Distancia_Casa 1470 9.19 8.11 1 2 7 14 29
Edad 1470 36.92 9.14 18 30 36 43 60
Años_Experiencia 1470 11.28 7.78 0 6 10 15 40
Antigüedad 1470 7.01 6.13 0 3 5 9 40
Antigüedad_Cargo 1470 4.23 3.62 0 2 3 7 18
Años_acargo_con_mismo_jefe 1470 4.12 3.57 0 2 3 7 17
Porcentaje_aumento_salarial 1470 15.21 3.66 11 12 14 18 25
Capacitaciones 1470 2.80 1.29 0 2 3 3 6
Trabajos_Anteriores 1470 2.69 2.50 0 1 2 4 9
Años_ultima_promoción 1470 2.19 3.22 0 0 1 3 15
# Selección de variables numéricas principales
num_sel <- rotacion[, c("Ingreso_Mensual","Edad","Antigüedad", "Distancia_Casa")]

melted <- melt(num_sel)

ggplot(melted, aes(x = value)) +
  geom_histogram(bins = 30, fill = "lightblue", color = "white") +
  facet_wrap(~variable, scales = "free") +
  theme_minimal() +
  labs(title = "Distribución de variables numéricas seleccionadas") +
  theme(
    plot.title = element_text(hjust = 0.5)
  )

El análisis descriptivo de las variables numéricas muestra que los empleados presentan un ingreso mensual promedio cercano a 6.500, aunque con una gran dispersión y clara asimetría hacia la izquierda (evidenciada en el histograma), con una elevada concentración de salarios bajos y medios y unos pocos casos en niveles muy altos, correspondientes a cargos directivos. La edad promedio es de 37 años, con una distribución aproximadamente simétrica entre los 30 y 43, lo que refleja una plantilla en su mayoría adulta joven. La antigüedad en la organización presenta una fuerte concentración en valores bajos (mediana de 5 años), confirmando que la mayor parte de los empleados lleva poco tiempo en la empresa, aunque existen algunos casos de larga permanencia. Los años desde la última promoción y el número de trabajos anteriores evidencian que la mayoría de los empleados ha tenido poca movilidad laboral, mientras que el porcentaje de aumento salarial presenta poca variabilidad mostrando una politica uniforme de incrementos. Finalmente, la distancia desde la vivienda al lugar de trabajo también evidencia asimetría positiva: la mayoría de los empleados vive a menos de 10 km, mientras que solo un grupo reducido recorre distancias mayores a 20 km.

# Funcion para calcular frecuencias
freq_tabla_larga <- function(data, vars){
  do.call(rbind, lapply(vars, function(v){
    x <- as.character(data[[v]])
    tb <- as.data.frame(table(x), stringsAsFactors = FALSE)
    names(tb) <- c("Categoría", "Frecuencia")
    tb$Variable <- v
    tb$`%` <- round(100 * tb$Frecuencia / sum(tb$Frecuencia), 1)
    tb[, c("Variable", "Categoría", "Frecuencia", "%")]
  }))
}

# Construir tabla para ordinales + nominales
tabla_cat_ord <- freq_tabla_larga(rotacion, c(ord_vars, cat_vars))

tabla <- tabla_cat_ord %>%
  select(Variable, Categoría, Frecuencia, `%`) %>%
  mutate(
    Variable = as.character(Variable),
    Categoria_num = suppressWarnings(as.numeric(as.character(Categoría)))
  )

# Ordenar por variable
tabla_ord <- tabla %>%
  split(.$Variable) %>%
  imap_dfr(function(df, var){
    if (var %in% ord_vars) {
      df %>% arrange(Categoria_num)
    } else {
      df %>% arrange(desc(Frecuencia), Categoría)  # más frecuentes primero
    }
  }) %>%
  select(-Categoria_num) %>%
  mutate(
    Variable = factor(Variable, levels = c(ord_vars, cat_vars))
  ) %>%
  arrange(Variable)

# Mostrar tabla
kable(
  tabla_ord,
  caption = "Frecuencias absolutas y relativas (%) por variable",
  col.names = c("Variable","Categoría","Frecuencia","%")
)
Frecuencias absolutas y relativas (%) por variable
Variable Categoría Frecuencia %
Satisfacción_Ambiental 1 284 19.3
Satisfacción_Ambiental 2 287 19.5
Satisfacción_Ambiental 3 453 30.8
Satisfacción_Ambiental 4 446 30.3
Satisfación_Laboral 1 289 19.7
Satisfación_Laboral 2 280 19.0
Satisfación_Laboral 3 442 30.1
Satisfación_Laboral 4 459 31.2
Equilibrio_Trabajo_Vida 1 80 5.4
Equilibrio_Trabajo_Vida 2 344 23.4
Equilibrio_Trabajo_Vida 3 893 60.7
Equilibrio_Trabajo_Vida 4 153 10.4
Rendimiento_Laboral 3 1244 84.6
Rendimiento_Laboral 4 226 15.4
Educación 1 170 11.6
Educación 2 282 19.2
Educación 3 572 38.9
Educación 4 398 27.1
Educación 5 48 3.3
Viaje de Negocios Raramente 1043 71.0
Viaje de Negocios Frecuentemente 277 18.8
Viaje de Negocios No_Viaja 150 10.2
Departamento IyD 961 65.4
Departamento Ventas 446 30.3
Departamento RH 63 4.3
Campo_Educación Ciencias 606 41.2
Campo_Educación Salud 464 31.6
Campo_Educación Mercadeo 159 10.8
Campo_Educación Tecnicos 132 9.0
Campo_Educación Otra 82 5.6
Campo_Educación Humanidades 27 1.8
Genero M 882 60.0
Genero F 588 40.0
Cargo Ejecutivo_Ventas 326 22.2
Cargo Investigador_Cientifico 292 19.9
Cargo Tecnico_Laboratorio 259 17.6
Cargo Director_Manofactura 145 9.9
Cargo Representante_Salud 131 8.9
Cargo Gerente 102 6.9
Cargo Representante_Ventas 83 5.6
Cargo Director_Investigación 80 5.4
Cargo Recursos_Humanos 52 3.5
Estado_Civil Casado 673 45.8
Estado_Civil Soltero 470 32.0
Estado_Civil Divorciado 327 22.2
Horas_Extra No 1054 71.7
Horas_Extra Si 416 28.3

El análisis de frecuencias revela un perfil claro de la plantilla. En cuanto a la percepcion de los empleados, la mayoría reporta altos niveles de satisfacción laboral y ambiental. Sin embargo, el equilibrio trabajo-vida prevalece la categoría intermedia, con una mayoría significativa del 60,7%.

Respecto a las características profesionales, el rendimiento laboral está altamente concentrado en la categoría 3 (84,6%), mientras que en educación predominan los niveles intermedios/altos (3 y 4).

El análisis de las variables nominales refleja condiciones laborales específicas: la gran mayoría de los empleados viaja raramente por negocios (71%) y no realiza horas extra (71,7%), lo que indica que la sobrecarga laboral no es un problema generalizado en la organización.

Finalmente, la plantilla se caracteriza por una fuerte presencia en el departamento de Investigación y Desarrollo (65,4%) y en el área de Ciencias (41,2%), con predominio del género masculino (60%) y de empleados casados (45,8%).

# Grafico distribución de la variable rotación
ggplot(rotacion, aes(x = Rotación)) +
  geom_bar(fill = "lightblue") +
  geom_text(stat = "count", aes(label = ..count..), vjust = -0.3) +
  labs(
    title = "Distribución de la variable Rotación",
    x = "Rotación (0 = No, 1 = Sí)",
    y = "Frecuencia"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5))

Finalmente, se observa que la variable Rotación presenta un marcado desbalance de clases, ya que aproximadamente el 83% de los registros corresponden a empleados que no rotan, mientras que solo una proporción reducida refleja casos de rotación.

Análisis bivariado

# Análisis bivariado para variables numéricas
res_num <- lapply(num_vars, function(v){
  f <- as.formula(paste("Rotación ~", v))
  m <- glm(f, data = rotacion, family = binomial)
  broom::tidy(m)[2, c("term","estimate","p.value")]
}) %>% bind_rows()

# Mostrar tabla
knitr::kable(res_num,
             caption = "Regresiones logísticas bivariadas - Variables numéricas",
             col.names = c("Variable","Coeficiente","p-valor"))
Regresiones logísticas bivariadas - Variables numéricas
Variable Coeficiente p-valor
Ingreso_Mensual -0.0001271 0.0000000
Distancia_Casa 0.0247101 0.0029517
Edad -0.0522544 0.0000000
Años_Experiencia -0.0777307 0.0000000
Antigüedad -0.0807589 0.0000004
Antigüedad_Cargo -0.1462777 0.0000000
Años_acargo_con_mismo_jefe -0.1413767 0.0000000
Porcentaje_aumento_salarial -0.0101238 0.6053630
Capacitaciones -0.1299500 0.0228531
Trabajos_Anteriores 0.0456464 0.0959595
Años_ultima_promoción -0.0297867 0.2064513

Los coeficientes negativos obtenidos indican que a medida que aumenta el valor de la variable, disminuye la probabilidad de rotación. Entre estas variables se destacan aquellas relacionadas con una mayor estabilidad y permanencia en la organización, como la Edad, los años de experiencia, los años en el cargo con el mismo jefe, la antigüedad en la empresa y la antigüedad en el cargo. Tambien, aspectos económicos como el ingreso mensual y formativos como el número de capacitaciones muestran una relación directamente proporcional con la probabilidad de permanencia de los empleados, mientras que, aspectos como una mayor distancia hasta la casa influyen positivamente en la probabilidad de rotación.

Finalmente, variables como el número de Trabajos_Anteriores, el Porcentaje_aumento_salarial y los Años_ultima_promoción no presentan efectos significativos (p > 0.05), lo que indica que, de manera aislada, no se relacionan de forma clara con la rotación.

En línea con los resultados, las tres hipótesis planteadas inicialmente encuentran respaldo:

  • El Ingreso_Mensual confirmó ser un factor clave de permanencia.

  • La Antigüedad demostró asociarse con una menor probabilidad de rotación.

  • La Distancia_Casa mostró que una mayor proximidad al trabajo reduce la probabilidad de rotación, mientras que una distancia mayor la incrementa.

# Analisis bivariado para variables categóricas
res_cat <- data.frame()

for (v in cat_vars) {
  m <- glm(rotacion[[1]] ~ rotacion[[v]], family = binomial)
  result <- broom::tidy(m)[-1, c("term", "estimate", "p.value")]
  result$variable <- v
  res_cat <- rbind(res_cat, result)
}

# Limpiar y presentar resultados
res_cat_clean <- res_cat %>%
  mutate(
    categoria = gsub("rotacion\\[\\[v\\]\\]", "", term),
    categoria = trimws(categoria)
  ) %>%
  select(variable, categoria, estimate, p.value)

# Mostrar tabla
knitr::kable(res_cat_clean,
             caption = "Regresiones logisticas bivariadas - Variables categoricas nominales",
             col.names = c("Variable", "Categoria", "Coeficiente", "p-valor"),
             digits = c(0, 0, 3, 4),
             align = c("l", "l", "r", "r"))
Regresiones logisticas bivariadas - Variables categoricas nominales
Variable Categoria Coeficiente p-valor
Viaje de Negocios No_Viaja -1.339 0.0001
Viaje de Negocios Raramente -0.635 0.0001
Departamento RH 0.382 0.2533
Departamento Ventas 0.481 0.0013
Campo_Educación Humanidades 0.710 0.1180
Campo_Educación Mercadeo 0.494 0.0267
Campo_Educación Otra -0.105 0.7592
Campo_Educación Salud -0.091 0.6067
Campo_Educación Tecnicos 0.620 0.0079
Genero M 0.166 0.2591
Cargo Director_Manofactura 1.061 0.1780
Cargo Ejecutivo_Ventas 2.112 0.0039
Cargo Gerente 0.698 0.4116
Cargo Investigador_Cientifico 2.012 0.0061
Cargo Recursos_Humanos 2.460 0.0018
Cargo Representante_Salud 1.057 0.1838
Cargo Representante_Ventas 3.248 0.0000
Cargo Tecnico_Laboratorio 2.507 0.0006
Estado_Civil Divorciado -0.239 0.2709
Estado_Civil Soltero 0.877 0.0000
Horas_Extra Si 1.327 0.0000
# Análisis bivariado para variables ordinales
res_ord <- data.frame()

for (v in ord_vars) {
  # Convertir a factor regular (no ordenado) para evitar contrastes polinomiales
  temp_var <- factor(rotacion[[v]], ordered = FALSE)
  m <- glm(rotacion$Rotación ~ temp_var, family = binomial)
  result <- broom::tidy(m)[-1, c("term", "estimate", "p.value")]
  result$variable <- v
  res_ord <- rbind(res_ord, result)
}

# Limpiar y presentar resultados ordinales
res_ord_clean <- res_ord %>%
  # Limpiar el nombre de las categorías
  mutate(
    categoria = gsub("temp_var", "", term),
    categoria = trimws(categoria),
    # Crear etiquetas descriptivas
    nivel = as.numeric(categoria),
    categoria_desc = case_when(
      !is.na(nivel) ~ paste("Nivel", nivel),
      TRUE ~ categoria
    )
  ) %>%
  select(variable, categoria = categoria_desc, estimate, p.value)

# Mostrar tabla 
knitr::kable(res_ord_clean,
             caption = "Regresiones logísticas bivariadas - Variable categoricas ordinales",
             col.names = c("Variable", "Categoría", "Coeficiente", "p-valor"),
             digits = c(0, 0, 3, 4),
             align = c("l", "l", "r", "r"))
Regresiones logísticas bivariadas - Variable categoricas ordinales
Variable Categoría Coeficiente p-valor
Satisfacción_Ambiental Nivel 2 -0.656 0.0022
Satisfacción_Ambiental Nivel 3 -0.762 0.0001
Satisfacción_Ambiental Nivel 4 -0.782 0.0001
Satisfación_Laboral Nivel 2 -0.409 0.0555
Satisfación_Laboral Nivel 3 -0.403 0.0339
Satisfación_Laboral Nivel 4 -0.840 0.0000
Equilibrio_Trabajo_Vida Nivel 2 -0.807 0.0041
Equilibrio_Trabajo_Vida Nivel 3 -1.009 0.0001
Equilibrio_Trabajo_Vida Nivel 4 -0.752 0.0192
Rendimiento_Laboral Nivel 4 0.022 0.9118
Educación Nivel 2 -0.188 0.4665
Educación Nivel 3 -0.063 0.7800
Educación Nivel 4 -0.268 0.2724
Educación Nivel 5 -0.651 0.2038

El análisis bivariado revela patrones significativos en la rotación de personal. En variables categóricas nominales, destacan como factores de riesgo de rotación los empleados solteros, quienes realizan horas extra y ciertos cargos como Representante de Ventas, Tecnicos de Laboratorio y Recursos Humanos. Como factor que influye positivamente en la permanencia sobresalen los empleados que no viajan por negocios (coef. = -1.339, p < 0.001).

En variables ordinales, se observa una relación consistente: mayores niveles de satisfacción ambiental, laboral y equilibrio trabajo-vida se asocian con menor probabilidad de rotación, mostrando en casi todos los casos coeficientes negativos significativos.

En línea con los resultados, las tres hipótesis planteadas inicialmente encuentran respaldo:

  • La Satisfacción_Laboral confirmó que a mayor nivel de satisfacción, menor probabilidad de rotación.

  • Las Horas_Extra demostraron que los empleados que las realizan tienen significativamente mayor probabilidad de rotación.

  • El Equilibrio_Trabajo_Vida mostró que mejores niveles de equilibrio se asocian con una mayor probabilidad de permanencia en el cargo.

Estimación del modelo y evaluación

# Ajustar nombres de variables para evitar errores
names(rotacion) <- c("Rotacion", "Edad", "Viaje_de_Negocios", "Departamento", "Distancia_Casa", 
                    "Educacion", "Campo_Educacion", "Satisfaccion_Ambiental", "Genero", "Cargo",
                    "Satisfaccion_Laboral", "Estado_Civil", "Ingreso_Mensual", "Trabajos_Anteriores",
                    "Horas_Extra", "Porcentaje_aumento_salarial", "Rendimiento_Laboral", 
                    "Años_Experiencia", "Capacitaciones", "Equilibrio_Trabajo_Vida", 
                    "Antiguedad", "Antiguedad_Cargo", "Años_ultima_promocion", 
                    "Años_acargo_con_mismo_jefe")
# Division Train/Test
set.seed(123)
id_entrenamiento <- sample(seq_len(nrow(rotacion)), size = 0.7 * nrow(rotacion))
datos_entrenamiento <- rotacion[id_entrenamiento, ]
datos_prueba <- rotacion[-id_entrenamiento, ]

# Convertir variables ordinales a factor regular
datos_entrenamiento <- datos_entrenamiento %>%
  dplyr::mutate(
    Ingreso_k = Ingreso_Mensual/1000,
    Satisfaccion_Laboral = factor(Satisfaccion_Laboral, 
                                  levels = sort(unique(Satisfaccion_Laboral)),
                                  ordered = FALSE),
    Equilibrio_Trabajo_Vida = factor(Equilibrio_Trabajo_Vida, 
                                     levels = sort(unique(Equilibrio_Trabajo_Vida)),
                                     ordered = FALSE)
  )

datos_prueba <- datos_prueba %>%
  dplyr::mutate(
    Ingreso_k = Ingreso_Mensual/1000,
    Satisfaccion_Laboral = factor(Satisfaccion_Laboral, 
                                  levels = levels(datos_entrenamiento$Satisfaccion_Laboral),
                                  ordered = FALSE),
    Equilibrio_Trabajo_Vida = factor(Equilibrio_Trabajo_Vida, 
                                     levels = levels(datos_entrenamiento$Equilibrio_Trabajo_Vida),
                                     ordered = FALSE)
  )

# Ponderaciones por desbalance (más peso a la clase 1)
y_entrenamiento <- if (is.factor(datos_entrenamiento$Rotacion)) {
  as.integer(as.character(datos_entrenamiento$Rotacion))
} else {
  as.integer(datos_entrenamiento$Rotacion)
}
n_rotacion <- sum(y_entrenamiento == 1)
n_no_rotacion <- sum(y_entrenamiento == 0)
pesos <- ifelse(y_entrenamiento == 1, n_no_rotacion / n_rotacion, 1)
pesos <- pesos / mean(pesos)

# Ajuste del modelo con ponderación
modelo_final <- glm(Rotacion ~ Horas_Extra + Satisfaccion_Laboral + Equilibrio_Trabajo_Vida +
                     Ingreso_k + Distancia_Casa + Antiguedad,
                   data = datos_entrenamiento, family = binomial(), weights = pesos)

# Limpiar y presentar resultados
tabla_coeficientes_clean <- broom::tidy(modelo_final) %>%
  dplyr::mutate(
    # Limpiar nombres de términos
    term_clean = gsub("Satisfaccion_Laboral", "Satisf_Lab_", term),
    term_clean = gsub("Equilibrio_Trabajo_Vida", "Eq_Trab_Vida_", term_clean),
    
    # Extraer números y crear etiquetas limpias
    nivel = stringr::str_extract(term_clean, "[0-9]+$"),
    variable_final = ifelse(!is.na(nivel),
                           paste0(gsub("_[0-9]+$", "", term_clean), " Nivel ", nivel),
                           term_clean),
    
    # Calcular OR e IC95%
    OR = exp(estimate),
    IC95_L = exp(estimate - 1.96 * std.error),
    IC95_U = exp(estimate + 1.96 * std.error)
  ) %>%
  dplyr::select(Variable = variable_final, Coeficiente = estimate, OR, IC95_L, IC95_U, p.value)

# Mostrar tabla
knitr::kable(tabla_coeficientes_clean, digits = 2, 
             caption = "Modelo logistico multivariado con ponderacion")
Modelo logistico multivariado con ponderacion
Variable Coeficiente OR IC95_L IC95_U p.value
(Intercept) 1.62 5.04 2.68 9.46 0.00
Horas_ExtraSi 1.28 3.59 2.69 4.79 0.00
Satisf_Lab Nivel 2 -0.55 0.58 0.38 0.88 0.01
Satisf_Lab Nivel 3 -0.56 0.57 0.39 0.83 0.00
Satisf_Lab Nivel 4 -0.89 0.41 0.28 0.60 0.00
Eq_Trab_Vida Nivel 2 -0.96 0.38 0.21 0.70 0.00
Eq_Trab_Vida Nivel 3 -1.18 0.31 0.18 0.53 0.00
Eq_Trab_Vida Nivel 4 -0.65 0.52 0.27 0.99 0.05
Ingreso_k -0.14 0.87 0.84 0.91 0.00
Distancia_Casa 0.02 1.02 1.00 1.04 0.02
Antiguedad 0.00 1.00 0.97 1.02 0.78

El modelo multivariado muestra que hacer horas extra es el principal factor de riesgo: se asocia con un riesgo de rotación aproximadamente 3,59 veces mayor que en quienes no las realizan (p<0,001). En contraste, una mayor satisfacción laboral reduce las odds de forma progresiva y significativa (niveles 2–4: OR 0,57–0,41), y un mejor equilibrio trabajo–vida también protege (niveles 2–3: OR 0,38–0,31, aunque el nivel 4 no sigue la tendencia pero no es significativo, p=0,05). un mayor salario protege frente a la rotación pues por cada $1.000 adicionales, las odds de rotar bajan ~12,6% (OR=0,87; p<0,001). En cambio, vivir más lejos aumenta ligeramente el riesgo pues cada kilómetro extra se asocia con ~2,0% más en las odds de rotación (OR=1,02; p=0,02). Por ultimo, la antigüedad no muestra efecto significativo en la probabilidad de rotacion (p=0,78). En conjunto, los hallazgos confirman de manera general las hipótesis planteadas: mayor carga (horas extra) y mayor distancia aumentan la rotación; mejor salario, satisfacción y equilibrio disminuyen la rotación.

# Probabilidades y variable respuesta
p_test <- predict(modelo_final, newdata = datos_prueba, type = "response")
y_test <- as.numeric(as.character(datos_prueba$Rotacion))

# Calcular y mostrar AUC
roc_obj <- roc(y_test, p_test, quiet = TRUE)
auc_val <- auc(roc_obj)
ci_auc <- ci.auc(roc_obj)

cat(sprintf("AUC = %.3f (IC95%%: %.3f - %.3f)\n", auc_val, ci_auc[1], ci_auc[3]))
## AUC = 0.730 (IC95%: 0.665 - 0.795)
# Gráfico
plot(roc_obj, main = sprintf("Curva ROC (AUC = %.3f)", auc_val))
abline(0, 1, lty = 2, col = "gray")

La curva ROC muestra un desempeño aceptable: un AUC=0.730 indica que, en promedio, el modelo asigna una probabilidad mayor de rotación al empleado que realmente rota que al que no rota en 73.6% de los pares comparados. El IC95% [0.665–0.795] está claramente por encima de 0.5, por lo que el modelo discrimina mejor que el azar, aunque aún hay margen de mejora.

Estimación hipotética

# Individuo hipotético
new_emp <- data.frame(
  Horas_Extra             = factor("Si", levels = levels(datos_entrenamiento$Horas_Extra)),
  Satisfaccion_Laboral    = factor("3",  levels = levels(datos_entrenamiento$Satisfaccion_Laboral)),   
  Equilibrio_Trabajo_Vida = factor("2",  levels = levels(datos_entrenamiento$Equilibrio_Trabajo_Vida)),
  Ingreso_k               = 3.5,  
  Distancia_Casa          = 12,   
  Antiguedad              = 2      
)

# Probabilidad de rotación del individuo
p_new <- predict(modelo_final, newdata = new_emp, type = "response")

# Definir corte (umbral óptimo de Youden usando el set de prueba)
p_test <- predict(modelo_final, newdata = datos_prueba, type = "response")
y_test <- if (is.factor(datos_prueba$Rotacion)) {
  as.integer(as.character(datos_prueba$Rotacion))
} else {
  as.integer(datos_prueba$Rotacion)
}

roc_obj <- roc(y_test, p_test, quiet = TRUE)
thr <- coords(roc_obj, "best", ret = "threshold", best.method = "youden")

# Decisión de intervención
decision <- ifelse(p_new >= thr, "Intervenir", "No Intervenir")

res_pred <- data.frame(
  `Prob.rotacion` = round(p_new, 3),
  `Umbral.Youden` = round(thr, 3),
  `AUC.test` = round(auc_val, 3),
  `Decision` = decision
)

# Mostrar Tabla
knitr::kable(
  res_pred,
  caption = "Prediccion de rotación y decision de intervencion (individuo hipotetico)"
)
Prediccion de rotación y decision de intervencion (individuo hipotetico)
Prob.rotacion threshold AUC.test threshold.1
0.756 0.569 0.73 Intervenir

Para el caso de este colaborador hipotético, al tener una probabilidad elevada de rotación (0.756) por encima del umbral establecido, se sugeriria intervenir al empleado para investigar los motivos de su insatisfacción laboral para implementar un plan de retención integral que aborde los factores clave identificados: redistribución de tareas para reducir horas extra, implementación de teletrabajo para mitigar la distancia al trabajo, programas de bienestar laboral que mejoren el equilibrio trabajo-vida, y una revisión de la estructura compensacional que considere ajustes salariales, todo ello complementado con oportunidades de capacitación y desarrollo profesional.

Conclusiones

Para disminuir la rotación, la estrategia de intervencion a los empleados con probabilidades por encima del umbral definido debe atacar los factores con mayor impacto identificados por el modelo:

  • Horas extra: redistribuir turnos y cargas, incrementar personal en equipos críticos y fijar límites/alertas de horas para prevenir sobrecarga.

  • Satisfacción laboral: fortalecer liderazgo y clima laboral con reuniones de equipo frecuentes, capacitaciones e integraciones.

  • Equilibrio trabajo-vida: ampliar la flexiblibilidad en los horarios con opcion de teletrabajo/híbrido, horarios escalonados y una buena gestión de vacaciones.

  • Distancia a casa: ofrecer beneficios de movilidad (rutas o auxilios de transporte) o priorizar flexibilidad para quienes viven más lejos.

  • Ingresos: aplicar ajustes salariales selectivos o bonos de retención para roles y personas con altas responsabilidades, con revisiones periódicas de equidad interna.