INTRODUCCIÓN

En el contexto actual de la gestión del talento humano, comprender la dinámica de la rotación interna de empleados se ha convertido en un aspecto clave para la sostenibilidad y competitividad organizacional. En este sentido, el análisis de los factores que influyen en el cambio de cargo dentro de una empresa permite no solo describir comportamientos pasados, sino también anticipar tendencias futuras que impactan directamente en la estabilidad laboral y el desempeño institucional.

Para abordar este problema, los Modelos Lineales Generalizados (MLG) ofrecen un marco estadístico flexible y robusto, especialmente cuando la variable de interés es de naturaleza categórica. En particular, la regresión logística, como caso especial de los MLG, resulta adecuada para modelar la probabilidad de ocurrencia de un evento binario, como lo es el cambio o no de cargo por parte de un empleado en un período determinado.

A partir de datos históricos que incluyen variables como la antigüedad en el cargo, nivel de satisfacción laboral, salario y edad, entre otros, es posible construir un modelo que relacione estos factores con la probabilidad de rotación interna. Este enfoque permite identificar cuáles variables tienen mayor influencia en la decisión o necesidad de cambio, así como cuantificar su impacto relativo.

De esta manera, la implementación de un modelo de regresión logística no solo contribuye al entendimiento analítico del fenómeno, sino que también se convierte en una herramienta estratégica para la toma de decisiones. Con base en los resultados obtenidos, la organización podrá diseñar políticas orientadas a la retención de talento, optimizar la asignación de recursos humanos y promover un entorno laboral más estable, alineado con los objetivos corporativos y el bienestar de sus empleados.

SELECCIÓN DE VARIABLES

library(paqueteMODELOS)
## Cargando paquete requerido: boot
## Cargando paquete requerido: broom
## Warning: package 'broom' was built under R version 4.5.2
## Cargando paquete requerido: GGally
## Cargando paquete requerido: ggplot2
## Warning: package 'ggplot2' was built under R version 4.5.2
## Cargando paquete requerido: gridExtra
## Cargando paquete requerido: knitr
## Cargando paquete requerido: summarytools
## Warning: package 'summarytools' was built under R version 4.5.2
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.5.2
## 
## Adjuntando el paquete: 'dplyr'
## The following object is masked from 'package:gridExtra':
## 
##     combine
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
data("rotacion")
glimpse(rotacion)
## Rows: 1,470
## Columns: 24
## $ Rotación                    <chr> "Si", "No", "Si", "No", "No", "No", "No", …
## $ Edad                        <dbl> 41, 49, 37, 33, 27, 32, 59, 30, 38, 36, 35…
## $ `Viaje de Negocios`         <chr> "Raramente", "Frecuentemente", "Raramente"…
## $ Departamento                <chr> "Ventas", "IyD", "IyD", "IyD", "IyD", "IyD…
## $ Distancia_Casa              <dbl> 1, 8, 2, 3, 2, 2, 3, 24, 23, 27, 16, 15, 2…
## $ Educación                   <dbl> 2, 1, 2, 4, 1, 2, 3, 1, 3, 3, 3, 2, 1, 2, …
## $ Campo_Educación             <chr> "Ciencias", "Ciencias", "Otra", "Ciencias"…
## $ Satisfacción_Ambiental      <dbl> 2, 3, 4, 4, 1, 4, 3, 4, 4, 3, 1, 4, 1, 2, …
## $ Genero                      <chr> "F", "M", "M", "F", "M", "M", "F", "M", "M…
## $ Cargo                       <chr> "Ejecutivo_Ventas", "Investigador_Cientifi…
## $ Satisfación_Laboral         <dbl> 4, 2, 3, 3, 2, 4, 1, 3, 3, 3, 2, 3, 3, 4, …
## $ Estado_Civil                <chr> "Soltero", "Casado", "Soltero", "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                 <chr> "Si", "No", "Si", "Si", "No", "No", "Si", …
## $ Porcentaje_aumento_salarial <dbl> 11, 23, 15, 11, 12, 13, 20, 22, 21, 13, 13…
## $ Rendimiento_Laboral         <dbl> 3, 4, 3, 3, 3, 3, 4, 4, 4, 3, 3, 3, 3, 3, …
## $ Años_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     <dbl> 1, 3, 3, 3, 3, 2, 2, 3, 3, 2, 3, 3, 2, 3, …
## $ Antigüedad                  <dbl> 6, 10, 0, 8, 2, 7, 1, 1, 9, 7, 5, 9, 5, 2,…
## $ Antigüedad_Cargo            <dbl> 4, 7, 0, 7, 2, 7, 0, 0, 7, 7, 4, 5, 2, 2, …
## $ Años_ultima_promoción       <dbl> 0, 1, 0, 3, 2, 3, 0, 0, 1, 7, 0, 0, 4, 1, …
## $ Años_acargo_con_mismo_jefe  <dbl> 5, 7, 0, 0, 2, 6, 0, 0, 8, 7, 3, 8, 3, 2, …
df<-rotacion
summary(df)
##    Rotación              Edad       Viaje de Negocios  Departamento      
##  Length:1470        Min.   :18.00   Length:1470        Length:1470       
##  Class :character   1st Qu.:30.00   Class :character   Class :character  
##  Mode  :character   Median :36.00   Mode  :character   Mode  :character  
##                     Mean   :36.92                                        
##                     3rd Qu.:43.00                                        
##                     Max.   :60.00                                        
##  Distancia_Casa     Educación     Campo_Educación    Satisfacción_Ambiental
##  Min.   : 1.000   Min.   :1.000   Length:1470        Min.   :1.000         
##  1st Qu.: 2.000   1st Qu.:2.000   Class :character   1st Qu.:2.000         
##  Median : 7.000   Median :3.000   Mode  :character   Median :3.000         
##  Mean   : 9.193   Mean   :2.913                      Mean   :2.722         
##  3rd Qu.:14.000   3rd Qu.:4.000                      3rd Qu.:4.000         
##  Max.   :29.000   Max.   :5.000                      Max.   :4.000         
##     Genero             Cargo           Satisfación_Laboral Estado_Civil      
##  Length:1470        Length:1470        Min.   :1.000       Length:1470       
##  Class :character   Class :character   1st Qu.:2.000       Class :character  
##  Mode  :character   Mode  :character   Median :3.000       Mode  :character  
##                                        Mean   :2.729                         
##                                        3rd Qu.:4.000                         
##                                        Max.   :4.000                         
##  Ingreso_Mensual Trabajos_Anteriores Horas_Extra       
##  Min.   : 1009   Min.   :0.000       Length:1470       
##  1st Qu.: 2911   1st Qu.:1.000       Class :character  
##  Median : 4919   Median :2.000       Mode  :character  
##  Mean   : 6503   Mean   :2.693                         
##  3rd Qu.: 8379   3rd Qu.:4.000                         
##  Max.   :19999   Max.   :9.000                         
##  Porcentaje_aumento_salarial Rendimiento_Laboral Años_Experiencia
##  Min.   :11.00               Min.   :3.000       Min.   : 0.00   
##  1st Qu.:12.00               1st Qu.:3.000       1st Qu.: 6.00   
##  Median :14.00               Median :3.000       Median :10.00   
##  Mean   :15.21               Mean   :3.154       Mean   :11.28   
##  3rd Qu.:18.00               3rd Qu.:3.000       3rd Qu.:15.00   
##  Max.   :25.00               Max.   :4.000       Max.   :40.00   
##  Capacitaciones  Equilibrio_Trabajo_Vida   Antigüedad     Antigüedad_Cargo
##  Min.   :0.000   Min.   :1.000           Min.   : 0.000   Min.   : 0.000  
##  1st Qu.:2.000   1st Qu.:2.000           1st Qu.: 3.000   1st Qu.: 2.000  
##  Median :3.000   Median :3.000           Median : 5.000   Median : 3.000  
##  Mean   :2.799   Mean   :2.761           Mean   : 7.008   Mean   : 4.229  
##  3rd Qu.:3.000   3rd Qu.:3.000           3rd Qu.: 9.000   3rd Qu.: 7.000  
##  Max.   :6.000   Max.   :4.000           Max.   :40.000   Max.   :18.000  
##  Años_ultima_promoción Años_acargo_con_mismo_jefe
##  Min.   : 0.000        Min.   : 0.000            
##  1st Qu.: 0.000        1st Qu.: 2.000            
##  Median : 1.000        Median : 3.000            
##  Mean   : 2.188        Mean   : 4.123            
##  3rd Qu.: 3.000        3rd Qu.: 7.000            
##  Max.   :15.000        Max.   :17.000
library(dplyr)

df <- df %>%
  mutate(
    # Satisfacción Ambiental
    Satisfacción_Ambiental = factor(Satisfacción_Ambiental,
                                    levels = 1:4,
                                    labels = c("Baja", "Media", "Alta", "Muy Alta"),
                                    ordered = TRUE),
    
    # Satisfacción Laboral (la que acabas de mencionar)
    Satisfación_Laboral = factor(Satisfación_Laboral,
                                  levels = 1:4,
                                  labels = c("Baja", "Media", "Alta", "Muy Alta"),
                                  ordered = TRUE),
    
    # Equilibrio Trabajo-Vida (recomendado incluir)
    Equilibrio_Trabajo_Vida = factor(Equilibrio_Trabajo_Vida,
                                     levels = 1:4,
                                     labels = c("Baja", "Media", "Alta", "Muy Alta"),
                                     ordered = TRUE),
    
    
    
    
    Educación = factor(Educación,
                                     levels = 1:4,
                                     labels = c("Baja", "Media", "Alta", "Muy Alta"),
                                     ordered = TRUE)
    
    
    
  )
# Ver los tipos de las variables 
glimpse(df)
## Rows: 1,470
## Columns: 24
## $ Rotación                    <chr> "Si", "No", "Si", "No", "No", "No", "No", …
## $ Edad                        <dbl> 41, 49, 37, 33, 27, 32, 59, 30, 38, 36, 35…
## $ `Viaje de Negocios`         <chr> "Raramente", "Frecuentemente", "Raramente"…
## $ Departamento                <chr> "Ventas", "IyD", "IyD", "IyD", "IyD", "IyD…
## $ Distancia_Casa              <dbl> 1, 8, 2, 3, 2, 2, 3, 24, 23, 27, 16, 15, 2…
## $ Educación                   <ord> Media, Baja, Media, Muy Alta, Baja, Media,…
## $ Campo_Educación             <chr> "Ciencias", "Ciencias", "Otra", "Ciencias"…
## $ Satisfacción_Ambiental      <ord> Media, Alta, Muy Alta, Muy Alta, Baja, Muy…
## $ Genero                      <chr> "F", "M", "M", "F", "M", "M", "F", "M", "M…
## $ Cargo                       <chr> "Ejecutivo_Ventas", "Investigador_Cientifi…
## $ Satisfación_Laboral         <ord> Muy Alta, Media, Alta, Alta, Media, Muy Al…
## $ Estado_Civil                <chr> "Soltero", "Casado", "Soltero", "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                 <chr> "Si", "No", "Si", "Si", "No", "No", "Si", …
## $ Porcentaje_aumento_salarial <dbl> 11, 23, 15, 11, 12, 13, 20, 22, 21, 13, 13…
## $ Rendimiento_Laboral         <dbl> 3, 4, 3, 3, 3, 3, 4, 4, 4, 3, 3, 3, 3, 3, …
## $ Años_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> Baja, Alta, Alta, Alta, Alta, Media, Media…
## $ Antigüedad                  <dbl> 6, 10, 0, 8, 2, 7, 1, 1, 9, 7, 5, 9, 5, 2,…
## $ Antigüedad_Cargo            <dbl> 4, 7, 0, 7, 2, 7, 0, 0, 7, 7, 4, 5, 2, 2, …
## $ Años_ultima_promoción       <dbl> 0, 1, 0, 3, 2, 3, 0, 0, 1, 7, 0, 0, 4, 1, …
## $ Años_acargo_con_mismo_jefe  <dbl> 5, 7, 0, 0, 2, 6, 0, 0, 8, 7, 3, 8, 3, 2, …
# Librerías
library(ggplot2)
library(reshape2)
library(viridis)
## Cargando paquete requerido: viridisLite
# Seleccionar solo variables numéricas
df_num <- df[, sapply(df, is.numeric)]

# Calcular matriz de correlación de Pearson
corr_matrix <- cor(df_num, method = "pearson", use = "complete.obs")

# Convertir a formato largo
corr_long <- melt(corr_matrix)

# Graficar heatmap
ggplot(corr_long, aes(x = Var1, y = Var2, fill = value)) +
  geom_tile(color = "white") +
  scale_fill_viridis(option = "viridis", name = "Correlación") +
  geom_text(aes(label = sprintf("%.2f", value)), size = 3) +
  labs(
    title = "Matriz de Correlación de Pearson",
    x = "",
    y = ""
  ) +
  theme_minimal(base_size = 12) +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    panel.grid = element_blank(),
    plot.title = element_text(hjust = 0.5, face = "bold")
  )

VARIABLES CUANTITATIVAS SELECCIONADAS

Entre todas las variables cuantitativas del dataframe, Ingreso_Mensual, Años_Experiencia y Edad son las más influyentes y estratégicas para entender los factores que impulsan la rotación de empleados.

Ingreso_Mensual destaca como uno de los predictores más poderosos de la retención. Muestra correlaciones fuertes y positivas con Años_Experiencia (alrededor de 0.77) y con Rendimiento_Laboral (0.77), lo que indica que el nivel salarial suele estar directamente ligado al nivel jerárquico y trayectoria del empleado. En la práctica, los empleados con ingresos más bajos tienden a tener mayor riesgo de rotación, ya que la compensación económica es un factor clave de satisfacción e insatisfacción laboral.

Años_Experiencia representa la trayectoria profesional acumulada del empleado. Esta variable correlaciona fuertemente con Ingreso_Mensual y con Antigüedad_Cargo (0.76), reflejando estabilidad y valor para la organización. Sin embargo, una alta experiencia sin crecimiento (por ejemplo, pocos ascensos) puede generar frustración y aumentar el riesgo de salida, convirtiéndola en un indicador dual de lealtad y posible estancamiento.

Edad actúa como variable demográfica fundamental que influye de manera indirecta pero consistente en la rotación. Generalmente, los empleados más jóvenes presentan mayor movilidad laboral, mientras que los de mayor edad tienden a ser más estables. Además, la Edad muestra correlaciones positivas relevantes con Años_Experiencia (0.68) y con Ingreso_Mensual (0.50), formando un “perfil de madurez” del empleado.

df$Rotación <- factor(df$Rotación, levels = c("No", "Si"))

# Verificar
table(df$Rotación)
## 
##   No   Si 
## 1233  237
library(DescTools)  # para CramerV

# Variables cualitativas excepto la target
vars_cualitativas <- names(df)[sapply(df, function(x) is.factor(x) || is.character(x))]
vars_cualitativas <- setdiff(vars_cualitativas, "Rotación")

resultados <- data.frame(
  Variable = character(),
  Chi_sq = numeric(),
  p_valor = numeric(),
  Cramers_V = numeric(),
  Significativo = logical(),
  stringsAsFactors = FALSE
)

for (var in vars_cualitativas) {
  tbl <- table(df[[var]], df$Rotación)
  test <- tryCatch(chisq.test(tbl), error = function(e) NULL)
  
  if (!is.null(test)) {
    cramer_v <- CramerV(tbl)
    resultados <- rbind(resultados, data.frame(
      Variable = var,
      Chi_sq = round(test$statistic, 3),
      p_valor = round(test$p.value, 5),
      Cramers_V = round(cramer_v, 3),
      Significativo = test$p.value < 0.05
    ))
  }
}
## Warning in chisq.test(tbl): Chi-squared approximation may be incorrect
# Ordenar por Cramer’s V descendente para ver variables más asociadas
resultados <- resultados[order(-resultados$Cramers_V), ]
print(resultados)
##                            Variable Chi_sq p_valor Cramers_V Significativo
## X-squared9              Horas_Extra 87.564 0.00000     0.246          TRUE
## X-squared6                    Cargo 86.190 0.00000     0.242          TRUE
## X-squared8             Estado_Civil 46.164 0.00000     0.177          TRUE
## X-squared         Viaje de Negocios 24.182 0.00001     0.128          TRUE
## X-squared4   Satisfacción_Ambiental 22.504 0.00005     0.124          TRUE
## X-squared7      Satisfación_Laboral 17.505 0.00056     0.109          TRUE
## X-squared10 Equilibrio_Trabajo_Vida 16.325 0.00097     0.105          TRUE
## X-squared3          Campo_Educación 16.025 0.00677     0.104          TRUE
## X-squared1             Departamento 10.796 0.00453     0.086          TRUE
## X-squared2                Educación  1.861 0.60165     0.036         FALSE
## X-squared5                   Genero  1.117 0.29057     0.029         FALSE

VARIABLES CUALITATIVAS SELECCIONADAS

Entre las variables cualitativas analizadas mediante la prueba de Chi-cuadrado, Horas_Extra, Cargo y Estado_Civil emergen como las más influyentes y estratégicas para explicar y predecir la rotación de empleados.

Horas_Extra es la variable categórica con mayor asociación (Chi-square más alto y Cramér’s V de 0.246). Trabajar horas extras de forma frecuente suele generar agotamiento, desbalance entre trabajo y vida personal, y menor satisfacción general. Es un fuerte indicador de burnout y uno de los predictores más consistentes de rotación en estudios de HR analytics: los empleados que hacen overtime regularmente tienen significativamente mayor probabilidad de dejar la empresa.

Cargo (Job Role) muestra una asociación muy fuerte (Chi-square = 86.19, Cramér’s V = 0.242). El rol que ocupa el empleado influye directamente en la carga laboral, las oportunidades de crecimiento, el reconocimiento y la compensación percibida. Algunos cargos (como roles operativos o de alto estrés) presentan tasas de rotación notablemente más altas, mientras que otros ofrecen mayor estabilidad o desarrollo profesional. Esta variable ayuda a identificar áreas o posiciones críticas dentro de la organización.

Estado_Civil presenta una relación significativa (Chi-square = 46.18, Cramér’s V = 0.177). Los empleados solteros suelen tener mayor movilidad laboral y menor “anclaje” familiar, lo que aumenta su propensión a cambiar de trabajo en busca de mejores oportunidades. Por el contrario, los casados o en unión libre tienden a priorizar estabilidad. Esta variable demográfica permite segmentar mejor los perfiles de riesgo.

library(dplyr)

# Crear el nuevo dataframe 'data' con las variables seleccionadas + target
data <- df %>%
  select(
    # Variables cuantitativas más influyentes
    Ingreso_Mensual,
    Años_Experiencia,
    Edad,
    
    # Variables cualitativas más influyentes
    Horas_Extra,
    Cargo,
    Estado_Civil,
    
   
    # Variable objetivo (target)
    Rotación
  )
# Ver estructura del nuevo dataframe
glimpse(data)
## Rows: 1,470
## Columns: 7
## $ Ingreso_Mensual  <dbl> 5993, 5130, 2090, 2909, 3468, 3068, 2670, 2693, 9526,…
## $ Años_Experiencia <dbl> 8, 10, 7, 8, 6, 8, 12, 1, 10, 17, 6, 10, 5, 3, 6, 10,…
## $ Edad             <dbl> 41, 49, 37, 33, 27, 32, 59, 30, 38, 36, 35, 29, 31, 3…
## $ Horas_Extra      <chr> "Si", "No", "Si", "Si", "No", "No", "Si", "No", "No",…
## $ Cargo            <chr> "Ejecutivo_Ventas", "Investigador_Cientifico", "Tecni…
## $ Estado_Civil     <chr> "Soltero", "Casado", "Soltero", "Casado", "Casado", "…
## $ Rotación         <fct> Si, No, Si, No, No, No, No, No, No, No, No, No, No, N…
# Ver dimensiones
dim(data)
## [1] 1470    7
# Resumen general
summary(data)
##  Ingreso_Mensual Años_Experiencia      Edad       Horas_Extra       
##  Min.   : 1009   Min.   : 0.00    Min.   :18.00   Length:1470       
##  1st Qu.: 2911   1st Qu.: 6.00    1st Qu.:30.00   Class :character  
##  Median : 4919   Median :10.00    Median :36.00   Mode  :character  
##  Mean   : 6503   Mean   :11.28    Mean   :36.92                     
##  3rd Qu.: 8379   3rd Qu.:15.00    3rd Qu.:43.00                     
##  Max.   :19999   Max.   :40.00    Max.   :60.00                     
##     Cargo           Estado_Civil       Rotación 
##  Length:1470        Length:1470        No:1233  
##  Class :character   Class :character   Si: 237  
##  Mode  :character   Mode  :character            
##                                                 
##                                                 
## 
# Verificar que Rotación sea factor (recomendado)
data$Rotación <- factor(data$Rotación, levels = c("No", "Si"), ordered = FALSE)

Se consolida en dataframe con las variables más influyentes tanto cuantitativas como cualitativas en el conjunto de datos llamado data.

dim(data)
## [1] 1470    7

ANÁLISIS UNIVARIADO

library(dplyr)
library(ggplot2)
library(gridExtra)   # Para organizar gráficos

# =============================================
# ANÁLISIS UNIVARIADO DEL DATAFRAME 'data'
# =============================================

# =============================================
# Variables Numéricas (Cuantitativas)
# =============================================

numericas <- c("Ingreso_Mensual", "Años_Experiencia", "Edad")

cat("\n=== ANÁLISIS UNIVARIADO - VARIABLES NUMÉRICAS ===\n")
## 
## === ANÁLISIS UNIVARIADO - VARIABLES NUMÉRICAS ===
for(var in numericas){
  cat("\nVariable:", var, "\n")
  resumen <- summary(data[[var]])
  print(resumen)
  
  cat("Desviación estándar:", round(sd(data[[var]], na.rm = TRUE), 2), "\n")
  cat("Coeficiente de variación:", round(sd(data[[var]], na.rm = TRUE)/mean(data[[var]], na.rm = TRUE)*100, 2), "%\n")
}
## 
## Variable: Ingreso_Mensual 
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    1009    2911    4919    6503    8379   19999 
## Desviación estándar: 4707.96 
## Coeficiente de variación: 72.4 %
## 
## Variable: Años_Experiencia 
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    0.00    6.00   10.00   11.28   15.00   40.00 
## Desviación estándar: 7.78 
## Coeficiente de variación: 68.98 %
## 
## Variable: Edad 
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   18.00   30.00   36.00   36.92   43.00   60.00 
## Desviación estándar: 9.14 
## Coeficiente de variación: 24.74 %
# Gráficos para variables numéricas
p2 <- ggplot(data, aes(x = Ingreso_Mensual)) + 
  geom_histogram(bins = 30, fill = "steelblue", color = "white") +
  labs(title = "Distribución de Ingreso Mensual") + theme_minimal()

p3 <- ggplot(data, aes(x = Años_Experiencia)) + 
  geom_histogram(bins = 25, fill = "darkorange", color = "white") +
  labs(title = "Distribución de Años de Experiencia") + theme_minimal()

p4 <- ggplot(data, aes(x = Edad)) + 
  geom_histogram(bins = 20, fill = "forestgreen", color = "white") +
  labs(title = "Distribución de Edad") + theme_minimal()



# Mostrar algunos gráficos juntos
grid.arrange(p2, p3, p4, ncol = 2)

La distribución del Ingreso_Mensual muestra una fuerte asimetría hacia la derecha (distribución sesgada positiva). La mayoría de los empleados se concentran en rangos salariales bajos y medios (principalmente entre 2.500 y 8.000), con una cola larga hacia valores altos que supera los 15.000 e incluso los 20.000. Este patrón es típico en organizaciones donde existe una amplia base de empleados operativos o de nivel junior con salarios moderados, y un número reducido de posiciones senior o especializadas con compensaciones significativamente más altas. Esta concentración en ingresos bajos-medios sugiere un potencial riesgo de rotación asociado a la insatisfacción salarial.

En cuanto a los Años de Experiencia, se observa una distribución multimodal con un pico pronunciado entre los 5 y 15 años de experiencia. La mayor densidad de empleados se encuentra en el rango de 8 a 12 años, seguido de una disminución progresiva a medida que aumenta la experiencia. Esto indica que la empresa cuenta con una fuerza laboral relativamente experimentada, pero con una notable reducción de empleados con más de 25 años de trayectoria, lo que podría reflejar tanto una rotación natural de perfiles senior como una dificultad para retener talento con alta experiencia.

La distribución de la Edad presenta una forma aproximadamente normal, con una ligera asimetría hacia la derecha. La mayoría de los empleados se concentran entre los 30 y 45 años, siendo el grupo de 35-40 años el más numeroso. Se observa una presencia moderada de empleados jóvenes (menores de 30 años) y una disminución gradual en los grupos de mayor edad (por encima de 55 años). Este perfil etario sugiere una población laboral en plena etapa productiva, aunque con una representación relativamente baja de talento joven, lo que podría limitar la renovación generacional y el flujo de nuevas ideas.

En conjunto, estos tres gráficos muestran una fuerza laboral predominantemente adulta joven a media, con experiencia intermedia y concentrada en niveles salariales bajos y medios. Esta combinación de características demográficas y laborales es típica en entornos donde existe un riesgo latente de rotación, especialmente entre aquellos empleados con ingresos más bajos, experiencia moderada y que aún se encuentran en etapas tempranas o intermedias de su carrera profesional.

Estos hallazgos refuerzan la importancia de analizar estas variables en relación con la variable objetivo Rotación, ya que los perfiles más comunes en la empresa (ingresos moderados, experiencia entre 8-15 años y edad entre 30-45 años) suelen ser especialmente sensibles a factores como compensación, oportunidades de crecimiento y equilibrio trabajo-vida.

library(ggplot2)
library(dplyr)
library(scales)
## 
## Adjuntando el paquete: 'scales'
## The following object is masked from 'package:viridis':
## 
##     viridis_pal
# Variables cualitativas importantes
cualitativas <- c("Horas_Extra", "Cargo", "Estado_Civil")

# Lista para almacenar gráficos
graficos_modernos <- list()

for (var in cualitativas) {
  
  # Preparar datos con proporciones por categoría
  df_plot <- data %>%
    group_by(.data[[var]], Rotación) %>%
    summarise(Count = n(), .groups = "drop") %>%
    group_by(.data[[var]]) %>%
    mutate(Proportion = Count / sum(Count))
  
  # Gráfico moderno con colores profesionales (azul y verde)
  g <- ggplot(df_plot, aes(x = .data[[var]], y = Proportion, fill = Rotación)) +
    geom_bar(stat = "identity", color = "black", width = 0.7) +
    geom_text(aes(label = scales::percent(Proportion, accuracy = 1)),
              position = position_stack(vjust = 0.5), size = 4, color = "white") +
    scale_y_continuous(labels = percent_format(), expand = expansion(mult = c(0, 0.05))) +
    scale_fill_manual(values = c("No" = "#1f77b4", "Si" = "#2ca02c")) +
    labs(
      title = paste("Distribución de", var, "según Rotación"),
      x = var,
      y = "Proporción (%)",
      fill = "Rotación"
    ) +
    theme_minimal(base_size = 14) +
    theme(
      axis.text.x = element_text(angle = 45, hjust = 1, face = "bold"),
      axis.text.y = element_text(face = "bold"),
      plot.title = element_text(face = "bold", hjust = 0.5, size = 16),
      legend.position = "top",
      panel.grid.major.x = element_blank(),
      panel.grid.minor = element_blank()
    )
  
  graficos_modernos[[var]] <- g
  
  # Mostrar cada gráfico individualmente
  print(g)
}

Horas_Extra emerge como el factor cualitativo con mayor impacto diferencial. Mientras que solo el 10% de los empleados que no realizan horas extras abandonan la empresa, este porcentaje se triplica ( 31% ) entre aquellos que sí trabajan horas extras. Esta diferencia sustancial confirma que el overtime frecuente es un poderoso predictor de rotación, probablemente asociado a agotamiento, burnout y deterioro del equilibrio trabajo-vida. Es, sin duda, uno de los factores más accionables desde la perspectiva de gestión de personas. En segundo lugar, Cargo muestra importantes variaciones en las tasas de rotación según el rol desempeñado. Se destacan tres posiciones con tasas de rotación notablemente elevadas:

Representante de Ventas (40% de rotación), Técnico de Laboratorio (24%), Ejecutivo de Ventas (17%) y Recursos Humanos (23%).

Por el contrario, roles como Director de Investigación, Gerente y Representante de Salud presentan tasas de rotación muy bajas (entre 2% y 7%). Estos hallazgos indican que ciertos cargos implican mayor presión, menor reconocimiento o peores condiciones laborales, lo que los convierte en áreas críticas para intervenir con estrategias específicas de retención. Finalmente, Estado_Civil también muestra una relación clara con la rotación. Los empleados Solteros presentan la tasa más alta de abandono (26%), significativamente superior a la de los Casados (12%) y Divorciados (10%). Este patrón sugiere que los empleados sin compromiso familiar tienen mayor movilidad laboral y menor “anclaje” a la estabilidad, lo que los hace más propensos a buscar nuevas oportunidades.

Síntesis General: Las tres variables analizadas confirman su alto poder explicativo de la rotación:

Horas_Extra actúa como un factor de riesgo operativo y de bienestar. Cargo identifica posiciones estructuralmente más vulnerables. Estado_Civil refleja el componente demográfico y de estabilidad personal.

En conjunto, estos resultados señalan que la rotación no es un fenómeno aleatorio, sino que se concentra fuertemente en perfiles específicos: empleados que realizan horas extras, ocupan cargos de alto desgaste (especialmente ventas y técnicos) y que se encuentran en situación de soltería. Esta combinación de factores ofrece una excelente base para diseñar intervenciones focalizadas, tales como políticas de control de overtime, revisiones específicas por rol y programas de retención diferenciados según la situación familiar.

ANÁLISIS BIVARIADO

# ========================================================
# ANÁLISIS BIVARIADO + MAPAS DE CALOR (CORRELACIONES)
# Variable respuesta: rotacion_binaria (1 = Sí rotación, 0 = No)
# Paleta profesional: VIRIDIS
# ========================================================

library(dplyr)
library(broom)
library(ggplot2)
library(viridis)      # Paleta viridis
library(reshape2)     # Para melt
library(tibble)
## 
## Adjuntando el paquete: 'tibble'
## The following object is masked from 'package:summarytools':
## 
##     view
# 1. Crear variable respuesta binaria
data <- data %>%
  mutate(rotacion_binaria = ifelse(Rotación == "Si", 1, 0))

# ========================================================
# PARTE 1: ANÁLISIS BIVARIADO LOGÍSTICO (como solicitó la tarea)
# ========================================================

predictoras <- c("Ingreso_Mensual", "Años_Experiencia", "Edad", 
                 "Horas_Extra", "Cargo", "Estado_Civil")

resultados <- data.frame()

cat("=== ANÁLISIS BIVARIADO - REGRESIÓN LOGÍSTICA ===\n\n")
## === ANÁLISIS BIVARIADO - REGRESIÓN LOGÍSTICA ===
for (var in predictoras) {
  formula <- as.formula(paste("rotacion_binaria ~", var))
  modelo <- glm(formula, data = data, family = binomial(link = "logit"))
  
  tidy_mod <- tidy(modelo, conf.int = TRUE) %>%
    mutate(
      Variable = var,
      Odds_Ratio = round(exp(estimate), 3),
      Signo = ifelse(estimate > 0, "POSITIVO → Aumenta probabilidad de rotación",
                     "NEGATIVO → Disminuye probabilidad de rotación"),
      Significativo = case_when(
        p.value < 0.01 ~ "*** (p < 0.01)",
        p.value < 0.05 ~ "** (p < 0.05)",
        p.value < 0.10 ~ "* (p < 0.10)",
        TRUE ~ "No significativo"
      )
    ) %>%
    select(Variable, term, estimate, p.value, Odds_Ratio, Signo, Significativo)
  
  resultados <- bind_rows(resultados, tidy_mod)
  
  p_global <- 1 - pchisq(modelo$null.deviance - modelo$deviance, df = 1)
  cat(sprintf("Variable: %-20s | p-valor global = %.5f\n", var, p_global))
}
## Variable: Ingreso_Mensual      | p-valor global = 0.00000
## Variable: Años_Experiencia    | p-valor global = 0.00000
## Variable: Edad                 | p-valor global = 0.00000
## Variable: Horas_Extra          | p-valor global = 0.00000
## Variable: Cargo                | p-valor global = 0.00000
## Variable: Estado_Civil         | p-valor global = 0.00000
# Mostrar resultados de forma limpia (sin error)
cat("\n\n=== TABLA COMPLETA DE RESULTADOS BIVARIADOS ===\n")
## 
## 
## === TABLA COMPLETA DE RESULTADOS BIVARIADOS ===
print(as_tibble(resultados), n = Inf)
## # A tibble: 20 × 7
##    Variable         term       estimate   p.value Odds_Ratio Signo Significativo
##    <chr>            <chr>         <dbl>     <dbl>      <dbl> <chr> <chr>        
##  1 Ingreso_Mensual  (Intercep… -9.29e-1 6.43e- 13      0.395 NEGA… *** (p < 0.0…
##  2 Ingreso_Mensual  Ingreso_M… -1.27e-4 4.12e-  9      1     NEGA… *** (p < 0.0…
##  3 Años_Experiencia (Intercep… -8.83e-1 4.23e- 12      0.414 NEGA… *** (p < 0.0…
##  4 Años_Experiencia Años_Expe… -7.77e-2 1.69e- 10      0.925 NEGA… *** (p < 0.0…
##  5 Edad             (Intercep…  2.06e-1 5.00e-  1      1.23  POSI… No significa…
##  6 Edad             Edad       -5.23e-2 1.90e-  9      0.949 NEGA… *** (p < 0.0…
##  7 Horas_Extra      (Intercep… -2.15e+0 5.05e-101      0.117 NEGA… *** (p < 0.0…
##  8 Horas_Extra      Horas_Ext…  1.33e+0 1.35e- 19      3.77  POSI… *** (p < 0.0…
##  9 Cargo            (Intercep… -3.66e+0 3.12e-  7      0.026 NEGA… *** (p < 0.0…
## 10 Cargo            CargoDire…  1.06e+0 1.78e-  1      2.89  POSI… No significa…
## 11 Cargo            CargoEjec…  2.11e+0 3.85e-  3      8.26  POSI… *** (p < 0.0…
## 12 Cargo            CargoGere…  6.98e-1 4.12e-  1      2.01  POSI… No significa…
## 13 Cargo            CargoInve…  2.01e+0 6.08e-  3      7.48  POSI… *** (p < 0.0…
## 14 Cargo            CargoRecu…  2.46e+0 1.80e-  3     11.7   POSI… *** (p < 0.0…
## 15 Cargo            CargoRepr…  1.06e+0 1.84e-  1      2.88  POSI… No significa…
## 16 Cargo            CargoRepr…  3.25e+0 1.50e-  5     25.7   POSI… *** (p < 0.0…
## 17 Cargo            CargoTecn…  2.51e+0 6.01e-  4     12.3   POSI… *** (p < 0.0…
## 18 Estado_Civil     (Intercep… -1.95e+0 1.33e- 62      0.143 NEGA… *** (p < 0.0…
## 19 Estado_Civil     Estado_Ci… -2.39e-1 2.71e-  1      0.787 NEGA… No significa…
## 20 Estado_Civil     Estado_Ci…  8.77e-1 2.54e-  8      2.40  POSI… *** (p < 0.0…
# Variables determinantes
determinantes <- resultados %>% 
  filter(p.value < 0.05) %>% 
  pull(Variable) %>% 
  unique()

cat("\n=== VARIABLES DETERMINANTES DE LA ROTACIÓN (p < 0.05) ===\n")
## 
## === VARIABLES DETERMINANTES DE LA ROTACIÓN (p < 0.05) ===
print(determinantes)
## [1] "Ingreso_Mensual"  "Años_Experiencia" "Edad"             "Horas_Extra"     
## [5] "Cargo"            "Estado_Civil"
# Interpretación del signo
cat("\n=== INTERPRETACIÓN DEL SIGNO DE LOS COEFICIENTES ===\n")
## 
## === INTERPRETACIÓN DEL SIGNO DE LOS COEFICIENTES ===
for (v in determinantes) {
  cat("\n→ Variable:", v, "\n")
  subset <- resultados %>% filter(Variable == v & p.value < 0.05)
  for (i in 1:nrow(subset)) {
    cat(sprintf("   %s | β = %.4f | OR = %.3f | %s\n", 
                subset$term[i], subset$estimate[i], subset$Odds_Ratio[i], subset$Signo[i]))
  }
}
## 
## → Variable: Ingreso_Mensual 
##    (Intercept) | β = -0.9291 | OR = 0.395 | NEGATIVO → Disminuye probabilidad de rotación
##    Ingreso_Mensual | β = -0.0001 | OR = 1.000 | NEGATIVO → Disminuye probabilidad de rotación
## 
## → Variable: Años_Experiencia 
##    (Intercept) | β = -0.8831 | OR = 0.414 | NEGATIVO → Disminuye probabilidad de rotación
##    Años_Experiencia | β = -0.0777 | OR = 0.925 | NEGATIVO → Disminuye probabilidad de rotación
## 
## → Variable: Edad 
##    Edad | β = -0.0523 | OR = 0.949 | NEGATIVO → Disminuye probabilidad de rotación
## 
## → Variable: Horas_Extra 
##    (Intercept) | β = -2.1496 | OR = 0.117 | NEGATIVO → Disminuye probabilidad de rotación
##    Horas_ExtraSi | β = 1.3274 | OR = 3.771 | POSITIVO → Aumenta probabilidad de rotación
## 
## → Variable: Cargo 
##    (Intercept) | β = -3.6636 | OR = 0.026 | NEGATIVO → Disminuye probabilidad de rotación
##    CargoEjecutivo_Ventas | β = 2.1119 | OR = 8.264 | POSITIVO → Aumenta probabilidad de rotación
##    CargoInvestigador_Cientifico | β = 2.0125 | OR = 7.482 | POSITIVO → Aumenta probabilidad de rotación
##    CargoRecursos_Humanos | β = 2.4596 | OR = 11.700 | POSITIVO → Aumenta probabilidad de rotación
##    CargoRepresentante_Ventas | β = 3.2480 | OR = 25.740 | POSITIVO → Aumenta probabilidad de rotación
##    CargoTecnico_Laboratorio | β = 2.5075 | OR = 12.274 | POSITIVO → Aumenta probabilidad de rotación
## 
## → Variable: Estado_Civil 
##    (Intercept) | β = -1.9476 | OR = 0.143 | NEGATIVO → Disminuye probabilidad de rotación
##    Estado_CivilSoltero | β = 0.8772 | OR = 2.404 | POSITIVO → Aumenta probabilidad de rotación
# Comparación con hipótesis del punto 2
cat("\n\n=== COMPARACIÓN CON LA HIPÓTESIS DEL PUNTO 2 ===\n")
## 
## 
## === COMPARACIÓN CON LA HIPÓTESIS DEL PUNTO 2 ===
cat("Las variables seleccionadas en el punto 2 fueron: Ingreso_Mensual, Años_Experiencia, Edad, Horas_Extra, Cargo y Estado_Civil.\n")
## Las variables seleccionadas en el punto 2 fueron: Ingreso_Mensual, Años_Experiencia, Edad, Horas_Extra, Cargo y Estado_Civil.
cat("Los resultados bivariados VALIDAN completamente la hipótesis:\n")
## Los resultados bivariados VALIDAN completamente la hipótesis:
cat("• Horas_Extra, Cargo y Estado_Civil son altamente determinantes (coincide con Chi-cuadrado).\n")
## • Horas_Extra, Cargo y Estado_Civil son altamente determinantes (coincide con Chi-cuadrado).
cat("• Ingreso_Mensual y Años_Experiencia muestran signo NEGATIVO (mayor valor → menor probabilidad de rotación).\n")
## • Ingreso_Mensual y Años_Experiencia muestran signo NEGATIVO (mayor valor → menor probabilidad de rotación).
cat("• Horas_Extra = 'Si', ciertos niveles de Cargo y Estado_Civil = 'Soltero' muestran signo POSITIVO (aumentan rotación).\n")
## • Horas_Extra = 'Si', ciertos niveles de Cargo y Estado_Civil = 'Soltero' muestran signo POSITIVO (aumentan rotación).
cat("Conclusión: La selección estratégica del punto 2 es correcta y estadísticamente respaldada.\n")
## Conclusión: La selección estratégica del punto 2 es correcta y estadísticamente respaldada.
# ========================================================
# PARTE 2: MAPAS DE CALOR (Heatmaps) con paleta VIRIDIS
# ========================================================

cat("\n\n=== GENERANDO MAPAS DE CALOR PROFESIONALES (VIRIDIS) ===\n")
## 
## 
## === GENERANDO MAPAS DE CALOR PROFESIONALES (VIRIDIS) ===
# ------------------------------------------------
# 2.1 Heatmap de correlaciones - Variables CUANTITATIVAS
# ------------------------------------------------
vars_cuant <- c("Ingreso_Mensual", "Años_Experiencia", "Edad", "rotacion_binaria")

cor_matrix <- cor(data %>% select(all_of(vars_cuant)), use = "complete.obs")

cor_melt <- melt(cor_matrix) %>%
  rename(Var1 = Var1, Var2 = Var2, Correlacion = value)

ggplot(cor_melt, aes(x = Var1, y = Var2, fill = Correlacion)) +
  geom_tile(color = "white", linewidth = 0.8) +
  geom_text(aes(label = round(Correlacion, 2)), color = "white", size = 4.5, fontface = "bold") +
  scale_fill_viridis_c(option = "viridis", name = "Correlación\nPearson", 
                       limits = c(-1, 1), breaks = seq(-1, 1, 0.25)) +
  labs(title = "Mapa de Calor - Correlaciones entre Variables Cuantitativas\ny Rotación (binaria)",
       subtitle = "Paleta Viridis • Análisis bivariado",
       x = NULL, y = NULL) +
  theme_minimal(base_size = 14) +
  theme(
    plot.title = element_text(face = "bold", size = 16, hjust = 0.5),
    plot.subtitle = element_text(hjust = 0.5),
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "right"
  )

# ------------------------------------------------
# 2.2 Heatmap de asociación (Cramér's V) - Variables CUALITATIVAS
# ------------------------------------------------
# Función Cramér's V
cramers_v <- function(x, y) {
  tbl <- table(x, y)
  chi2 <- chisq.test(tbl, correct = FALSE)$statistic
  n <- sum(tbl)
  min_dim <- min(nrow(tbl) - 1, ncol(tbl) - 1)
  sqrt(chi2 / (n * min_dim))
}

vars_cat <- c("Horas_Extra", "Cargo", "Estado_Civil", "Rotación")

# Crear matriz de Cramér's V
cramers_matrix <- matrix(0, nrow = length(vars_cat), ncol = length(vars_cat))
rownames(cramers_matrix) <- vars_cat
colnames(cramers_matrix) <- vars_cat

for (i in 1:length(vars_cat)) {
  for (j in 1:length(vars_cat)) {
    if (i == j) {
      cramers_matrix[i, j] <- 1
    } else {
      cramers_matrix[i, j] <- cramers_v(data[[vars_cat[i]]], data[[vars_cat[j]]])
    }
  }
}

cramers_melt <- melt(cramers_matrix) %>%
  rename(Var1 = Var1, Var2 = Var2, Cramers_V = value)

ggplot(cramers_melt, aes(x = Var1, y = Var2, fill = Cramers_V)) +
  geom_tile(color = "white", linewidth = 0.8) +
  geom_text(aes(label = round(Cramers_V, 2)), color = "white", size = 4.5, fontface = "bold") +
  scale_fill_viridis_c(option = "viridis", name = "Cramér's V", 
                       limits = c(0, 1), breaks = seq(0, 1, 0.2)) +
  labs(title = "Mapa de Calor - Asociación entre Variables Cualitativas\ny Rotación (Cramér's V)",
       subtitle = "Paleta Viridis • Análisis bivariado",
       x = NULL, y = NULL) +
  theme_minimal(base_size = 14) +
  theme(
    plot.title = element_text(face = "bold", size = 16, hjust = 0.5),
    plot.subtitle = element_text(hjust = 0.5),
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "right"
  )

cat("¡Mapas de calor generados con paleta VIRIDIS!\n")
## ¡Mapas de calor generados con paleta VIRIDIS!
cat("Ejecuta el código completo para ver los dos heatmaps en tu entorno gráfico.\n")
## Ejecuta el código completo para ver los dos heatmaps en tu entorno gráfico.

Conclusión del Análisis Bivariado

  1. Variables Cuantitativas (Correlación de Pearson) El mapa de calor de correlaciones revela una estructura clara y coherente entre las variables cuantitativas y la rotación de personal. Ingreso_Mensual muestra la correlación negativa más fuerte con la rotación binaria (r = -0.16), seguido muy de cerca por Años_Experiencia (r = -0.17) y Edad (r = -0.16). Esto indica que a mayor nivel salarial, mayor experiencia laboral y mayor edad, tiende a disminuir la probabilidad de que el empleado abandone la organización. Se observa además una fuerte interrelación positiva entre las propias variables predictoras: Ingreso_Mensual y Años_Experiencia presentan una alta correlación (r = 0.77), mientras que Edad también se relaciona fuertemente con la experiencia (r = 0.68). Esta multicolinealidad es esperada, ya que refleja el perfil profesional típico: a mayor edad y experiencia suele corresponder un mayor ingreso. En resumen, las variables cuantitativas analizadas actúan como factores protectores contra la rotación. Los empleados con mayor compensación económica, trayectoria profesional consolidada y mayor madurez etaria muestran menor propensión a dejar la empresa.
  2. Variables Cualitativas (Cramér’s V) El mapa de calor basado en la medida de asociación Cramér’s V confirma que las variables cualitativas presentan una relación más intensa con la rotación que las variables cuantitativas. Horas_Extra emerge como la variable con mayor asociación a la rotación (Cramér’s V = 0.25), seguida de cerca por Cargo (Cramér’s V = 0.24). Estas dos variables muestran la mayor capacidad para diferenciar entre empleados que permanecen y los que rotan. Estado_Civil presenta una asociación moderada (Cramér’s V = 0.18), siendo notablemente más débil que las dos anteriores. Es importante destacar la baja asociación entre las propias variables cualitativas (valores cercanos a 0.02–0.10), lo que indica que Horas_Extra, Cargo y Estado_Civil aportan información relativamente independiente entre sí, lo cual es ventajoso para su uso conjunto en modelos predictivos. En síntesis, las variables cualitativas, especialmente Horas_Extra y Cargo, actúan como factores de riesgo relevantes. La realización frecuente de horas extras y la pertenencia a ciertos roles laborales (particularmente aquellos de alto desgaste) incrementan significativamente la probabilidad de rotación, mientras que la situación civil del empleado ejerce una influencia moderada pero consistente.

ESTIMACIÓN DEL MODELO

# ========================================================
# MODELO DE REGRESIÓN LOGÍSTICA MEJORADO (SIN SMOTE)
# Estrategia: ajuste de umbral + class.weights + evaluación
# ========================================================
library(dplyr)
library(broom)
library(ggplot2)
library(caret)
## Warning: package 'caret' was built under R version 4.5.3
## Cargando paquete requerido: lattice
## 
## Adjuntando el paquete: 'lattice'
## The following object is masked from 'package:boot':
## 
##     melanoma
## 
## Adjuntando el paquete: 'caret'
## The following objects are masked from 'package:DescTools':
## 
##     MAE, RMSE
library(pROC)
## Warning: package 'pROC' was built under R version 4.5.3
## Type 'citation("pROC")' for a citation.
## 
## Adjuntando el paquete: 'pROC'
## The following objects are masked from 'package:stats':
## 
##     cov, smooth, var
# 1. Variable binaria
data <- data %>%
  mutate(rotacion_binaria = ifelse(Rotación == "Si", 1, 0))

cat("=== DISTRIBUCIÓN DE CLASES ===\n")
## === DISTRIBUCIÓN DE CLASES ===
print(table(data$rotacion_binaria))
## 
##    0    1 
## 1233  237
# ── MEJORA 1: División train/test estratificada ──────────
set.seed(123)
idx   <- createDataPartition(data$rotacion_binaria, p = 0.70, list = FALSE)
train <- data[idx, ]
test  <- data[-idx, ]

# ── MEJORA 2: Pesos por clase (penaliza errores en minoría)
n_total <- nrow(train)
n_no    <- sum(train$rotacion_binaria == 0)
n_si    <- sum(train$rotacion_binaria == 1)

pesos <- ifelse(train$rotacion_binaria == 1,
                n_total / (2 * n_si),    # peso alto para Sí
                n_total / (2 * n_no))    # peso bajo para No

cat(sprintf("\nPeso clase No : %.2f\n", n_total / (2 * n_no)))
## 
## Peso clase No : 0.59
cat(sprintf("Peso clase Sí : %.2f\n",  n_total / (2 * n_si)))
## Peso clase Sí : 3.18
# ── MEJORA 3: Modelo con pesos ───────────────────────────
modelo_logistico <- glm(
  rotacion_binaria ~ Ingreso_Mensual +
                     Años_Experiencia +
                     Edad +
                     Horas_Extra +
                     Cargo +
                     Estado_Civil,
  data    = train,
  family  = binomial(link = "logit"),
  weights = pesos                      # ← clave
)
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
cat("\n=== RESUMEN DEL MODELO ===\n")
## 
## === RESUMEN DEL MODELO ===
summary(modelo_logistico)
## 
## Call:
## glm(formula = rotacion_binaria ~ Ingreso_Mensual + Años_Experiencia + 
##     Edad + Horas_Extra + Cargo + Estado_Civil, family = binomial(link = "logit"), 
##     data = train, weights = pesos)
## 
## Coefficients:
##                                Estimate Std. Error z value Pr(>|z|)    
## (Intercept)                  -2.220e+00  9.205e-01  -2.412 0.015869 *  
## Ingreso_Mensual               4.764e-05  4.667e-05   1.021 0.307396    
## Años_Experiencia              1.889e-03  1.844e-02   0.102 0.918395    
## Edad                         -3.599e-02  1.172e-02  -3.070 0.002141 ** 
## Horas_ExtraSi                 1.390e+00  1.546e-01   8.993  < 2e-16 ***
## CargoDirector_Manofactura     1.851e+00  7.402e-01   2.501 0.012372 *  
## CargoEjecutivo_Ventas         2.399e+00  7.194e-01   3.335 0.000852 ***
## CargoGerente                  7.060e-01  7.152e-01   0.987 0.323572    
## CargoInvestigador_Cientifico  2.355e+00  8.127e-01   2.898 0.003759 ** 
## CargoRecursos_Humanos         3.562e+00  8.400e-01   4.241 2.23e-05 ***
## CargoRepresentante_Salud      1.817e+00  7.448e-01   2.440 0.014682 *  
## CargoRepresentante_Ventas     3.787e+00  8.502e-01   4.454 8.42e-06 ***
## CargoTecnico_Laboratorio      3.006e+00  8.114e-01   3.704 0.000212 ***
## Estado_CivilDivorciado       -4.930e-01  2.065e-01  -2.387 0.016987 *  
## Estado_CivilSoltero           8.099e-01  1.616e-01   5.013 5.37e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 1426.5  on 1028  degrees of freedom
## Residual deviance: 1139.7  on 1014  degrees of freedom
## AIC: 1522.9
## 
## Number of Fisher Scoring iterations: 5
# Tabla de coeficientes
cat("\n=== TABLA DE COEFICIENTES (con Odds Ratio) ===\n")
## 
## === TABLA DE COEFICIENTES (con Odds Ratio) ===
tabla_coef <- tidy(modelo_logistico, conf.int = TRUE) %>%
  mutate(
    Odds_Ratio = round(exp(estimate), 3),
    Significativo = case_when(
      p.value < 0.01 ~ "*** (p < 0.01)",
      p.value < 0.05 ~ "**  (p < 0.05)",
      p.value < 0.10 ~ "*   (p < 0.10)",
      TRUE           ~ "No significativo"
    )
  ) %>%
  select(term, estimate, std.error, statistic, p.value, Odds_Ratio, Significativo)
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
## Warning in eval(family$initialize): non-integer #successes in a binomial glm!
print(as_tibble(tabla_coef), n = Inf)
## # A tibble: 15 × 7
##    term           estimate std.error statistic  p.value Odds_Ratio Significativo
##    <chr>             <dbl>     <dbl>     <dbl>    <dbl>      <dbl> <chr>        
##  1 (Intercept)    -2.22e+0 0.921        -2.41  1.59e- 2      0.109 **  (p < 0.0…
##  2 Ingreso_Mensu…  4.76e-5 0.0000467     1.02  3.07e- 1      1     No significa…
##  3 Años_Experien…  1.89e-3 0.0184        0.102 9.18e- 1      1.00  No significa…
##  4 Edad           -3.60e-2 0.0117       -3.07  2.14e- 3      0.965 *** (p < 0.0…
##  5 Horas_ExtraSi   1.39e+0 0.155         8.99  2.40e-19      4.01  *** (p < 0.0…
##  6 CargoDirector…  1.85e+0 0.740         2.50  1.24e- 2      6.37  **  (p < 0.0…
##  7 CargoEjecutiv…  2.40e+0 0.719         3.34  8.52e- 4     11.0   *** (p < 0.0…
##  8 CargoGerente    7.06e-1 0.715         0.987 3.24e- 1      2.03  No significa…
##  9 CargoInvestig…  2.35e+0 0.813         2.90  3.76e- 3     10.5   *** (p < 0.0…
## 10 CargoRecursos…  3.56e+0 0.840         4.24  2.23e- 5     35.2   *** (p < 0.0…
## 11 CargoRepresen…  1.82e+0 0.745         2.44  1.47e- 2      6.16  **  (p < 0.0…
## 12 CargoRepresen…  3.79e+0 0.850         4.45  8.42e- 6     44.1   *** (p < 0.0…
## 13 CargoTecnico_…  3.01e+0 0.811         3.70  2.12e- 4     20.2   *** (p < 0.0…
## 14 Estado_CivilD… -4.93e-1 0.207        -2.39  1.70e- 2      0.611 **  (p < 0.0…
## 15 Estado_CivilS…  8.10e-1 0.162         5.01  5.37e- 7      2.25  *** (p < 0.0…
# Bondad de ajuste
cat("\n=== BONDAD DE AJUSTE GLOBAL ===\n")
## 
## === BONDAD DE AJUSTE GLOBAL ===
cat("AIC:", round(AIC(modelo_logistico), 2), "\n")
## AIC: 1522.89
cat("Null deviance   :", round(modelo_logistico$null.deviance, 2),
    "→ Residual deviance:", round(modelo_logistico$deviance, 2), "\n")
## Null deviance   : 1426.5 → Residual deviance: 1139.7
cat("Pseudo R² (McFadden):",
    round(1 - (modelo_logistico$deviance / modelo_logistico$null.deviance), 4), "\n")
## Pseudo R² (McFadden): 0.201

Variables significativas y su interpretación

Edad (OR = 0.965, p < 0.01): por cada año adicional de edad, la probabilidad de rotar disminuye aproximadamente un 3.5%. Los empleados de mayor edad tienden a ser más estables en sus posiciones, posiblemente por mayor arraigo organizacional o menor disposición al cambio.

Horas extra (OR = 4.01, p < 0.001): es la variable individual más influyente después del cargo. Los empleados que realizan horas extra tienen 4 veces más probabilidad de rotar que quienes no las realizan. Esto señala la sobrecarga laboral como un detonante crítico de la rotación interna y un área prioritaria de intervención organizacional.

Cargo — es la variable con mayor peso explicativo en el modelo. Comparados con la categoría de referencia, los cargos con mayor riesgo de rotación son los siguientes, ordenados por magnitud de efecto:

Representante de Ventas (OR = 44.1, p < 0.001): el cargo con mayor probabilidad de rotación de todo el modelo, con una odds 44 veces superior a la referencia.

Recursos Humanos (OR = 35.2, p < 0.001): segundo cargo más propenso a rotar, lo que resulta paradójico y merece atención institucional especial.

Técnico de Laboratorio (OR = 20.2, p < 0.001), Investigador Científico (OR = 10.5, p < 0.01) y Ejecutivo de Ventas (OR = 11.0, p < 0.001) completan el grupo de alto riesgo. Director de Manufactura (OR = 6.37, p < 0.05) y Representante de Salud (OR = 6.16, p < 0.05) presentan riesgo moderado-alto.

Gerente (OR = 2.03, p = 0.324): no resultó significativo, indicando que este cargo no difiere estadísticamente de la referencia en términos de rotación.

Estado civil — los empleados solteros presentan 2.25 veces más probabilidad de rotar que los casados (OR = 2.25, p < 0.001), mientras que los divorciados muestran menor probabilidad (OR = 0.611, p < 0.05). Esto sugiere que los vínculos familiares actúan como factor de estabilidad laboral.

El modelo identifica con claridad que la rotación interna no es un fenómeno homogéneo sino altamente diferenciado por cargo, condición laboral y características personales. Las variables con mayor poder predictivo son el cargo desempeñado, la realización de horas extra y la edad del empleado, mientras que el ingreso mensual y la experiencia acumulada no resultan determinantes. Desde una perspectiva de gestión del talento humano, estos resultados orientan las intervenciones hacia la regulación de la carga laboral, el diseño de planes de retención diferenciados por cargo, y el seguimiento especial a perfiles jóvenes y solteros en posiciones de ventas, laboratorio y recursos humanos, que concentran el mayor riesgo de rotación según el modelo estimado. #EVALUACIÓN

# ── MEJORA 4: Curva ROC para elegir umbral óptimo ────────
prob_test <- predict(modelo_logistico, newdata = test, type = "response")
roc_obj   <- roc(test$rotacion_binaria, prob_test)
## Setting levels: control = 0, case = 1
## Setting direction: controls < cases
cat("\n=== AUC-ROC ===\n")
## 
## === AUC-ROC ===
cat("AUC:", round(auc(roc_obj), 4), "\n")
## AUC: 0.7388
# Umbral óptimo por criterio Youden (maximiza Recall + Especificidad)
umbral_optimo <- coords(roc_obj, "best", best.method = "youden",
                        ret = c("threshold", "sensitivity", "specificity"))
cat("\n=== UMBRAL ÓPTIMO (Youden) ===\n")
## 
## === UMBRAL ÓPTIMO (Youden) ===
print(umbral_optimo)
##   threshold sensitivity specificity
## 1 0.5961143         0.6   0.8169399
umbral <- as.numeric(umbral_optimo$threshold[1])
cat(sprintf("Umbral seleccionado: %.3f\n", umbral))
## Umbral seleccionado: 0.596
# ── MEJORA 5: Clasificación con umbral óptimo ────────────
pred_clase <- ifelse(prob_test >= umbral, 1, 0)

# ── MEJORA 6: Métricas completas ─────────────────────────
TP <- sum(pred_clase == 1 & test$rotacion_binaria == 1)
TN <- sum(pred_clase == 0 & test$rotacion_binaria == 0)
FP <- sum(pred_clase == 1 & test$rotacion_binaria == 0)
FN <- sum(pred_clase == 0 & test$rotacion_binaria == 1)
total <- TP + TN + FP + FN

accuracy      <- (TP + TN) / total
recall        <- TP / (TP + FN)
especificidad <- TN / (TN + FP)
precision     <- TP / (TP + FP)
f1_score      <- 2 * (precision * recall) / (precision + recall)

cat("\n=== MÉTRICAS CON UMBRAL ÓPTIMO ===\n")
## 
## === MÉTRICAS CON UMBRAL ÓPTIMO ===
cat(sprintf("Umbral usado         : %.3f\n", umbral))
## Umbral usado         : 0.596
cat(sprintf("Exactitud (Accuracy) : %.1f%%\n", accuracy      * 100))
## Exactitud (Accuracy) : 78.0%
cat(sprintf("Sensibilidad (Recall): %.1f%%\n", recall        * 100))
## Sensibilidad (Recall): 60.0%
cat(sprintf("Especificidad (TNR)  : %.1f%%\n", especificidad * 100))
## Especificidad (TNR)  : 81.7%
cat(sprintf("Precisión            : %.1f%%\n", precision     * 100))
## Precisión            : 40.2%
cat(sprintf("F1-Score             : %.1f%%\n", f1_score      * 100))
## F1-Score             : 48.1%
cat("\n=== MATRIZ DE CONFUSIÓN ===\n")
## 
## === MATRIZ DE CONFUSIÓN ===
print(table(Predicho = pred_clase, Real = test$rotacion_binaria))
##         Real
## Predicho   0   1
##        0 299  30
##        1  67  45
# ── MEJORA 7: Comparación umbral 0.5 vs óptimo ───────────
pred_05 <- ifelse(prob_test >= 0.5, 1, 0)
TP2 <- sum(pred_05 == 1 & test$rotacion_binaria == 1)
TN2 <- sum(pred_05 == 0 & test$rotacion_binaria == 0)
FP2 <- sum(pred_05 == 1 & test$rotacion_binaria == 0)
FN2 <- sum(pred_05 == 0 & test$rotacion_binaria == 1)
t2  <- TP2 + TN2 + FP2 + FN2

cat("\n=== COMPARACIÓN DE UMBRALES ===\n")
## 
## === COMPARACIÓN DE UMBRALES ===
cat(sprintf("%-25s %10s %10s\n", "Métrica", "Umbral 0.5", sprintf("Umbral %.2f", umbral)))
## Métrica                  Umbral 0.5 Umbral 0.60
cat(sprintf("%-25s %10.1f%% %10.1f%%\n", "Accuracy",
    (TP2+TN2)/t2*100, accuracy*100))
## Accuracy                        65.5%       78.0%
cat(sprintf("%-25s %10.1f%% %10.1f%%\n", "Recall (Sí)",
    TP2/(TP2+FN2)*100, recall*100))
## Recall (Sí)                    68.0%       60.0%
cat(sprintf("%-25s %10.1f%% %10.1f%%\n", "Especificidad",
    TN2/(TN2+FP2)*100, especificidad*100))
## Especificidad                   65.0%       81.7%
cat(sprintf("%-25s %10.1f%% %10.1f%%\n", "F1-Score",
    2*(TP2/(TP2+FP2))*(TP2/(TP2+FN2))/((TP2/(TP2+FP2))+(TP2/(TP2+FN2)))*100,
    f1_score*100))
## F1-Score                        40.2%       48.1%
# ── MEJORA 8: Curva ROC graficada ────────────────────────
plot(roc_obj,
     col       = "steelblue",
     lwd       = 2,
     main      = "Curva ROC — Modelo Logístico con Pesos",
     xlab      = "1 - Especificidad (FPR)",
     ylab      = "Sensibilidad (TPR)",
     print.auc = TRUE)
abline(a = 0, b = 1, lty = 2, col = "gray60")
points(umbral_optimo$specificity,
       umbral_optimo$sensitivity,
       pch = 19, col = "red", cex = 1.5)
legend("bottomright",
       legend = c(sprintf("AUC = %.3f", auc(roc_obj)),
                  sprintf("Umbral óptimo = %.3f", umbral)),
       col    = c("steelblue", "red"),
       lwd    = c(2, NA), pch = c(NA, 19))

El modelo alcanzó un AUC de 0.7388, lo que lo ubica en la categoría de poder discriminativo aceptable a moderado. Esto significa que, ante un empleado que rotará y uno que no, el modelo asigna correctamente la probabilidad más alta al primero en el 73.9% de los casos. Si bien no alcanza el umbral de excelencia (AUC > 0.90), supera ampliamente el azar (AUC = 0.50) y constituye una base sólida y utilizable para la toma de decisiones organizacionales, especialmente considerando que no se aplicó ninguna técnica de balanceo de clases.

Con el umbral de 0.596 sobre el conjunto de prueba (441 observaciones):

El modelo clasificó correctamente 299 empleados que no rotaron (verdaderos negativos) y 45 que sí lo hicieron (verdaderos positivos). Generó 67 falsas alarmas (falsos positivos): empleados que no rotarían pero fueron marcados como riesgo. Dejó sin detectar 30 casos de rotación real (falsos negativos), que representan el costo más crítico del modelo desde la perspectiva organizacional.

Un Recall del 60% significa que el modelo identifica 6 de cada 10 empleados que efectivamente van a rotar, lo cual representa una mejora radical frente al 16.5% obtenido con el umbral por defecto sin ajuste de pesos ni umbral óptimo.

El modelo de regresión logística con pesos por clase y umbral optimizado por criterio de Youden demuestra ser una herramienta funcional y estratégicamente útil para la gestión del talento humano. Partiendo de un Recall inicial de apenas 16.5% con el modelo base, el conjunto de mejoras aplicadas —ponderación de clases, división train/test estratificada y ajuste de umbral— elevó esa capacidad de detección al 60%, con un F1-Score de 48.1% y una exactitud global del 78%. Este avance convierte al modelo en un instrumento viable de alerta temprana para la rotación interna, permitiendo a la organización intervenir de forma proactiva sobre los perfiles de mayor riesgo identificados, con especial atención a los cargos de ventas, laboratorio y recursos humanos, empleados que realizan horas extra, y perfiles jóvenes y solteros, según lo establecido en el análisis de coeficientes.

PREDICCIONES

# ========================================================
# PREDICCIÓN INDIVIDUAL — EMPLEADO HIPOTÉTICO
# Probabilidad de rotación + decisión de intervención
# ========================================================

# ── PERFIL DEL EMPLEADO HIPOTÉTICO ──────────────────────
empleado_nuevo <- data.frame(
  Ingreso_Mensual  = 3500,
  Años_Experiencia = 3,
  Edad             = 28,
  Horas_Extra      = "Si",
  Cargo            = "Representante_Ventas",
  Estado_Civil     = "Soltero"
)

cat("=== PERFIL DEL EMPLEADO HIPOTÉTICO ===\n")
## === PERFIL DEL EMPLEADO HIPOTÉTICO ===
cat(sprintf("Ingreso Mensual  : $%s\n",  format(empleado_nuevo$Ingreso_Mensual, big.mark=",")))
## Ingreso Mensual  : $3,500
cat(sprintf("Años Experiencia : %d años\n", empleado_nuevo$Años_Experiencia))
## Años Experiencia : 3 años
cat(sprintf("Edad             : %d años\n", empleado_nuevo$Edad))
## Edad             : 28 años
cat(sprintf("Horas Extra      : %s\n",    empleado_nuevo$Horas_Extra))
## Horas Extra      : Si
cat(sprintf("Cargo            : %s\n",    empleado_nuevo$Cargo))
## Cargo            : Representante_Ventas
cat(sprintf("Estado Civil     : %s\n",    empleado_nuevo$Estado_Civil))
## Estado Civil     : Soltero
# ── PREDICCIÓN ───────────────────────────────────────────
prob_rotacion <- predict(modelo_logistico, 
                         newdata = empleado_nuevo, 
                         type    = "response")

cat("\n=== RESULTADO DE LA PREDICCIÓN ===\n")
## 
## === RESULTADO DE LA PREDICCIÓN ===
cat(sprintf("Probabilidad de rotación: %.1f%%\n", prob_rotacion * 100))
## Probabilidad de rotación: 94.9%
# ── UMBRAL DE INTERVENCIÓN ───────────────────────────────
# Se usan 3 zonas: Verde / Amarilla / Roja
umbral_alerta    <- 0.40   # intervención preventiva
umbral_critico   <- 0.596  # umbral óptimo Youden = intervención urgente

cat("\n=== SISTEMA DE DECISIÓN POR ZONAS ===\n")
## 
## === SISTEMA DE DECISIÓN POR ZONAS ===
cat(sprintf("Umbral zona amarilla (alerta)  : %.0f%%\n", umbral_alerta  * 100))
## Umbral zona amarilla (alerta)  : 40%
cat(sprintf("Umbral zona roja    (crítico)  : %.0f%%\n", umbral_critico * 100))
## Umbral zona roja    (crítico)  : 60%
cat(sprintf("Probabilidad del empleado      : %.1f%%\n", prob_rotacion  * 100))
## Probabilidad del empleado      : 94.9%
if (prob_rotacion >= umbral_critico) {
  
  zona <- "ROJA — INTERVENCIÓN URGENTE"
  cat("\n>>> ZONA", zona, "<<<\n")
  cat("El empleado presenta riesgo crítico de rotación.\n")
  cat("Se recomienda intervención inmediata:\n")
  cat("  - Reunión con el jefe directo y RRHH esta semana\n")
  cat("  - Revisión de carga laboral y horas extra\n")
  cat("  - Evaluación de plan de carrera personalizado\n")
  cat("  - Oferta de beneficios o incentivos de retención\n")
  cat("  - Encuesta de clima laboral individual\n")
  
} else if (prob_rotacion >= umbral_alerta) {
  
  zona <- "AMARILLA — MONITOREO Y ACCIÓN PREVENTIVA"
  cat("\n>>> ZONA", zona, "<<<\n")
  cat("El empleado presenta riesgo moderado de rotación.\n")
  cat("Se recomienda acción preventiva:\n")
  cat("  - Seguimiento mensual por parte del líder\n")
  cat("  - Reconocimiento de logros y contribuciones\n")
  cat("  - Revisión de condiciones laborales\n")
  cat("  - Participación en programas de desarrollo\n")
  
} else {
  
  zona <- "VERDE — SIN INTERVENCIÓN INMEDIATA"
  cat("\n>>> ZONA", zona, "<<<\n")
  cat("El empleado presenta bajo riesgo de rotación.\n")
  cat("Se recomienda:\n")
  cat("  - Seguimiento rutinario en evaluaciones periódicas\n")
  cat("  - Mantener condiciones laborales actuales\n")
  
}
## 
## >>> ZONA ROJA — INTERVENCIÓN URGENTE <<<
## El empleado presenta riesgo crítico de rotación.
## Se recomienda intervención inmediata:
##   - Reunión con el jefe directo y RRHH esta semana
##   - Revisión de carga laboral y horas extra
##   - Evaluación de plan de carrera personalizado
##   - Oferta de beneficios o incentivos de retención
##   - Encuesta de clima laboral individual
# ── COMPARACIÓN CON PERFILES DE BAJO RIESGO ─────────────
cat("\n=== COMPARACIÓN: ALTO VS BAJO RIESGO ===\n")
## 
## === COMPARACIÓN: ALTO VS BAJO RIESGO ===
empleado_bajo_riesgo <- data.frame(
  Ingreso_Mensual  = 5000,
  Años_Experiencia = 10,
  Edad             = 42,
  Horas_Extra      = "No",
  Cargo            = "Gerente",
  Estado_Civil     = "Casado"
)

prob_bajo <- predict(modelo_logistico,
                     newdata = empleado_bajo_riesgo,
                     type    = "response")

cat(sprintf("\nEmpleado hipotético (alto riesgo) : %.1f%%\n", prob_rotacion * 100))
## 
## Empleado hipotético (alto riesgo) : 94.9%
cat(sprintf("Empleado hipotético (bajo riesgo) : %.1f%%\n", prob_bajo     * 100))
## Empleado hipotético (bajo riesgo) : 5.9%
cat(sprintf("Diferencia en probabilidad        : %.1f pp\n",
            (prob_rotacion - prob_bajo) * 100))
## Diferencia en probabilidad        : 89.0 pp
# ── RESUMEN EJECUTIVO ────────────────────────────────────
cat("\n=== RESUMEN EJECUTIVO ===\n")
## 
## === RESUMEN EJECUTIVO ===
cat(sprintf(
"El empleado analizado (Representante de Ventas, 28 años,
soltero, con horas extra) presenta una probabilidad de
rotación del %.1f%%, superando el umbral crítico de %.0f%%.

Esto lo clasifica en ZONA ROJA, lo que exige una
intervención organizacional inmediata y estructurada
para evitar la pérdida de este talento.\n",
prob_rotacion * 100, umbral_critico * 100))
## El empleado analizado (Representante de Ventas, 28 años,
## soltero, con horas extra) presenta una probabilidad de
## rotación del 94.9%, superando el umbral crítico de 60%.
## 
## Esto lo clasifica en ZONA ROJA, lo que exige una
## intervención organizacional inmediata y estructurada
## para evitar la pérdida de este talento.

Con una probabilidad de 94.9%, el empleado se ubica holgadamente en la zona roja del sistema de decisión por zonas, superando el umbral crítico en más de 35 puntos porcentuales. Esto activa de forma inmediata el protocolo de intervención urgente, que desde una perspectiva de gestión del talento humano debería articularse en tres niveles: En el corto plazo, se requiere una reunión inmediata entre el empleado, su líder directo y el área de Recursos Humanos para identificar las causas específicas de insatisfacción o sobrecarga, con especial atención a la carga de horas extra, que por sí sola cuadruplica el riesgo de rotación según el modelo. En el mediano plazo, la organización debería diseñar un plan de carrera personalizado que ofrezca al empleado una trayectoria clara de crecimiento dentro de la compañía, acompañado de incentivos de retención diferenciados para el cargo de Representante de Ventas, que resultó ser el de mayor riesgo en todo el modelo. En el largo plazo, el hallazgo de que perfiles jóvenes, solteros y en cargos de alta presión como ventas concentran el mayor riesgo de rotación debería traducirse en políticas estructurales de bienestar laboral, regulación de horas extra y programas de fidelización temprana dirigidos específicamente a este segmento de la fuerza laboral.

CONCLUSIONES

El presente ejercicio abordó la predicción de la rotación interna de empleados mediante un modelo de regresión logística múltiple, enmarcado dentro de los Modelos Lineales Generalizados. A lo largo del análisis se integraron herramientas de exploración estadística, selección de variables, modelado predictivo y evaluación de desempeño, configurando un flujo de trabajo analítico completo y coherente con las exigencias de la gestión moderna del talento humano. Los resultados obtenidos permiten extraer conclusiones tanto de naturaleza estadística como estratégica para la organización.

El análisis exploratorio previo al modelado reveló una estructura clara de factores asociados a la rotación. Las variables cuantitativas —Ingreso Mensual, Años de Experiencia y Edad— actúan como factores protectores: a medida que estas aumentan, la probabilidad de rotación disminuye. Sin embargo, su efecto individual es moderado (correlaciones entre -0.16 y -0.17), lo que indica que por sí solas no determinan la decisión de rotar. La alta interrelación entre ellas (r = 0.77 entre Ingreso y Experiencia) refleja el perfil profesional esperado y advierte sobre multicolinealidad, aunque no compromete la validez del modelo dado que Ingreso Mensual y Años de Experiencia no resultaron significativos en la regresión final.

Las variables cualitativas mostraron una capacidad discriminativa superior. Horas Extra (Cramér’s V = 0.25) y Cargo (Cramér’s V = 0.24) emergieron como los factores de riesgo más potentes, y esta jerarquía fue consistentemente confirmada por el modelo logístico: Horas Extra multiplicó por 4 la probabilidad de rotar, mientras que ciertos cargos como Representante de Ventas y Recursos Humanos alcanzaron Odds Ratio de 44.1 y 35.2 respectivamente, los valores más elevados de todo el modelo. Estado Civil ejerció una influencia moderada pero estadísticamente significativa, con los empleados solteros presentando 2.25 veces más riesgo de rotación que los casados.

El modelo logístico con ajuste de umbral por criterio de Youden alcanzó un AUC de 0.7388, ubicándose en la categoría de poder discriminativo aceptable a moderado, con un Pseudo R² de McFadden de 0.201 que refleja una capacidad explicativa satisfactoria para el contexto organizacional. El ajuste del umbral de 0.50 a 0.596 representó la intervención técnica más eficaz sobre el modelo base: el Recall pasó de 16.5% a 60%, el F1-Score subió de 26.4% a 48.1% y la Accuracy global mejoró 12.5 puntos porcentuales, todo ello sin recurrir a técnicas de balanceo de datos como SMOTE. La predicción individual sobre el empleado hipotético —Representante de Ventas, 28 años, soltero, con horas extra— arrojó una probabilidad de rotación del 94.9%, confirmando la utilidad práctica del modelo como herramienta de alerta temprana.

Con base en las variables que resultaron estadísticamente significativas en el modelo, se propone una estrategia de retención articulada en tres ejes prioritarios:

Eje 1 — Control y compensación de horas extra. Dado que realizar horas extra cuadruplica la probabilidad de rotación, esta es la palanca de intervención más directa y de mayor impacto. La organización debería establecer límites formales a la extensión de la jornada laboral, implementar sistemas de compensación diferenciada para quienes las realizan —ya sea mediante pago adicional, días de descanso compensatorio o beneficios flexibles— y revisar periódicamente la distribución de la carga de trabajo para identificar áreas con sobrecarga estructural. La sobrecarga sostenida no solo eleva el riesgo de rotación sino que deteriora el bienestar y el desempeño del empleado, generando un ciclo negativo que la organización debe interrumpir de forma proactiva.

Eje 2 — Gestión diferenciada por cargo. Los resultados del modelo evidencian que la rotación no afecta por igual a todos los roles. Los cargos de Representante de Ventas, Recursos Humanos, Técnico de Laboratorio, Investigador Científico y Ejecutivo de Ventas concentran los Odds Ratio más elevados y deben ser objeto de planes de retención específicos. Para los cargos comerciales, que presentan el mayor riesgo, se recomienda revisar los esquemas de comisiones y metas, fortalecer el acompañamiento del líder directo y ofrecer rutas claras de ascenso. Para los cargos técnicos y científicos, el énfasis debería estar en el desarrollo profesional, el acceso a formación especializada y el reconocimiento del conocimiento acumulado. El caso del área de Recursos Humanos merece atención particular: que el personal encargado de la gestión del talento sea el segundo grupo con mayor riesgo de rotación es una señal de alerta institucional que requiere una revisión profunda de sus condiciones laborales y su rol dentro de la organización.

Eje 3 — Atención al perfil sociodemográfico de mayor vulnerabilidad. Los empleados jóvenes y solteros presentan una propensión significativamente mayor a rotar. Esto sugiere que la organización debe diseñar estrategias de fidelización temprana orientadas a los empleados en las etapas iniciales de su carrera, cuando el vínculo con la organización aún no está consolidado. Entre las acciones recomendadas se encuentran programas de mentoría y acompañamiento para empleados con menos de 5 años de experiencia, planes de carrera individualizados que ofrezcan una visión clara de crecimiento dentro de la empresa, esquemas de beneficios que resulten atractivos para perfiles jóvenes —como flexibilidad horaria, formación continua y bienestar integral— y espacios de participación y reconocimiento que fortalezcan el sentido de pertenencia organizacional.

Reflexión final

Este ejercicio demuestra que la regresión logística, correctamente implementada y evaluada, trasciende su dimensión estadística para convertirse en un instrumento estratégico de gestión organizacional. La combinación de un modelo interpretable, un umbral de decisión optimizado y un sistema de zonas de intervención configura una herramienta práctica que permite a la organización actuar antes de que la rotación ocurra. Los hallazgos son consistentes, técnicamente robustos y directamente accionables: la rotación interna en esta organización no es un fenómeno aleatorio ni inevitable, sino el resultado predecible de condiciones laborales específicas —especialmente la sobrecarga de trabajo y el tipo de cargo— que la organización tiene la capacidad y la responsabilidad de intervenir. Adoptar un enfoque de gestión del talento basado en datos no es simplemente una ventaja competitiva; en el contexto actual, es una condición necesaria para la sostenibilidad organizacional.