# La instalación del entorno se documenta en el Anexo A.
library(paqueteMODELOS) # base de datos `rotacion`
library(dplyr) # manipulación de datos
library(tidyr) # reestructuración
library(ggplot2) # gráficos
library(kableExtra) # formato de tablas
library(car) # Anova tipo II (LR) y GVIF
library(pROC) # ROC, AUC e IC de DeLong
library(rms) # bootstrap de optimismo (validate)
library(e1071) # asimetría y curtosis
library(nortest) # Anderson-Darling# ---- Paleta base del informe
COL_JAV <- "#003087"; COL_AZUL <- "#0073B1"; COL_GRIS <- "#D4D4D4"
COL_NAR <- "#E8833A"; COL_TXT <- "#4D4D4D"
# ---- Paleta ampliada.
# Se construye sobre el eje azul-naranja de la identidad visual, que es una
# pareja complementaria (matices ~205 y ~27 grados). Se añaden los matices
# análogos de cada extremo: hacia el azul, el verde azulado y el violeta;
# hacia el naranja, la terracota y el ámbar. Todos se fijan a una saturación
# y una luminosidad comparables, para que ningún color domine por brillo.
COL_TEAL <- "#1B8A8F" # análogo frío del azul (verde azulado)
COL_VIO <- "#6A5D9E" # análogo frío del azul (violeta)
COL_TERR <- "#B4552F" # análogo cálido del naranja (terracota)
COL_AMB <- "#D9A22B" # análogo cálido del naranja (ámbar)
PALETA <- c(COL_AZUL, COL_NAR, COL_TEAL, COL_VIO, COL_TERR, COL_AMB, COL_JAV)
# Familias temáticas: el color codifica el contenido de la variable, no la
# decora. La misma familia conserva su matiz en todas las figuras.
FAMILIA <- c(
Edad = "Perfil personal", Distancia_Casa = "Perfil personal",
Genero = "Perfil personal", Estado_Civil = "Perfil personal",
Ingreso_Mensual = "Compensación", Porcentaje_aumento_salarial = "Compensación",
Antigüedad = "Tiempo y trayectoria", Antigüedad_Cargo = "Tiempo y trayectoria",
Años_acargo_con_mismo_jefe = "Tiempo y trayectoria",
Años_ultima_promoción = "Tiempo y trayectoria",
Años_Experiencia = "Tiempo y trayectoria",
Trabajos_Anteriores = "Formación y desarrollo",
Capacitaciones = "Formación y desarrollo",
Educación = "Formación y desarrollo", Campo_Educación = "Formación y desarrollo",
Departamento = "Estructura de la empresa", Cargo = "Estructura de la empresa",
Horas_Extra = "Condiciones de trabajo",
`Viaje de Negocios` = "Condiciones de trabajo",
Viaje_Negocios = "Condiciones de trabajo",
Satisfacción_Ambiental = "Percepción del empleado",
Satisfación_Laboral = "Percepción del empleado",
Equilibrio_Trabajo_Vida = "Percepción del empleado",
Rendimiento_Laboral = "Percepción del empleado",
Rotación = "Respuesta")
COL_FAMILIA <- c(`Perfil personal` = COL_VIO, `Compensación` = COL_NAR,
`Tiempo y trayectoria` = COL_AZUL, `Formación y desarrollo` = COL_TEAL,
`Estructura de la empresa` = COL_JAV, `Condiciones de trabajo` = COL_TERR,
`Percepción del empleado` = COL_AMB, Respuesta = COL_NAR)
# Degradado de un mismo matiz, para los niveles ordenados de una ordinal
degradado <- function(col, k) colorRampPalette(c(colorspace_claro(col), col))(k)
colorspace_claro <- function(col, f = 0.72) {
rgb1 <- grDevices::col2rgb(col)[, 1]
grDevices::rgb(t(rgb1 + (255 - rgb1) * f), maxColorValue = 255)
}
theme_set(
theme_minimal(base_size = 11) +
theme(plot.title = element_text(face = "bold", colour = COL_JAV),
plot.subtitle = element_text(colour = "grey45"),
axis.title = element_text(colour = "grey40"),
axis.text = element_text(colour = "grey40"),
panel.grid.minor = element_blank(),
panel.grid.major.x = element_blank(),
legend.position = "none")
)
# Etiquetas cortas de niveles para los ejes de los gráficos
ETQ_NIVEL <- function(x) dplyr::recode(x, No_Viaja = "No viaja", Frecuentemente = "Frecuente",
Raramente = "Rara vez", Si = "Sí")
# ---- Números: coma decimal y punto de millar
num <- function(x, d = 2) {
ifelse(is.na(x), "—",
formatC(round(x, d), format = "f", digits = d,
big.mark = ".", decimal.mark = ","))
}
ent <- function(x) formatC(x, format = "d", big.mark = ".", decimal.mark = ",")
pct <- function(x, d = 1) paste0(num(100 * x, d), "\u00a0%")
# ---- Valores p: nunca "0"; bajo la precisión de máquina, < 2e-16
pval <- function(p, d = 3) {
sapply(p, function(pi) {
if (is.na(pi)) return("—")
if (pi < 2.2e-16) return("$<2\\times10^{-16}$")
if (pi < 10^(-d)) {
e <- floor(log10(pi)); m <- pi / 10^e
return(sprintf("$%s\\times10^{%d}$", num(m, 2), e))
}
num(pi, d)
})
}
# ---- Celdas HTML: se escapan &, < y >, y el modo matemático $...$ pasa a \(...\),
# que MathJax procesa también dentro de las tablas
html_math <- function(x) {
x <- as.character(x)
# dentro de $...$ los signos < y > pasan a \\lt y \\gt, que MathJax interpreta sin
# depender del escape HTML
x <- vapply(x, function(si) {
if (is.na(si)) return(si)
m <- gregexpr("\\$[^$]*\\$", si)
regmatches(si, m) <- lapply(regmatches(si, m), function(z)
gsub(">", "\\\\gt ", gsub("<", "\\\\lt ", z)))
si
}, character(1), USE.NAMES = FALSE)
x <- gsub("&", "&", x, fixed = TRUE)
x <- gsub("<", "<", x, fixed = TRUE)
x <- gsub(">", ">", x, fixed = TRUE)
gsub("\\$([^$]*)\\$", "\\\\(\\1\\\\)", x)
}
# Versión para texto en línea, dentro de $...$: incluye el signo (= o <)
pvm <- function(p, d = 3) {
x <- pval(p, d)
ifelse(grepl("^\\$<", x), gsub("\\$", "", x), paste0("=", gsub("\\$", "", x)))
}
# Decisión de un contraste con alfa = 0,05
decide <- function(p, a = 0.05) ifelse(p < a, "Se rechaza $H_0$", "No se rechaza $H_0$")
# ---- Envoltura única para todas las tablas (mismo formato de las Actividades 1 y 2)
tabla <- function(df, caption, digits = 2, align = NULL, ...) {
df <- as.data.frame(df, check.names = FALSE)
digits <- rep_len(digits, ncol(df))
for (j in seq_along(df)) {
v <- df[[j]]
if (is.numeric(v)) {
df[[j]] <- if (all(is.na(v) | v == round(v))) ifelse(is.na(v), "—", ent(v))
else num(v, digits[j])
} else df[[j]] <- ifelse(is.na(v), "—", as.character(v))
}
if (is.null(align)) align <- ifelse(sapply(df, function(v)
all(grepl("^[-$0-9.,<>\\\\{}times% —]+$", v))), "r", "l")
for (j in seq_along(df)) df[[j]] <- html_math(df[[j]])
kbl(df, format = "html", escape = FALSE, caption = html_math(caption),
align = align, col.names = html_math(names(df)), row.names = FALSE) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE, position = "center", font_size = 13) %>%
row_spec(0, bold = TRUE, background = COL_JAV, color = "white")
}
# ---- Etiquetas de coeficientes: se identifican por nombre, nunca por posición
ETIQUETAS <- c(
"(Intercept)" = "Intercepto",
"Horas_ExtraSi" = "Horas extra: Sí vs. No",
"Estado_CivilDivorciado" = "Estado civil: Divorciado vs. Casado",
"Estado_CivilSoltero" = "Estado civil: Soltero vs. Casado",
"Viaje_NegociosRaramente" = "Viaje: Raramente vs. No viaja",
"Viaje_NegociosFrecuentemente" = "Viaje: Frecuentemente vs. No viaja",
"Edad" = "Edad (por año)",
"log(Ingreso_Mensual)" = "ln(Ingreso mensual)",
"Antigüedad_Cargo" = "Antigüedad en el cargo (por año)"
)
etiqueta <- function(n) {
faltan <- setdiff(n, names(ETIQUETAS))
if (length(faltan)) stop("Coeficiente sin etiqueta: ", paste(faltan, collapse = ", "))
unname(ETIQUETAS[n])
}data("rotacion")
crudo <- rotacion
# Tipología estadística de las 24 variables
NOMINALES <- c("Rotación", "Departamento", "Campo_Educación", "Genero", "Cargo",
"Estado_Civil", "Horas_Extra")
ORDINALES <- c("Viaje de Negocios", "Educación", "Satisfacción_Ambiental",
"Satisfación_Laboral", "Rendimiento_Laboral", "Equilibrio_Trabajo_Vida")
CONTINUAS <- c("Edad", "Ingreso_Mensual")
DISCRETAS <- c("Distancia_Casa", "Trabajos_Anteriores", "Porcentaje_aumento_salarial",
"Años_Experiencia", "Capacitaciones", "Antigüedad", "Antigüedad_Cargo",
"Años_ultima_promoción", "Años_acargo_con_mismo_jefe")
CUANTITATIVAS <- c(CONTINUAS, DISCRETAS)
stopifnot(setequal(c(NOMINALES, ORDINALES, CUANTITATIVAS), names(crudo)))
# Base de trabajo: factores con referencias explícitas y niveles ordinales en su orden
datos <- crudo %>%
rename(Viaje_Negocios = `Viaje de Negocios`) %>%
mutate(
y = as.integer(Rotación == "Si"),
Rotación = factor(Rotación, levels = c("No", "Si")),
Horas_Extra = relevel(factor(Horas_Extra), ref = "No"),
Estado_Civil = relevel(factor(Estado_Civil), ref = "Casado"),
Viaje_Negocios = factor(Viaje_Negocios,
levels = c("No_Viaja", "Raramente", "Frecuentemente")),
across(c(Departamento, Campo_Educación, Genero, Cargo), factor)
)
stopifnot(!anyNA(datos$Viaje_Negocios), !anyNA(datos$y))
N <- nrow(datos)
N1 <- sum(datos$y); N0 <- N - N1
# Etiquetas de las escalas ordinales según el diccionario de datos del curso
SATISF <- c("1" = "Muy insatisfecho", "2" = "Insatisfecho", "3" = "Satisfecho",
"4" = "Muy satisfecho")
ETQ_ORD <- list(
Educación = c("1" = "Primaria", "2" = "Secundaria", "3" = "Técnico/tecnólogo",
"4" = "Pregrado", "5" = "Posgrado"),
Satisfacción_Ambiental = SATISF,
Satisfación_Laboral = SATISF,
Rendimiento_Laboral = c("1" = "Bajo", "2" = "Medio", "3" = "Alto", "4" = "Muy alto"),
Equilibrio_Trabajo_Vida = c("1" = "Muy bajo", "2" = "Bajo", "3" = "Medio", "4" = "Alto"))
etq_ord <- function(v, x) {
e <- ETQ_ORD[[v]]
if (is.null(e)) as.character(x) else paste0(x, " · ", e[as.character(x)])
}# Núcleo de resultados que alimenta el resumen ejecutivo. Cada sección
# posterior reutiliza estos objetos y los desarrolla en detalle.
FORMULA <- y ~ Horas_Extra + Estado_Civil + Viaje_Negocios +
Edad + log(Ingreso_Mensual) + Antigüedad_Cargo
modelo <- glm(FORMULA, family = binomial(link = "logit"), data = datos)
k_param <- length(coef(modelo)) - 1
# Partición estratificada 70/30
set.seed(2026)
idx_tr <- sort(unlist(lapply(split(seq_len(N), datos$y),
function(i) sample(i, round(0.7 * length(i))))))
entren <- datos[idx_tr, ]; prueba <- datos[-idx_tr, ]
modelo_tr <- glm(FORMULA, family = binomial, data = entren)
p_tr <- predict(modelo_tr, entren, type = "response")
p_te <- predict(modelo_tr, prueba, type = "response")
roc_tr <- roc(entren$y, p_tr, levels = c(0, 1), direction = "<", quiet = TRUE)
roc_te <- roc(prueba$y, p_te, levels = c(0, 1), direction = "<", quiet = TRUE)
ic_te <- ci.auc(roc_te, method = "delong")
# Métricas de clasificación para un corte c
metricas <- function(y, p, c) {
pred <- as.integer(p >= c)
VP <- sum(pred == 1 & y == 1); FP <- sum(pred == 1 & y == 0)
VN <- sum(pred == 0 & y == 0); FN <- sum(pred == 0 & y == 1)
sens <- VP / (VP + FN); esp <- VN / (VN + FP)
vpp <- if (VP + FP > 0) VP / (VP + FP) else NA
vpn <- if (VN + FN > 0) VN / (VN + FN) else NA
data.frame(corte = c, Sens = sens, Esp = esp, VPP = vpp, VPN = vpn,
Exactitud = (VP + VN) / length(y),
F1 = if (is.na(vpp) || vpp + sens == 0) NA else 2 * vpp * sens / (vpp + sens),
Youden = sens + esp - 1)
}
REJILLA <- seq(0.05, 0.95, by = 0.01)
rej_tr <- bind_rows(lapply(REJILLA, function(c) metricas(entren$y, p_tr, c)))
rej_te <- bind_rows(lapply(REJILLA, function(c) metricas(prueba$y, p_te, c)))
c_youden <- rej_tr$corte[which.max(rej_tr$Youden)]
m_youden_te <- metricas(prueba$y, p_te, c_youden)
# Matriz de confusión y métricas completas para un corte c (positivo: Y = 1)
cm_metricas <- function(y, p, c) {
pred <- as.integer(p >= c); n <- length(y)
VP <- sum(pred == 1 & y == 1); FP <- sum(pred == 1 & y == 0)
VN <- sum(pred == 0 & y == 0); FN <- sum(pred == 0 & y == 1)
wil <- function(k, m) prop.test(k, m, correct = FALSE)$conf.int[1:2] # IC de Wilson
acc <- (VP + VN) / n
nir <- max(mean(y), 1 - mean(y)) # tasa de no información
sens <- VP / (VP + FN); esp <- VN / (VN + FP)
vpp <- VP / (VP + FP); vpn <- VN / (VN + FN)
pe <- ((VP + FP) * (VP + FN) + (VN + FN) * (VN + FP)) / n^2 # acuerdo por azar
list(
corte = c, cm = c(VP = VP, FP = FP, VN = VN, FN = FN),
acc = acc, acc_ic = binom.test(VP + VN, n)$conf.int[1:2], # IC de Clopper-Pearson
nir = nir, p_nir = binom.test(VP + VN, n, p = nir, alternative = "greater")$p.value,
kappa = (acc - pe) / (1 - pe),
sens = sens, sens_ic = wil(VP, VP + FN), esp = esp, esp_ic = wil(VN, VN + FP),
vpp = vpp, vpp_ic = wil(VP, VP + FP), vpn = vpn, vpn_ic = wil(VN, VN + FN),
f1 = 2 * vpp * sens / (vpp + sens), bal = (sens + esp) / 2, J = sens + esp - 1,
mcc = (VP * VN - FP * FN) / sqrt(as.numeric(VP + FP) * (VP + FN) * (VN + FP) * (VN + FN)),
fpr = 1 - esp, fnr = 1 - sens, lrp = sens / (1 - esp), lrn = (1 - sens) / esp,
prev = mean(y), detec = VP / n, senal = (VP + FP) / n,
p_mcnemar = mcnemar.test(matrix(c(VP, FN, FP, VN), 2))$p.value)
}
met_05 <- cm_metricas(prueba$y, p_te, 0.5)
met_yt <- cm_metricas(prueba$y, p_te, c_youden)
met_ytr <- cm_metricas(entren$y, p_tr, c_youden)
# Empleado hipotético
PERFIL <- data.frame(Edad = 32, Ingreso_Mensual = 2500, Antigüedad_Cargo = 1,
Horas_Extra = factor("No", levels = levels(datos$Horas_Extra)),
Estado_Civil = factor("Soltero", levels = levels(datos$Estado_Civil)),
Viaje_Negocios = factor("Raramente", levels = levels(datos$Viaje_Negocios)))
pr_eta <- predict(modelo, PERFIL, type = "link", se.fit = TRUE)
pi_hat <- plogis(pr_eta$fit)
pi_ic <- plogis(pr_eta$fit + c(-1, 1) * qnorm(0.975) * pr_eta$se.fit)
OR_he <- exp(coef(modelo)["Horas_ExtraSi"])Con la base rotacion (1.470 empleados, 24 variables) se estimó un modelo de regresión logística para la probabilidad de rotación. En la base rotaron 237 empleados (16,1 %). Se seleccionaron seis covariables con hipótesis previas: horas extra, estado civil, frecuencia de viajes de negocios, edad, ingreso mensual y antigüedad en el cargo. Las seis resultaron asociadas con la rotación en el análisis bivariado y conservaron su significancia en el modelo ajustado (pruebas de razón de verosimilitudes, \(\alpha=0{,}05\)). Todas las direcciones coinciden con las hipótesis.
El factor de mayor peso es trabajar horas extra: manteniendo constantes las demás covariables, multiplica los odds de rotación por 4,34. Le siguen el estado civil (los solteros rotan más que los casados), los viajes frecuentes y el ingreso; la edad y la antigüedad en el cargo tienen efectos protectores de menor magnitud. En una muestra de prueba independiente, el modelo alcanza un AUC de 0,740 (IC 95 % de DeLong: 0,671–0,810). El corte que maximiza el índice de Youden en entrenamiento es 0,25; en prueba da una sensibilidad (recall) de 50,7 %, una especificidad de 85,1 %, una precisión de 39,6 % y un \(F_1\) de 0,444. La exactitud (79,6 %) no supera la de clasificar a todos como “no rota” (83,9 %), algo esperable con una prevalencia del 16 %; por eso el desempeño se juzga con métricas que no dependen de la clase mayoritaria (exactitud balanceada 0,679, \(\kappa=0,322\), MCC \(=0,325\)). Para un empleado hipotético de 32 años, soltero, sin horas extra, que viaja raramente, con ingreso de 2.500 y un año en el cargo, la probabilidad estimada de rotar es 0,254. La estrategia de retención prioriza la redistribución de las horas extra, la política de viajes y la revisión salarial.
Una organización busca comprender y prever los factores asociados a la rotación de sus empleados, con el fin de tomar medidas proactivas de retención. La gerencia propone un modelo de regresión logística que estime la probabilidad de que un empleado rote y que identifique los factores de mayor incidencia.
Objetivo general. Estimar la probabilidad de rotación de un empleado en función de características laborales y sociodemográficas, e identificar los factores asociados de mayor peso.
Objetivos específicos.
Nota
Inconsistencias del enunciado y cómo se resuelven. (i) El punto 3 remite a “la hipótesis planteada en el punto 2”, pero las hipótesis se formulan en el punto 1; se contrasta contra las del punto 1. (ii) El punto 7 pide la estrategia con base en las variables significativas “en el punto 3”; se reportan las significativas del bivariado y del modelo ajustado, y la estrategia prioriza las que lo son en ambos. (iii) El problema habla de “cambio de cargo”, pero Rotación no distingue entre cambio interno y retiro; la interpretación se ciñe a lo que la variable registra. (iv) Algunos nombres de la base tienen erratas (Satisfación_Laboral); se usan tal cual.
Los datos provienen de la base rotacion del paquete paqueteMODELOS, adaptada de Weiers (2006). La respuesta se codifica como \(Y=1\) si el empleado rotó (Rotación = "Si") y \(Y=0\) si no rotó (Rotación = "No"). La Tabla 2.1 verifica la recodificación.
tabla(as.data.frame.matrix(table(`Rotación (original)` = crudo$Rotación,
`Y (recodificada)` = datos$y)) %>%
tibble::rownames_to_column("Rotación (original)") %>%
rename(`Y = 0` = `0`, `Y = 1` = `1`),
"Verificación de la recodificación de la variable respuesta.")| Rotación (original) | Y = 0 | Y = 1 |
|---|---|---|
| No | 1.233 | 0 |
| Si | 0 | 237 |
Decisión metodológica
La variable Viaje de Negocios se renombra Viaje_Negocios en la base de trabajo, porque los espacios en el nombre obligan a usar comillas invertidas en las fórmulas. No se modifica ningún valor.
DESCRIPCION <- c(
"Rotación" = "El empleado rotó (Si/No)",
"Edad" = "Edad en años",
"Viaje de Negocios" = "Frecuencia de viajes de negocios",
"Departamento" = "Departamento de la empresa",
"Distancia_Casa" = "Distancia desde la casa (km)",
"Educación" = "Nivel educativo: 1 = primaria, 2 = secundaria, 3 = técnico/tecnólogo, 4 = pregrado, 5 = posgrado",
"Campo_Educación" = "Área de formación",
"Satisfacción_Ambiental" = "Satisfacción ambiental: 1 = muy insatisfecho, 2 = insatisfecho, 3 = satisfecho, 4 = muy satisfecho",
"Genero" = "Género (F/M)",
"Cargo" = "Cargo actual",
"Satisfación_Laboral" = "Satisfacción laboral: 1 = muy insatisfecho, 2 = insatisfecho, 3 = satisfecho, 4 = muy satisfecho",
"Estado_Civil" = "Estado civil",
"Ingreso_Mensual" = "Ingreso mensual",
"Trabajos_Anteriores" = "Cantidad de trabajos antes de ingresar a la empresa",
"Horas_Extra" = "Trabaja horas extra (Si/No)",
"Porcentaje_aumento_salarial" = "Último aumento salarial (%)",
"Rendimiento_Laboral" = "Rendimiento laboral: 1 = bajo, 2 = medio, 3 = alto, 4 = muy alto",
"Años_Experiencia" = "Años de experiencia laboral total",
"Capacitaciones" = "Capacitaciones en el último año",
"Equilibrio_Trabajo_Vida" = "Equilibrio trabajo-vida: 1 = muy bajo, 2 = bajo, 3 = medio, 4 = alto",
"Antigüedad" = "Años en la empresa",
"Antigüedad_Cargo" = "Años en el cargo actual",
"Años_ultima_promoción" = "Años desde la última promoción",
"Años_acargo_con_mismo_jefe" = "Años con el mismo jefe")
stopifnot(setequal(names(DESCRIPCION), names(crudo)))
tipo_est <- function(v) {
if (v %in% NOMINALES) "Cualitativa nominal"
else if (v %in% ORDINALES) "Cualitativa ordinal"
else if (v %in% CONTINUAS) "Cuantitativa continua"
else "Cuantitativa discreta"
}
dominio <- function(v) {
x <- crudo[[v]]
if (v == "Viaje de Negocios") "No_Viaja, Raramente, Frecuentemente"
else if (is.character(x)) paste(sort(unique(x)), collapse = ", ")
else if (v %in% ORDINALES) paste(sort(unique(x)), collapse = ", ")
else paste0("[", num(min(x), 0), "; ", num(max(x), 0), "]")
}
tipologia <- data.frame(
Variable = names(crudo),
Descripción = unname(DESCRIPCION[names(crudo)]),
`Tipo en R` = sapply(crudo, function(x) class(x)[1]),
`Tipo estadístico` = sapply(names(crudo), tipo_est),
`Valores o rango` = sapply(names(crudo), dominio),
check.names = FALSE, row.names = NULL)
tabla(tipologia, "Diccionario y tipología estadística de las 24 variables.", font = 7.5) %>%
column_spec(2, width = "4.2cm") %>% column_spec(5, width = "4.6cm")| Variable | Descripción | Tipo en R | Tipo estadístico | Valores o rango |
|---|---|---|---|---|
| Rotación | El empleado rotó (Si/No) | character | Cualitativa nominal | No, Si |
| Edad | Edad en años | numeric | Cuantitativa continua | [18; 60] |
| Viaje de Negocios | Frecuencia de viajes de negocios | character | Cualitativa ordinal | No_Viaja, Raramente, Frecuentemente |
| Departamento | Departamento de la empresa | character | Cualitativa nominal | IyD, RH, Ventas |
| Distancia_Casa | Distancia desde la casa (km) | numeric | Cuantitativa discreta | [1; 29] |
| Educación | Nivel educativo: 1 = primaria, 2 = secundaria, 3 = técnico/tecnólogo, 4 = pregrado, 5 = posgrado | numeric | Cualitativa ordinal | 1, 2, 3, 4, 5 |
| Campo_Educación | Área de formación | character | Cualitativa nominal | Ciencias, Humanidades, Mercadeo, Otra, Salud, Tecnicos |
| Satisfacción_Ambiental | Satisfacción ambiental: 1 = muy insatisfecho, 2 = insatisfecho, 3 = satisfecho, 4 = muy satisfecho | numeric | Cualitativa ordinal | 1, 2, 3, 4 |
| Genero | Género (F/M) | character | Cualitativa nominal | F, M |
| Cargo | Cargo actual | character | Cualitativa nominal | Director_Investigación, Director_Manofactura, Ejecutivo_Ventas, Gerente, Investigador_Cientifico, Recursos_Humanos, Representante_Salud, Representante_Ventas, Tecnico_Laboratorio |
| Satisfación_Laboral | Satisfacción laboral: 1 = muy insatisfecho, 2 = insatisfecho, 3 = satisfecho, 4 = muy satisfecho | numeric | Cualitativa ordinal | 1, 2, 3, 4 |
| Estado_Civil | Estado civil | character | Cualitativa nominal | Casado, Divorciado, Soltero |
| Ingreso_Mensual | Ingreso mensual | numeric | Cuantitativa continua | [1.009; 19.999] |
| Trabajos_Anteriores | Cantidad de trabajos antes de ingresar a la empresa | numeric | Cuantitativa discreta | [0; 9] |
| Horas_Extra | Trabaja horas extra (Si/No) | character | Cualitativa nominal | No, Si |
| Porcentaje_aumento_salarial | Último aumento salarial (%) | numeric | Cuantitativa discreta | [11; 25] |
| Rendimiento_Laboral | Rendimiento laboral: 1 = bajo, 2 = medio, 3 = alto, 4 = muy alto | numeric | Cualitativa ordinal | 3, 4 |
| Años_Experiencia | Años de experiencia laboral total | numeric | Cuantitativa discreta | [0; 40] |
| Capacitaciones | Capacitaciones en el último año | numeric | Cuantitativa discreta | [0; 6] |
| Equilibrio_Trabajo_Vida | Equilibrio trabajo-vida: 1 = muy bajo, 2 = bajo, 3 = medio, 4 = alto | numeric | Cualitativa ordinal | 1, 2, 3, 4 |
| Antigüedad | Años en la empresa | numeric | Cuantitativa discreta | [0; 40] |
| Antigüedad_Cargo | Años en el cargo actual | numeric | Cuantitativa discreta | [0; 18] |
| Años_ultima_promoción | Años desde la última promoción | numeric | Cuantitativa discreta | [0; 15] |
| Años_acargo_con_mismo_jefe | Años con el mismo jefe | numeric | Cuantitativa discreta | [0; 17] |
La base tiene 7 variables nominales, 6 ordinales, 2 continuas y 9 discretas (Tabla 2.2). Las escalas codificadas con números (Educación, las satisfacciones, Rendimiento_Laboral, Equilibrio_Trabajo_Vida) se tratan como ordinales: sus valores indican orden, pero la distancia entre categorías no tiene significado métrico. Viaje de Negocios es ordinal por la frecuencia creciente de sus niveles.
escalas <- bind_rows(lapply(names(ETQ_ORD), function(v)
data.frame(Variable = c(v, rep("", length(ETQ_ORD[[v]]) - 1)),
Código = names(ETQ_ORD[[v]]), Categoría = unname(ETQ_ORD[[v]]),
`n observado` = as.integer(table(factor(crudo[[v]], levels = names(ETQ_ORD[[v]])))),
check.names = FALSE)))
tabla(escalas, "Codificación de las escalas ordinales según el diccionario de datos del curso.",
align = c("l", "c", "l", "r"))| Variable | Código | Categoría | n observado |
|---|---|---|---|
| Educación | 1 | Primaria | 170 |
| 2 | Secundaria | 282 | |
| 3 | Técnico/tecnólogo | 572 | |
| 4 | Pregrado | 398 | |
| 5 | Posgrado | 48 | |
| Satisfacción_Ambiental | 1 | Muy insatisfecho | 284 |
| 2 | Insatisfecho | 287 | |
| 3 | Satisfecho | 453 | |
| 4 | Muy satisfecho | 446 | |
| Satisfación_Laboral | 1 | Muy insatisfecho | 289 |
| 2 | Insatisfecho | 280 | |
| 3 | Satisfecho | 442 | |
| 4 | Muy satisfecho | 459 | |
| Rendimiento_Laboral | 1 | Bajo | 0 |
| 2 | Medio | 0 | |
| 3 | Alto | 1.244 | |
| 4 | Muy alto | 226 | |
| Equilibrio_Trabajo_Vida | 1 | Muy bajo | 80 |
| 2 | Bajo | 344 | |
| 3 | Medio | 893 | |
| 4 | Alto | 153 |
Nota sobre el diccionario de datos
Las etiquetas de las escalas ordinales, la unidad de Distancia_Casa (kilómetros) y la definición de Trabajos_Anteriores (trabajos previos al ingreso a la empresa) provienen del diccionario de datos que la profesora del curso publicó para esta actividad (Tabla 2.3). En ese diccionario las dos escalas de satisfacción asignan “Muy insatisfecho” tanto al código 1 como al 4; por el orden creciente de la escala (1 = muy insatisfecho, 2 = insatisfecho, 3 = satisfecho), el código 4 se interpreta como “Muy satisfecho”. La escala de Equilibrio_Trabajo_Vida termina en “Alto”, como la define el diccionario. Rendimiento_Laboral solo registra los códigos 3 (alto) y 4 (muy alto): ningún empleado tiene una calificación baja o media.
Esta etapa se ejecuta sobre las 24 variables antes de seleccionar covariables. Ningún análisis posterior usa la base sin haber pasado por ella.
n_dup <- sum(duplicated(crudo))
n_na <- sum(is.na(crudo))
n_completos <- sum(complete.cases(crudo))
ESCALAS_1_4 <- c("Satisfacción_Ambiental", "Satisfación_Laboral", "Equilibrio_Trabajo_Vida")
CHK_DOM <- list(
"Cuantitativas sin valores negativos" = apply(crudo[CUANTITATIVAS] >= 0, 1, all),
"Edad en el rango laboral [18; 65]" = crudo$Edad >= 18 & crudo$Edad <= 65,
"Educación entera entre 1 y 5" = crudo$Educación %in% 1:5,
"Satisfacciones y equilibrio enteros entre 1 y 4" =
apply(crudo[ESCALAS_1_4], 1, function(r) all(r %in% 1:4)),
"Rendimiento laboral entero entre 1 y 4" = crudo$Rendimiento_Laboral %in% 1:4,
"Porcentaje de aumento en [0; 100]" =
crudo$Porcentaje_aumento_salarial >= 0 & crudo$Porcentaje_aumento_salarial <= 100,
"Etiquetas cualitativas sin espacios sobrantes" =
apply(crudo[sapply(crudo, is.character)], 1, function(r) all(r == trimws(r))))
viol_dom <- sapply(CHK_DOM, function(r) !r)
REGLAS <- list(
"Antigüedad en el cargo $\\le$ antigüedad en la empresa" =
crudo$Antigüedad_Cargo <= crudo$Antigüedad,
"Años con el mismo jefe $\\le$ antigüedad en la empresa" =
crudo$Años_acargo_con_mismo_jefe <= crudo$Antigüedad,
"Años desde la última promoción $\\le$ antigüedad en la empresa" =
crudo$Años_ultima_promoción <= crudo$Antigüedad,
"Antigüedad en la empresa $\\le$ experiencia total" =
crudo$Antigüedad <= crudo$Años_Experiencia,
"Edad $-$ experiencia $\\ge 14$ años" =
crudo$Edad - crudo$Años_Experiencia >= 14)
viol <- sapply(REGLAS, function(r) !r)
n_viol_unicos <- sum(rowSums(viol) > 0)
edad_inicio_min <- min(crudo$Edad - crudo$Años_Experiencia)
# Varianza casi nula: razón de frecuencias > 95/5 y menos del 10 % de valores únicos
nzv <- bind_rows(lapply(names(crudo), function(v) {
f <- sort(table(crudo[[v]]), decreasing = TRUE)
data.frame(razon = if (length(f) > 1) f[1] / f[2] else NA,
unicos = 100 * length(f) / N,
fmin = if (v %in% c(NOMINALES, ORDINALES)) as.numeric(min(f)) else NA)
}))
n_nzv <- sum(nzv$razon > 95 / 5 & nzv$unicos < 10, na.rm = TRUE)
min_nivel <- min(nzv$fmin, na.rm = TRUE)
# Cada registro excluido se cuenta una sola vez, aunque falle varias reglas
excluir <- duplicated(crudo) | rowSums(viol_dom) > 0 | rowSums(viol) > 0
n_excl <- sum(excluir)
stopifnot(nrow(datos) == nrow(crudo) - n_excl)
calidad <- data.frame(
Bloque = c("Integridad", "", "Dominio", rep("", length(CHK_DOM) - 1),
"Consistencia", rep("", length(REGLAS) - 1), "Variabilidad", "", "Depuración"),
Verificación = c("Filas duplicadas exactas", "Celdas con dato faltante",
names(CHK_DOM), names(REGLAS),
"Variables con varianza casi nula",
"Casos en el nivel cualitativo menos frecuente",
"Registros excluidos / registros finales"),
Resultado = c(ent(n_dup), ent(n_na), ent(colSums(viol_dom)), ent(colSums(viol)),
ent(n_nzv), ent(min_nivel), paste0(ent(n_excl), " / ", ent(N))),
check.names = FALSE)
tabla(calidad, "Verificaciones de calidad de la base (violaciones o conteos).",
align = c("l", "l", "r"), font = 8.5)| Bloque | Verificación | Resultado |
|---|---|---|
| Integridad | Filas duplicadas exactas | 0 |
| Celdas con dato faltante | 0 | |
| Dominio | Cuantitativas sin valores negativos | 0 |
| Edad en el rango laboral [18; 65] | 0 | |
| Educación entera entre 1 y 5 | 0 | |
| Satisfacciones y equilibrio enteros entre 1 y 4 | 0 | |
| Rendimiento laboral entero entre 1 y 4 | 0 | |
| Porcentaje de aumento en [0; 100] | 0 | |
| Etiquetas cualitativas sin espacios sobrantes | 0 | |
| Consistencia | Antigüedad en el cargo \(\le\) antigüedad en la empresa | 0 |
| Años con el mismo jefe \(\le\) antigüedad en la empresa | 0 | |
| Años desde la última promoción \(\le\) antigüedad en la empresa | 0 | |
| Antigüedad en la empresa \(\le\) experiencia total | 0 | |
| Edad \(-\) experiencia \(\ge 14\) años | 0 | |
| Variabilidad | Variables con varianza casi nula | 0 |
| Casos en el nivel cualitativo menos frecuente | 27 | |
| Depuración | Registros excluidos / registros finales | 0 / 1.470 |
La Tabla 3.1 resume las verificaciones de calidad. La base no tiene filas duplicadas (0) ni datos faltantes: los 1.470 registros son casos completos, así que no aplica la prueba MCAR de Little ni la imputación. Todas las variables están dentro de su dominio y ningún registro viola las restricciones lógicas; la diferencia mínima entre edad y experiencia es de 18 años. Con el criterio usual (razón entre la frecuencia del valor más común y la del segundo mayor que \(95/5\), y menos del 10 % de valores únicos) hay 0 variables con varianza casi nula, y el nivel cualitativo menos frecuente tiene 27 casos, así que no hay niveles escasos que agrupar. Rendimiento_Laboral solo toma los valores 3 y 4 de su escala (alto y muy alto). La base depurada conserva los 1.470 registros, y todos los análisis siguientes la usan.
Se aplican dos criterios a las 11 variables cuantitativas: el de Tukey (1977), que marca los valores fuera de \([Q_1-1{,}5\,\mathrm{RIC};\;Q_3+1{,}5\,\mathrm{RIC}]\), y el \(z\) robusto, que marca los valores con \(|x-\tilde x|/(1{,}4826\,\mathrm{MAD})>3{,}5\), donde \(\tilde x\) es la mediana.
atip <- bind_rows(lapply(CUANTITATIVAS, function(v) {
x <- crudo[[v]]; q <- quantile(x, c(.25, .75)); r <- diff(q)
tk <- sum(x < q[1] - 1.5 * r | x > q[2] + 1.5 * r)
s <- 1.4826 * mad(x, constant = 1)
zr <- if (s > 0) sum(abs(x - median(x)) / s > 3.5) else NA
data.frame(Variable = v, `Tukey (n)` = tk, `Tukey (%)` = 100 * tk / length(x),
`z robusto (n)` = zr, `z robusto (%)` = 100 * zr / length(x),
check.names = FALSE)
}))
tabla(atip, "Valores atípicos en las variables cuantitativas según dos criterios.",
digits = c(0, 0, 1, 0, 1))| Variable | Tukey (n) | Tukey (%) | z robusto (n) | z robusto (%) |
|---|---|---|---|---|
| Edad | 0 | 0,0 | 0 | 0,0 |
| Ingreso_Mensual | 114 | 7,8 | 118 | 8,0 |
| Distancia_Casa | 0 | 0,0 | 0 | 0,0 |
| Trabajos_Anteriores | 52 | 3,5 | 101 | 6,9 |
| Porcentaje_aumento_salarial | 0 | 0,0 | 18 | 1,2 |
| Años_Experiencia | 63 | 4,3 | 46 | 3,1 |
| Capacitaciones | 238 | 16,2 | 0 | 0,0 |
| Antigüedad | 104 | 7,1 | 66 | 4,5 |
| Antigüedad_Cargo | 21 | 1,4 | 0 | 0,0 |
| Años_ultima_promoción | 107 | 7,3 | 183 | 12,4 |
| Años_acargo_con_mismo_jefe | 14 | 1,0 | 0 | 0,0 |
largo_q <- crudo %>% select(all_of(CUANTITATIVAS)) %>%
pivot_longer(everything(), names_to = "var", values_to = "x") %>%
mutate(var = factor(var, levels = CUANTITATIVAS))
largo_q <- largo_q %>% mutate(familia = factor(FAMILIA[as.character(var)],
levels = names(COL_FAMILIA)))
ggplot(largo_q, aes(x = 0, y = x, fill = familia)) +
geom_boxplot(width = 0.5, colour = COL_TXT, alpha = 0.85,
outlier.colour = COL_TXT, outlier.size = 0.8, outlier.alpha = 0.5) +
scale_fill_manual(values = COL_FAMILIA, drop = TRUE, name = NULL) +
facet_wrap(~ var, scales = "free_y", ncol = 4) +
labs(title = "Los atípicos están en la cola superior, salvo en capacitaciones",
subtitle = "El color identifica la familia temática de la variable", x = NULL, y = NULL) +
theme(axis.text.x = element_blank(), strip.text = element_text(size = 7.5),
legend.position = "bottom", legend.text = element_text(size = 7),
legend.key.size = unit(0.35, "cm"))Figura 3.1: Diagramas de caja de las 11 variables cuantitativas. Los puntos marcan los valores fuera de las cercas de Tukey.
colas <- sapply(CUANTITATIVAS, function(v) {
x <- crudo[[v]]; q <- quantile(x, c(.25, .75)); r <- diff(q)
c(bajo = sum(x < q[1] - 1.5 * r), alto = sum(x > q[2] + 1.5 * r))
})La Figura 3.1 muestra dónde están los atípicos de la Tabla 3.2. Salvo en Capacitaciones, todos están en la cola superior, como corresponde a variables acotadas en cero y asimétricas a la derecha. En Capacitaciones la mitad central de los datos está en 2 y 3 capacitaciones, así que las cercas de Tukey dejan fuera tanto el 0 (54 casos) como el 5 y el 6 (184 casos): son valores posibles, no errores. Edad, Distancia_Casa y Porcentaje_aumento_salarial no tienen atípicos según Tukey.
Decisión metodológica
No se elimina ninguna observación por un criterio univariado. Un valor extremo pero posible (un ingreso alto, muchos años en la empresa) es información, no un error; solo se excluirían los valores imposibles, y los pasos anteriores no detectaron ninguno. En la regresión logística un atípico en \(x\) importa solo si es influyente, lo que se evalúa en la Sección 7.5 con la distancia de Cook, el apalancamiento y los DFBETAS. Para Ingreso_Mensual, que es asimétrico, se evalúa la escala logarítmica en la Sección 6.4.
ancho <- function(x) {
if (all(x == round(x)) && diff(range(x)) <= 40) 1
else 2 * IQR(x) / length(x)^(1/3) # regla de Freedman-Diaconis
}
capas <- lapply(CUANTITATIVAS, function(v)
geom_histogram(data = filter(largo_q, var == v), aes(x),
binwidth = ancho(crudo[[v]]), boundary = if (ancho(crudo[[v]]) == 1) 0.5 else NULL,
fill = COL_FAMILIA[FAMILIA[v]], colour = "white", linewidth = 0.2,
alpha = 0.9))
ggplot() + capas +
geom_vline(data = largo_q %>% group_by(var) %>% summarise(m = median(x), .groups = "drop"),
aes(xintercept = m), colour = COL_TXT, linewidth = 0.5, linetype = "22") +
facet_wrap(~ var, scales = "free", ncol = 4) +
scale_x_continuous(n.breaks = 4, labels = function(x) num(x, 0)) +
labs(title = "Casi todas las cuantitativas son asimétricas a la derecha",
subtitle = "La línea punteada marca la mediana; el color, la familia temática",
x = NULL, y = "Frecuencia") +
theme(strip.text = element_text(size = 7.5))Figura 3.2: Distribución de las 11 variables cuantitativas. Las variables discretas con pocos valores se grafican con un ancho de clase de 1; las demás, con el ancho de Freedman-Diaconis.
La Figura 3.2 anticipa lo que cuantifica la Tabla 5.2 (Sección 5.2): la edad es casi simétrica y el resto tiene cola derecha. Tres rasgos de forma son relevantes para el análisis. Ingreso_Mensual tiene una masa principal por debajo de 10.000 y un grupo reducido de ingresos altos. Antigüedad_Cargo y Años_acargo_con_mismo_jefe son multimodales, con picos en 0, 2 y 7 años; Años_ultima_promoción se concentra en 0 y 1 (el 63,8 % de los empleados). Porcentaje_aumento_salarial empieza en 11 %: no hay aumentos menores.
CUALI_EDA <- c(NOMINALES, ORDINALES)
bar_eda <- bind_rows(lapply(CUALI_EDA, function(v) {
x <- as.character(crudo[[v]])
niv <- if (v == "Viaje de Negocios") c("No_Viaja", "Raramente", "Frecuentemente")
else if (v %in% ORDINALES) as.character(sort(unique(crudo[[v]])))
else names(sort(table(x))) # de menor a mayor: arriba el más frecuente
t <- table(factor(x, levels = niv))
col_v <- unname(COL_FAMILIA[FAMILIA[v]])
relleno <- if (v %in% ORDINALES) degradado(col_v, length(niv))
else if (v == "Rotación") ifelse(niv == "Si", COL_NAR, COL_GRIS)
else rep(col_v, length(niv))
data.frame(var = v, nivel = niv, n = as.integer(t), relleno = relleno,
clave = paste(v, etq_ord(v, niv), sep = "___"))
})) %>%
mutate(var = factor(var, levels = CUALI_EDA),
clave = factor(clave, levels = unique(clave)))
ggplot(bar_eda, aes(n, clave, fill = relleno)) +
geom_col(width = 0.7) +
geom_text(aes(label = pct(n / N, 0)), hjust = -0.1, size = 2.3, colour = COL_TXT) +
scale_fill_identity() +
scale_y_discrete(labels = function(k) sub("^.*___", "", k)) +
scale_x_continuous(expand = expansion(mult = c(0, 0.35))) +
facet_wrap(~ var, scales = "free", ncol = 3) +
labs(title = "Varias cualitativas tienen una categoría dominante",
subtitle = "El color identifica la familia temática; las ordinales van en degradado",
x = "Empleados", y = NULL) +
theme(strip.text = element_text(size = 8), axis.text.y = element_text(size = 7),
axis.text.x = element_text(size = 6), panel.grid.major.y = element_blank())Figura 3.3: Distribución de las 13 variables cualitativas. Las nominales se ordenan por frecuencia; las ordinales, en su orden natural con un degradado del matiz de su familia. En Rotación, el naranja marca a quienes rotaron.
La Figura 3.3 muestra la estructura de la plantilla: dos tercios de los empleados están en Investigación y Desarrollo, los cargos más comunes son los de ventas y los técnicos o investigadores, y la mayoría viaja raramente. Entre las ordinales, las dos escalas de satisfacción se reparten de forma parecida entre sus cuatro niveles; Equilibrio_Trabajo_Vida se concentra en el nivel 3 (medio) y Rendimiento_Laboral solo toma los valores 3 (alto) y 4 (muy alto). Las frecuencias exactas están en las Tablas 5.4 y 5.5.
Se calculan dos matrices de asociación: la correlación de Spearman entre las variables cuantitativas y ordinales, y la V de Cramér entre las nominales. Spearman usa solo el orden de los valores, así que es válida para las ordinales codificadas con números y no la afectan los atípicos ni la asimetría. Viaje de Negocios entra con los puntajes 1, 2, 3 de su orden.
NUM_EDA <- c(CUANTITATIVAS, ORDINALES)
M_num <- crudo %>% mutate(`Viaje de Negocios` = match(`Viaje de Negocios`,
c("No_Viaja", "Raramente", "Frecuentemente"))) %>%
select(all_of(NUM_EDA))
R_all <- cor(M_num, method = "spearman")
# Orden por conglomerado jerárquico de 1 - |r| para agrupar las variables afines
orden <- hclust(as.dist(1 - abs(R_all)), method = "average")$order
niv_h <- NUM_EDA[orden]
heat <- as.data.frame(as.table(R_all)) %>%
setNames(c("v1", "v2", "r")) %>%
mutate(v1 = factor(v1, levels = niv_h), v2 = factor(v2, levels = rev(niv_h)))
marco <- heat %>% filter(v1 %in% c("Edad", "Ingreso_Mensual", "Antigüedad_Cargo"),
v2 %in% c("Edad", "Ingreso_Mensual", "Antigüedad_Cargo"))
ggplot(heat, aes(v1, v2, fill = r)) +
geom_tile(colour = "white", linewidth = 0.4) +
geom_tile(data = marco, fill = NA, colour = COL_TXT, linewidth = 0.6) +
geom_text(aes(label = num(r, 2), colour = abs(r) > 0.5), size = 2.1) +
scale_colour_manual(values = c(`TRUE` = "white", `FALSE` = COL_TXT), guide = "none") +
scale_fill_gradient2(low = COL_NAR, mid = "white", high = COL_JAV, midpoint = 0,
limits = c(-1, 1)) +
coord_equal() +
labs(title = "Las variables de tiempo forman un bloque muy correlacionado",
x = NULL, y = NULL) +
theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 6.5),
axis.text.y = element_text(size = 6.5), panel.grid = element_blank())Figura 3.4: Correlación de Spearman entre las 11 variables cuantitativas y las 6 ordinales. Azul: correlación positiva; naranja: negativa. Los recuadros marcan las tres cuantitativas seleccionadas.
ut <- which(upper.tri(R_all), arr.ind = TRUE)
pares <- data.frame(a = NUM_EDA[ut[, 1]], b = NUM_EDA[ut[, 2]], r = R_all[ut]) %>%
arrange(desc(abs(r)))
n_fuertes <- sum(abs(pares$r) > 0.7)
r_max_ord <- max(abs(R_all[ORDINALES, ORDINALES][upper.tri(R_all[ORDINALES, ORDINALES])]))
R_o <- abs(R_all[ORDINALES, setdiff(NUM_EDA, ORDINALES)])
R_o["Rendimiento_Laboral", "Porcentaje_aumento_salarial"] <- NA
r_ord_resto <- max(R_o, na.rm = TRUE)La Figura 3.4 ordena las variables por afinidad y deja ver un bloque de variables de tiempo: Antigüedad, Antigüedad_Cargo, Años_acargo_con_mismo_jefe, Años_ultima_promoción y Años_Experiencia, junto con la edad y el ingreso. Hay 4 pares con \(|r_s|>0{,}7\); el mayor es Antigüedad–Antigüedad_Cargo (\(r_s=0,854\)). Este bloque es el que justifica elegir una sola variable de antigüedad para el modelo (Sección 4). Las ordinales son casi independientes entre sí (la mayor correlación entre ellas es \(|r_s|=0,030\)) y del resto, con una excepción: Rendimiento_Laboral y Porcentaje_aumento_salarial tienen \(r_s=0,629\), lo que es esperable si el aumento se asigna según la calificación de rendimiento. Sin ese par, ninguna ordinal supera \(|r_s|=0,205\) con otra variable.
cramer <- function(a, b) {
t <- table(a, b)
ch <- suppressWarnings(chisq.test(t, correct = FALSE))
sqrt(unname(ch$statistic) / (sum(t) * (min(dim(t)) - 1)))
}
V <- outer(NOMINALES, NOMINALES, Vectorize(function(i, j)
if (i == j) 1 else cramer(crudo[[i]], crudo[[j]])))
dimnames(V) <- list(NOMINALES, NOMINALES)
heat_v <- as.data.frame(as.table(V)) %>% setNames(c("v1", "v2", "V")) %>%
mutate(v1 = factor(v1, levels = NOMINALES), v2 = factor(v2, levels = rev(NOMINALES)))
ggplot(heat_v, aes(v1, v2, fill = V)) +
geom_tile(colour = "white", linewidth = 0.4) +
geom_text(aes(label = num(V, 2), colour = V > 0.5), size = 2.5) +
scale_colour_manual(values = c(`TRUE` = "white", `FALSE` = COL_TXT), guide = "none") +
scale_fill_gradient(low = "white", high = COL_TEAL, limits = c(0, 1)) +
coord_equal() +
labs(title = "Departamento y cargo son casi redundantes",
x = NULL, y = NULL) +
theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 7),
axis.text.y = element_text(size = 7), panel.grid = element_blank())Figura 3.5: V de Cramér entre las 7 variables nominales, incluida la respuesta.
En la Figura 3.5, Departamento y Cargo tienen \(V=0,939\): cada cargo pertenece casi siempre a un solo departamento, así que incluir las dos en un modelo sería redundante. Campo_Educación tiene una asociación moderada con el departamento (\(V=0,590\)) y más débil con el cargo (\(V=0,343\)). Con la respuesta, la asociación nominal más fuerte es la de Horas_Extra (\(V=0,246\)), seguida de Cargo (\(V=0,242\)) y Estado_Civil (\(V=0,177\)). Cargo no se eligió como covariable a pesar de su asociación con la rotación: sus 9 niveles exigen 8 parámetros, lo mismo que las seis covariables juntas, y duplica la información de Departamento. Es una lectura descriptiva, sin contraste formal; las pruebas de asociación con la rotación se hacen en la Sección 6 para las covariables seleccionadas.
Se seleccionan tres covariables categóricas y tres cuantitativas con un mecanismo plausible de relación con la rotación (Tabla 4.1). La referencia de cada factor es el nivel frente al cual se interpretan los odds ratio (OR).
HIP <- data.frame(
Variable = c("Horas_Extra", "Estado_Civil", "Viaje_Negocios",
"Edad", "Ingreso_Mensual", "Antigüedad_Cargo"),
Tipo = c("Nominal (2 niveles)", "Nominal (3 niveles)", "Ordinal (3 niveles)",
"Continua", "Continua", "Discreta"),
Referencia = c("No", "Casado", "No_Viaja", "—", "—", "—"),
Mecanismo = c(
"La sobrecarga produce desgaste y conflicto entre trabajo y vida personal.",
"Menos cargas familiares reducen el costo de cambiar de empleo.",
"Los viajes aumentan el desgaste y el tiempo fuera de casa.",
"Los trabajadores jóvenes tienen más movilidad y menos arraigo.",
"Un salario mayor eleva el costo de oportunidad de irse.",
"La permanencia refleja ajuste al puesto y capital específico."),
`Dirección esperada` = c("Sí > No", "Soltero > Casado",
"Creciente con la frecuencia", "Negativa", "Negativa",
"Negativa"),
check.names = FALSE)
HIP$Signo <- c(+1, +1, +1, -1, -1, -1) # signo esperado del efecto principal
tabla(HIP[, 1:5], "Covariables seleccionadas, mecanismo y dirección esperada.",
font = 8) %>%
column_spec(4, width = "5.4cm") %>% column_spec(5, width = "2.6cm")| Variable | Tipo | Referencia | Mecanismo | Dirección esperada |
|---|---|---|---|---|
| Horas_Extra | Nominal (2 niveles) | No | La sobrecarga produce desgaste y conflicto entre trabajo y vida personal. | Sí > No |
| Estado_Civil | Nominal (3 niveles) | Casado | Menos cargas familiares reducen el costo de cambiar de empleo. | Soltero > Casado |
| Viaje_Negocios | Ordinal (3 niveles) | No_Viaja | Los viajes aumentan el desgaste y el tiempo fuera de casa. | Creciente con la frecuencia |
| Edad | Continua | — | Los trabajadores jóvenes tienen más movilidad y menos arraigo. | Negativa |
| Ingreso_Mensual | Continua | — | Un salario mayor eleva el costo de oportunidad de irse. | Negativa |
| Antigüedad_Cargo | Discreta | — | La permanencia refleja ajuste al puesto y capital específico. | Negativa |
En Estado_Civil la hipótesis direccional se plantea para solteros frente a casados. Para los divorciados no se formula una dirección, porque dos mecanismos opuestos compiten: menos cargas que un casado, pero en muchos casos con responsabilidades económicas por separado.
Hipótesis y regla de decisión
Todas las pruebas son bilaterales con \(\alpha=0{,}05\).
Rotación y \(X_j\) son independientes; \(H_1\): no lo son.Viaje_Negocios (bivariado): \(H_0\): la proporción de rotación no tiene tendencia lineal en los puntajes \(1,\,2,\,3\) de la frecuencia; \(H_1\): sí la tiene.La dirección se lee en el signo de \(\hat\beta_j\) o en el OR. Veredicto: confirmada si \(p<\alpha\) y el signo observado coincide con el esperado; significativa en sentido contrario si \(p<\alpha\) y el signo es el opuesto; no confirmada si \(p\ge\alpha\).
Mapa de las pruebas de hipótesis. La Tabla 4.2 reúne todas las pruebas del informe: qué hipótesis nula contrasta cada una, con qué estadístico y distribución de referencia, por qué es la adecuada y qué supuestos exige. En cada sección, antes de la tabla de resultados, un recuadro detalla la prueba en el contexto de las variables, y cada tabla de resultados incluye la decisión con \(\alpha=0{,}05\).
PRUEBAS <- data.frame(
Prueba = c("Shapiro-Wilk y Anderson-Darling", "$\\chi^2$ de independencia de Pearson",
"Tendencia de Cochran-Armitage", "Residuos estandarizados ajustados",
"Mann-Whitney (suma de rangos)", "Razón de verosimilitudes (logística simple)",
"Box-Tidwell", "Wald", "Razón de verosimilitudes tipo II",
"Razón de verosimilitudes global", "Hosmer-Lemeshow",
"Razón de verosimilitudes: lineal vs. splines", "AUC = 0,5 (Mann-Whitney sobre $\\hat\\pi$)",
"Binomial exacta: exactitud vs. NIR", "McNemar"),
Sección = c("5.2", "6.1", "6.1", "6.1", "6.2", "6.3", "6.4", "7.1", "7.1", "7.1", "7.5",
"7.6", "8.4", "8.4", "8.4"),
`Hipótesis nula` = c(
"La variable sigue una distribución normal",
"Rotación y la covariable son independientes",
"Sin tendencia lineal de la proporción de rotación en los puntajes 1, 2, 3",
"La celda se ajusta a la independencia",
"$A=P(X_1>X_0)+\\tfrac12P(X_1=X_0)=\\tfrac12$",
"$\\beta_1=0$ en el modelo simple",
"$\\gamma=0$: el logit es lineal en la covariable",
"$\\beta_j=0$ (un coeficiente)",
"Todos los coeficientes del término son nulos",
"$\\beta_1=\\dots=\\beta_8=0$",
"El modelo está bien calibrado",
"Los términos no lineales son nulos",
"El modelo no discrimina mejor que el azar",
"Exactitud $\\le$ NIR",
"$P(FP)=P(FN)$"),
`Estadístico y distribución bajo $H_0$` = c(
"$W$ y $A^2$ (distribuciones propias)",
"$X^2=\\sum(O-E)^2/E\\sim\\chi^2_{(r-1)(c-1)}$",
"$Z^2\\sim\\chi^2_1$",
"$r_{ij}\\sim N(0,\\,1)$",
"$U$; $z\\sim N(0,\\,1)$ con corrección por empates",
"$G^2=D_0-D_1\\sim\\chi^2_{\\text{gl}}$",
"$G^2\\sim\\chi^2_1$",
"$z=\\hat\\beta_j/\\widehat{\\mathrm{EE}}\\sim N(0,\\,1)$",
"$G^2\\sim\\chi^2_{\\text{gl}}$",
"$G^2\\sim\\chi^2_8$",
"$\\hat C\\sim\\chi^2_{g-2}$",
"$G^2\\sim\\chi^2_6$",
"$U$; $z\\sim N(0,\\,1)$",
"Aciertos $\\sim\\text{Bin}(n,\\text{NIR})$",
"$(|FP-FN|-1)^2/(FP+FN)\\sim\\chi^2_1$"),
`Por qué esta prueba` = c(
"Describe la forma; Anderson-Darling pondra más las colas",
"Dos variables categóricas",
"Covariable ordinal: usa el orden y concentra la potencia en 1 gl",
"Localiza las celdas que generan la asociación",
"Covariable cuantitativa no normal, con atípicos y empates",
"Más fiable que Wald; da la dirección del efecto",
"Contrasta el supuesto de linealidad en el logit",
"Contrasta cada nivel por separado",
"Factores de varios niveles; no depende del orden de entrada",
"Significancia conjunta del modelo",
"Bondad de ajuste con covariables continuas",
"Compara modelos anidados",
"Discriminación en datos nuevos",
"Compara con la regla trivial de la clase mayoritaria",
"Detecta si el corte sesga los errores hacia un lado"),
Supuestos = c(
"Observaciones independientes",
"Independencia; condición de Cochran (si falla, Fisher)",
"Independencia; puntajes fijados a priori",
"Muestra grande",
"Dos muestras independientes",
"Observaciones independientes; MV regular",
"Covariable positiva",
"Normalidad asintótica del EMV",
"Modelo sin interacciones",
"Observaciones independientes",
"Grupos con frecuencias esperadas suficientes",
"Modelos anidados en los mismos datos",
"Muestra de prueba independiente",
"Muestra de prueba independiente",
"Datos pareados (real y predicho del mismo empleado)"),
check.names = FALSE)
PRUEBAS$`Por qué esta prueba`[1] <- "Describe la forma; Anderson-Darling pondera más las colas"
tabla(PRUEBAS, "Pruebas de hipótesis del informe: hipótesis nula, estadístico, justificación y supuestos.",
align = c("l", "c", "l", "l", "l", "l")) %>%
pack_rows("Análisis univariado", 1, 1) %>% pack_rows("Análisis bivariado", 2, 7) %>%
pack_rows("Modelo de regresión logística", 8, 12) %>% pack_rows("Evaluación predictiva", 13, 15)| Prueba | Sección | Hipótesis nula | Estadístico y distribución bajo \(H_0\) | Por qué esta prueba | Supuestos |
|---|---|---|---|---|---|
| Análisis univariado | |||||
| Shapiro-Wilk y Anderson-Darling | 5.2 | La variable sigue una distribución normal | \(W\) y \(A^2\) (distribuciones propias) | Describe la forma; Anderson-Darling pondera más las colas | Observaciones independientes |
| Análisis bivariado | |||||
| \(\chi^2\) de independencia de Pearson | 6.1 | Rotación y la covariable son independientes | \(X^2=\sum(O-E)^2/E\sim\chi^2_{(r-1)(c-1)}\) | Dos variables categóricas | Independencia; condición de Cochran (si falla, Fisher) |
| Tendencia de Cochran-Armitage | 6.1 | Sin tendencia lineal de la proporción de rotación en los puntajes 1, 2, 3 | \(Z^2\sim\chi^2_1\) | Covariable ordinal: usa el orden y concentra la potencia en 1 gl | Independencia; puntajes fijados a priori |
| Residuos estandarizados ajustados | 6.1 | La celda se ajusta a la independencia | \(r_{ij}\sim N(0,\,1)\) | Localiza las celdas que generan la asociación | Muestra grande |
| Mann-Whitney (suma de rangos) | 6.2 | \(A=P(X_1\gt X_0)+\tfrac12P(X_1=X_0)=\tfrac12\) | \(U\); \(z\sim N(0,\,1)\) con corrección por empates | Covariable cuantitativa no normal, con atípicos y empates | Dos muestras independientes |
| Razón de verosimilitudes (logística simple) | 6.3 | \(\beta_1=0\) en el modelo simple | \(G^2=D_0-D_1\sim\chi^2_{\text{gl}}\) | Más fiable que Wald; da la dirección del efecto | Observaciones independientes; MV regular |
| Box-Tidwell | 6.4 | \(\gamma=0\): el logit es lineal en la covariable | \(G^2\sim\chi^2_1\) | Contrasta el supuesto de linealidad en el logit | Covariable positiva |
| Modelo de regresión logística | |||||
| Wald | 7.1 | \(\beta_j=0\) (un coeficiente) | \(z=\hat\beta_j/\widehat{\mathrm{EE}}\sim N(0,\,1)\) | Contrasta cada nivel por separado | Normalidad asintótica del EMV |
| Razón de verosimilitudes tipo II | 7.1 | Todos los coeficientes del término son nulos | \(G^2\sim\chi^2_{\text{gl}}\) | Factores de varios niveles; no depende del orden de entrada | Modelo sin interacciones |
| Razón de verosimilitudes global | 7.1 | \(\beta_1=\dots=\beta_8=0\) | \(G^2\sim\chi^2_8\) | Significancia conjunta del modelo | Observaciones independientes |
| Hosmer-Lemeshow | 7.5 | El modelo está bien calibrado | \(\hat C\sim\chi^2_{g-2}\) | Bondad de ajuste con covariables continuas | Grupos con frecuencias esperadas suficientes |
| Razón de verosimilitudes: lineal vs. splines | 7.6 | Los términos no lineales son nulos | \(G^2\sim\chi^2_6\) | Compara modelos anidados | Modelos anidados en los mismos datos |
| Evaluación predictiva | |||||
| AUC = 0,5 (Mann-Whitney sobre \(\hat\pi\)) | 8.4 | El modelo no discrimina mejor que el azar | \(U\); \(z\sim N(0,\,1)\) | Discriminación en datos nuevos | Muestra de prueba independiente |
| Binomial exacta: exactitud vs. NIR | 8.4 | Exactitud \(\le\) NIR | Aciertos \(\sim\text{Bin}(n,\text{NIR})\) | Compara con la regla trivial de la clase mayoritaria | Muestra de prueba independiente |
| McNemar | 8.4 | \(P(FP)=P(FN)\) | \((|FP-FN|-1)^2/(FP+FN)\sim\chi^2_1\) | Detecta si el corte sesga los errores hacia un lado | Datos pareados (real y predicho del mismo empleado) |
Decisión metodológica
Satisfacción laboral no entra entre las seis covariables. Es categórica ordinal y podría entrar como factor, igual que Viaje_Negocios. Se excluye por su papel causal plausible: la satisfacción puede ser mediadora entre las condiciones de trabajo (horas extra, viajes) y la rotación, y ajustar por ella absorbería parte del efecto de esas variables. Además, es una medida autorreportada, a diferencia de las seleccionadas, que son registros administrativos. Se describe en el análisis univariado.
Colinealidad entre candidatas. Años_Experiencia y Antigüedad se descartan como covariables por su correlación con las seleccionadas (Tabla 4.3). La experiencia tiene \(r_s=0,710\) con el ingreso y \(r_s=0,657\) con la edad; la antigüedad en la empresa tiene \(r_s=0,854\) con la antigüedad en el cargo. Entre las tres cuantitativas seleccionadas la correlación máxima es moderada, y la colinealidad en el modelo se verifica con el GVIF (Sección 7.5).
vars_sp <- c("Edad", "Ingreso_Mensual", "Antigüedad_Cargo", "Antigüedad", "Años_Experiencia")
R_sp <- cor(crudo[vars_sp], method = "spearman")
tabla(data.frame(Variable = vars_sp, R_sp, check.names = FALSE),
"Correlaciones de Spearman entre las cuantitativas seleccionadas y las descartadas.",
digits = 3, font = 8.5)| Variable | Edad | Ingreso_Mensual | Antigüedad_Cargo | Antigüedad | Años_Experiencia |
|---|---|---|---|---|---|
| Edad | 1,000 | 0,472 | 0,198 | 0,252 | 0,657 |
| Ingreso_Mensual | 0,472 | 1,000 | 0,395 | 0,464 | 0,710 |
| Antigüedad_Cargo | 0,198 | 0,395 | 1,000 | 0,854 | 0,493 |
| Antigüedad | 0,252 | 0,464 | 0,854 | 1,000 | 0,594 |
| Años_Experiencia | 0,657 | 0,710 | 0,493 | 0,594 | 1,000 |
La correlación máxima entre las tres seleccionadas es \(|r_s|=0,472\).
Rotaciónw <- prop.test(N1, N, correct = FALSE)$conf.int # intervalo de Wilson
tabla(data.frame(Rotación = c("No (Y = 0)", "Sí (Y = 1)", "Total"),
n = c(N0, N1, N), `Proporción` = c(N0, N1, N) / N,
check.names = FALSE),
"Distribución de la variable respuesta.", digits = c(0, 0, 4))| Rotación | n | Proporción |
|---|---|---|
| No (Y = 0) | 1.233 | 0,8388 |
| Sí (Y = 1) | 237 | 0,1612 |
| Total | 1.470 | 1,0000 |
data.frame(g = factor(c("No rotó", "Rotó"), levels = c("Rotó", "No rotó")),
n = c(N0, N1)) %>%
ggplot(aes(n, g, fill = g)) + geom_col(width = 0.6) +
geom_text(aes(label = paste0(ent(n), " (", pct(n / N), ")")), hjust = -0.08,
size = 3.3, colour = COL_TXT) +
scale_fill_manual(values = c("Rotó" = COL_NAR, "No rotó" = COL_GRIS)) +
scale_x_continuous(expand = expansion(mult = c(0, 0.25))) +
labs(title = "Uno de cada seis empleados rotó", x = "Empleados", y = NULL) +
theme(panel.grid.major.y = element_blank())Figura 5.1: Distribución de la rotación.
Rotaron 237 de los 1.470 empleados, una proporción de \(\hat p=0,1612\) con IC 95 % de Wilson \([0,1433;\ 0,1809]\) (Tabla 5.1, Figura 5.1). El intervalo de Wilson se prefiere al de Wald porque mantiene la cobertura nominal con proporciones alejadas de 0,5 (Agresti, 2013, §1.4).
Nota
Consecuencia del desbalance. Con una prevalencia cercana al 16 %, un modelo que asignara “No” a todos acertaría en el 83,9 % de los casos. Por eso la exactitud no sirve como medida principal, y un corte de 0,5 tenderá a clasificar a casi todos como “No”, con baja sensibilidad. El corte se decide en la Sección 9 con criterios explícitos.
desc_cuanti <- function(v) {
x <- crudo[[v]]
data.frame(Variable = v, n = length(x), Media = mean(x), DE = sd(x),
Mediana = median(x), RIC = IQR(x), Mín = min(x), Máx = max(x),
Asimetría = skewness(x, type = 2), `Curtosis (exceso)` = kurtosis(x, type = 2),
check.names = FALSE)
}
tabla(bind_rows(lapply(CUANTITATIVAS, desc_cuanti)),
"Estadísticos descriptivos de las variables cuantitativas.", font = 8,
digits = c(0, 0, 2, 2, 1, 1, 0, 0, 3, 3))| Variable | n | Media | DE | Mediana | RIC | Mín | Máx | Asimetría | Curtosis (exceso) |
|---|---|---|---|---|---|---|---|---|---|
| Edad | 1.470 | 36,92 | 9,14 | 36 | 13 | 18 | 60 | 0,413 | -0,405 |
| Ingreso_Mensual | 1.470 | 6.502,93 | 4.707,96 | 4.919 | 5.468 | 1.009 | 19.999 | 1,370 | 1,005 |
| Distancia_Casa | 1.470 | 9,19 | 8,11 | 7 | 12 | 1 | 29 | 0,958 | -0,225 |
| Trabajos_Anteriores | 1.470 | 2,69 | 2,50 | 2 | 3 | 0 | 9 | 1,026 | 0,010 |
| Porcentaje_aumento_salarial | 1.470 | 15,21 | 3,66 | 14 | 6 | 11 | 25 | 0,821 | -0,301 |
| Años_Experiencia | 1.470 | 11,28 | 7,78 | 10 | 9 | 0 | 40 | 1,117 | 0,918 |
| Capacitaciones | 1.470 | 2,80 | 1,29 | 3 | 1 | 0 | 6 | 0,553 | 0,495 |
| Antigüedad | 1.470 | 7,01 | 6,13 | 5 | 6 | 0 | 40 | 1,765 | 3,936 |
| Antigüedad_Cargo | 1.470 | 4,23 | 3,62 | 3 | 5 | 0 | 18 | 0,917 | 0,477 |
| Años_ultima_promoción | 1.470 | 2,19 | 3,22 | 1 | 3 | 0 | 15 | 1,984 | 3,613 |
| Años_acargo_con_mismo_jefe | 1.470 | 4,12 | 3,57 | 3 | 5 | 0 | 17 | 0,833 | 0,171 |
La asimetría y la curtosis se calculan con los estimadores de tipo 2 (Joanes y Gill, 1998), que son los que reportan los programas estadísticos habituales; la curtosis se reporta en exceso, de modo que 0 corresponde a la normal. Todas las cuantitativas son asimétricas a la derecha salvo la edad, que es casi simétrica. El ingreso mensual es la más asimétrica de las seleccionadas: su media supera a su mediana en 1.584 unidades.
SEL_CUANTI <- c("Edad", "Ingreso_Mensual", "Antigüedad_Cargo")
largo <- crudo %>% select(all_of(SEL_CUANTI)) %>%
pivot_longer(everything(), names_to = "var", values_to = "x") %>%
mutate(var = factor(var, levels = SEL_CUANTI))
largo <- largo %>% mutate(col = unname(COL_FAMILIA[FAMILIA[as.character(var)]]))
g1 <- ggplot(largo, aes(x)) +
geom_histogram(aes(y = after_stat(density), fill = col), bins = 30, colour = "white") +
geom_density(colour = COL_TXT, linewidth = 0.6) +
scale_fill_identity() +
facet_wrap(~ var, scales = "free", nrow = 1) +
labs(title = "Ingreso y antigüedad en el cargo, asimétricos a la derecha; edad, casi simétrica",
x = NULL, y = "Densidad")
g2 <- ggplot(largo, aes(x, y = 0)) +
geom_boxplot(aes(fill = col), width = 0.5, colour = COL_TXT, alpha = 0.85,
outlier.colour = COL_TXT, outlier.size = 0.9, outlier.alpha = 0.5) +
scale_fill_identity() +
facet_wrap(~ var, scales = "free_x", nrow = 1) +
labs(x = NULL, y = NULL) +
theme(axis.text.y = element_blank(), strip.text = element_blank())
gridExtra::grid.arrange(g1, g2, heights = c(2.3, 1))Figura 5.2: Distribución de las tres covariables cuantitativas seleccionadas. Arriba, histograma con densidad; abajo, diagrama de caja (los puntos son atípicos según Tukey).
ggplot(largo, aes(sample = x)) +
stat_qq(aes(colour = col), size = 0.5, alpha = 0.6) + scale_colour_identity() +
stat_qq_line(colour = COL_TXT, linewidth = 0.5) +
facet_wrap(~ var, scales = "free", nrow = 1) +
labs(title = "Las tres se apartan de la normalidad en las colas",
x = "Cuantiles teóricos", y = "Cuantiles observados")Figura 5.3: Gráficos cuantil-cuantil normales de las tres covariables cuantitativas seleccionadas.
Pruebas de normalidad de Shapiro-Wilk y Anderson-Darling
\(H_0\): la covariable sigue una distribución normal; \(H_1\): no la sigue. Shapiro-Wilk usa \(W=\left(\sum a_i x_{(i)}\right)^2/\sum(x_i-\bar x)^2\), que compara los estadísticos de orden con sus valores esperados bajo normalidad; Anderson-Darling usa \(A^2=n\int(F_n-F)^2\,[F(1-F)]^{-1}dF\), que pondera más las discrepancias en las colas. Se rechaza \(H_0\) si \(p<0{,}05\).
Por qué estas pruebas. Shapiro-Wilk (Shapiro y Wilk, 1965) es la prueba general de mayor potencia, y Anderson-Darling (Anderson y Darling, 1954) complementa la lectura de las colas, donde están los atípicos de estas variables. Se usan solo para describir la forma: la regresión logística no supone normalidad de las covariables, y la conclusión orienta la elección de pruebas no paramétricas en el análisis bivariado.
norm_tab <- bind_rows(lapply(SEL_CUANTI, function(v) {
x <- crudo[[v]]; sw <- shapiro.test(x); ad <- ad.test(x)
data.frame(Variable = v,
`W (Shapiro-Wilk)` = sw$statistic, `p (SW)` = pval(sw$p.value),
`A (Anderson-Darling)` = ad$statistic, `p (AD)` = pval(ad$p.value),
Decisión = ifelse(sw$p.value < 0.05 & ad$p.value < 0.05,
"Se rechaza $H_0$ con ambas", "Ver valores p"),
check.names = FALSE)
}))
tabla(norm_tab, "Pruebas de normalidad de las tres covariables cuantitativas seleccionadas.",
digits = c(0, 4, 0, 3, 0, 0))| Variable | W (Shapiro-Wilk) | p (SW) | A (Anderson-Darling) | p (AD) | Decisión |
|---|---|---|---|---|---|
| Edad | 0,9774 | \(2,03\times10^{-14}\) | 9,975 | \(\lt 2\times10^{-16}\) | Se rechaza \(H_0\) con ambas |
| Ingreso_Mensual | 0,8279 | \(\lt 2\times10^{-16}\) | 85,391 | \(\lt 2\times10^{-16}\) | Se rechaza \(H_0\) con ambas |
| Antigüedad_Cargo | 0,8962 | \(\lt 2\times10^{-16}\) | 50,263 | \(\lt 2\times10^{-16}\) | Se rechaza \(H_0\) con ambas |
Las pruebas de Shapiro-Wilk y Anderson-Darling rechazan la normalidad en las tres variables (Tabla 5.3; Figuras 5.2 y 5.3). Se reportan solo como descripción de la forma: con \(n=1.470\) estas pruebas detectan desviaciones de magnitud trivial, y la normalidad de las covariables no es un supuesto de la regresión logística. La forma sí orienta dos decisiones: el uso de pruebas no paramétricas en el bivariado y la evaluación de la escala logarítmica del ingreso.
frec_de <- function(vars) bind_rows(lapply(vars, function(v) {
x <- crudo[[v]]
t <- if (v == "Viaje de Negocios")
table(factor(x, levels = c("No_Viaja", "Raramente", "Frecuentemente")))
else if (v %in% ORDINALES) table(factor(x, levels = sort(unique(x))))
else sort(table(x), decreasing = TRUE)
data.frame(Variable = c(v, rep("", length(t) - 1)), Nivel = etq_ord(v, names(t)),
n = as.integer(t), `%` = 100 * as.numeric(t) / length(x), check.names = FALSE)
}))
tabla(frec_de(setdiff(NOMINALES, "Rotación")),
"Frecuencias de las variables cualitativas nominales.",
digits = c(0, 0, 0, 1), font = 8)| Variable | Nivel | n | % |
|---|---|---|---|
| Departamento | IyD | 961 | 65,4 |
| Ventas | 446 | 30,3 | |
| RH | 63 | 4,3 | |
| Campo_Educación | Ciencias | 606 | 41,2 |
| Salud | 464 | 31,6 | |
| Mercadeo | 159 | 10,8 | |
| Tecnicos | 132 | 9,0 | |
| Otra | 82 | 5,6 | |
| Humanidades | 27 | 1,8 | |
| Genero | M | 882 | 60,0 |
| F | 588 | 40,0 | |
| Cargo | Ejecutivo_Ventas | 326 | 22,2 |
| Investigador_Cientifico | 292 | 19,9 | |
| Tecnico_Laboratorio | 259 | 17,6 | |
| Director_Manofactura | 145 | 9,9 | |
| Representante_Salud | 131 | 8,9 | |
| Gerente | 102 | 6,9 | |
| Representante_Ventas | 83 | 5,6 | |
| Director_Investigación | 80 | 5,4 | |
| Recursos_Humanos | 52 | 3,5 | |
| Estado_Civil | Casado | 673 | 45,8 |
| Soltero | 470 | 32,0 | |
| Divorciado | 327 | 22,2 | |
| Horas_Extra | No | 1.054 | 71,7 |
| Si | 416 | 28,3 |
tabla(frec_de(ORDINALES), "Frecuencias de las variables cualitativas ordinales.",
digits = c(0, 0, 0, 1), font = 8)| Variable | Nivel | n | % |
|---|---|---|---|
| Viaje de Negocios | No_Viaja | 150 | 10,2 |
| Raramente | 1.043 | 71,0 | |
| Frecuentemente | 277 | 18,8 | |
| Educación | 1 · Primaria | 170 | 11,6 |
| 2 · Secundaria | 282 | 19,2 | |
| 3 · Técnico/tecnólogo | 572 | 38,9 | |
| 4 · Pregrado | 398 | 27,1 | |
| 5 · Posgrado | 48 | 3,3 | |
| Satisfacción_Ambiental | 1 · Muy insatisfecho | 284 | 19,3 |
| 2 · Insatisfecho | 287 | 19,5 | |
| 3 · Satisfecho | 453 | 30,8 | |
| 4 · Muy satisfecho | 446 | 30,3 | |
| Satisfación_Laboral | 1 · Muy insatisfecho | 289 | 19,7 |
| 2 · Insatisfecho | 280 | 19,0 | |
| 3 · Satisfecho | 442 | 30,1 | |
| 4 · Muy satisfecho | 459 | 31,2 | |
| Rendimiento_Laboral | 3 · Alto | 1.244 | 84,6 |
| 4 · Muy alto | 226 | 15,4 | |
| Equilibrio_Trabajo_Vida | 1 · Muy bajo | 80 | 5,4 |
| 2 · Bajo | 344 | 23,4 | |
| 3 · Medio | 893 | 60,7 | |
| 4 · Alto | 153 | 10,4 |
ord_num <- setdiff(ORDINALES, "Viaje de Negocios")
moda <- function(x) { t <- table(x); names(t)[which.max(t)] }
tabla(data.frame(Variable = c(ord_num, "Viaje de Negocios"),
Mediana = c(sapply(ord_num, function(v) etq_ord(v, median(crudo[[v]]))),
levels(datos$Viaje_Negocios)[median(as.integer(datos$Viaje_Negocios))]),
Moda = c(sapply(ord_num, function(v) etq_ord(v, moda(crudo[[v]]))),
moda(crudo$`Viaje de Negocios`)),
row.names = NULL),
"Mediana y moda de las variables ordinales.")| Variable | Mediana | Moda |
|---|---|---|
| Educación | 3 · Técnico/tecnólogo | 3 · Técnico/tecnólogo |
| Satisfacción_Ambiental | 3 · Satisfecho | 3 · Satisfecho |
| Satisfación_Laboral | 3 · Satisfecho | 4 · Muy satisfecho |
| Rendimiento_Laboral | 3 · Alto | 3 · Alto |
| Equilibrio_Trabajo_Vida | 3 · Medio | 3 · Medio |
| Viaje de Negocios | Raramente | Raramente |
En las ordinales se reportan la mediana y la moda, que solo usan el orden de las categorías (Tabla 5.6). Satisfación_Laboral tiene mediana 3 · Satisfecho: al menos la mitad de los empleados está satisfecha o muy satisfecha, aunque el 38,7 % está insatisfecho o muy insatisfecho. La moda de la educación es el pregrado y la del equilibrio trabajo-vida, el nivel medio.
SEL_CUALI <- c("Horas_Extra", "Estado_Civil", "Viaje_Negocios")
bar <- bind_rows(lapply(SEL_CUALI, function(v) {
t <- table(datos[[v]])
data.frame(var = v, nivel = names(t), n = as.integer(t))
})) %>%
mutate(var = factor(var, levels = SEL_CUALI),
nivel = factor(nivel, levels = unique(c(levels(datos$Horas_Extra),
levels(datos$Estado_Civil),
levels(datos$Viaje_Negocios)))),
relleno = case_when(nivel == "No_Viaja" ~ degradado(COL_TERR, 3)[1],
nivel == "Raramente" ~ degradado(COL_TERR, 3)[2],
nivel == "Frecuentemente" ~ COL_TERR,
nivel %in% c("No", "Si") ~ COL_TERR,
TRUE ~ COL_VIO))
ggplot(bar, aes(nivel, n, fill = relleno)) + geom_col(width = 0.65) +
geom_text(aes(label = paste0(ent(n), "\n", pct(n / N))), vjust = -0.15, size = 2.7,
colour = COL_TXT, lineheight = 0.85) +
scale_fill_identity() +
scale_x_discrete(labels = ETQ_NIVEL) +
scale_y_continuous(expand = expansion(mult = c(0, 0.3))) +
facet_wrap(~ var, scales = "free_x", nrow = 1) +
labs(title = "La mayoría no trabaja horas extra, está casada y viaja raramente",
x = NULL, y = "Empleados")Figura 5.4: Distribución de las tres covariables categóricas seleccionadas. Los niveles de viaje de negocios se muestran en su orden natural, con un degradado del matiz de su familia.
La mayoría de los empleados no trabaja horas extra (71,7 %), está casada (45,8 %) y viaja raramente (71,0 %). El nivel menos frecuente entre las seleccionadas es No_Viaja, con 150 casos: suficientes para estimar su efecto, aunque es la referencia con menos información.
La respuesta es \(Y\) (\(1\) = rotó, \(0\) = no rotó). Para cada covariable se usa la prueba que corresponde a su tipo, se reporta un tamaño de efecto y se estima una regresión logística simple, cuyo signo responde a la pregunta del enunciado sobre la dirección del efecto.
prop_nivel <- function(v) datos %>% group_by(nivel = .data[[v]]) %>%
summarise(n = n(), Rotan = sum(y), .groups = "drop") %>%
mutate(var = v, prop = Rotan / n)
pn <- bind_rows(lapply(SEL_CUALI, prop_nivel))
tabla(pn %>% transmute(Variable = ifelse(duplicated(var), "", var), Nivel = as.character(nivel),
n, `Rotan (n)` = Rotan, `Proporción de rotación` = prop),
"Proporción de rotación por nivel de las covariables categóricas.",
digits = c(0, 0, 0, 0, 4))| Variable | Nivel | n | Rotan (n) | Proporción de rotación |
|---|---|---|---|---|
| Horas_Extra | No | 1.054 | 110 | 0,1044 |
| Si | 416 | 127 | 0,3053 | |
| Estado_Civil | Casado | 673 | 84 | 0,1248 |
| Divorciado | 327 | 33 | 0,1009 | |
| Soltero | 470 | 120 | 0,2553 | |
| Viaje_Negocios | No_Viaja | 150 | 12 | 0,0800 |
| Raramente | 1.043 | 156 | 0,1496 | |
| Frecuentemente | 277 | 69 | 0,2491 |
ggplot(pn %>% mutate(var = factor(var, levels = SEL_CUALI)),
aes(nivel, prop)) +
geom_col(width = 0.6, fill = COL_NAR) +
geom_hline(yintercept = N1 / N, linetype = "dashed", colour = "grey50") +
geom_text(aes(label = pct(prop)), vjust = -0.3, size = 2.9, colour = COL_TXT) +
scale_y_continuous(labels = function(x) pct(x, 0), expand = expansion(mult = c(0, 0.18))) +
scale_x_discrete(labels = ETQ_NIVEL) +
facet_wrap(~ var, scales = "free_x", nrow = 1) +
labs(title = "Rotan más: con horas extra, solteros y viajeros frecuentes",
x = NULL, y = "Proporción que rota")Figura 6.1: Proporción de rotación por nivel de las covariables categóricas. La línea punteada es la proporción global.
Prueba \(\chi^2\) de independencia de Pearson
\(H_0\): Rotación y la covariable \(X\) son independientes, \(\pi_{ij}=\pi_{i+}\pi_{+j}\) en todas las celdas; \(H_1\): al menos una celda cumple \(\pi_{ij}\neq\pi_{i+}\pi_{+j}\). El estadístico es \(X^2=\sum_{i,j}(O_{ij}-E_{ij})^2/E_{ij}\), con \(E_{ij}=n_{i+}n_{+j}/n\); bajo \(H_0\), \(X^2\overset{a}{\sim}\chi^2_{(r-1)(c-1)}\). Se rechaza \(H_0\) si \(p<0{,}05\).
Por qué esta prueba. Las dos variables son categóricas y cada empleado aporta una sola observación, así que las observaciones son independientes. La aproximación \(\chi^2\) requiere frecuencias esperadas suficientes; se verifica la condición de Cochran (ninguna \(E_{ij}<1\) y al menos el 80 % de las \(E_{ij}\ge5\)) y, si fallara, se usaría la prueba exacta de Fisher. No se aplica la corrección de Yates, que es conservadora. Como con \(n=1.470\) el valor \(p\) no mide la magnitud de la asociación, se reporta la \(V\) de Cramér, \(V=\sqrt{X^2/[n(\min(r,c)-1)]}\in[0,\,1]\).
prueba_chi <- function(v) {
t <- table(datos[[v]], datos$y)
ch <- suppressWarnings(chisq.test(t, correct = FALSE))
cochran <- all(ch$expected >= 1) && mean(ch$expected >= 5) >= 0.8
p <- if (cochran) ch$p.value else fisher.test(t)$p.value
data.frame(Variable = v, Prueba = if (cochran) "Chi-cuadrado" else "Fisher",
`Chi-cuadrado` = unname(ch$statistic), gl = unname(ch$parameter),
`Valor p` = p, `Mín. esperado` = min(ch$expected),
`V de Cramér` = sqrt(unname(ch$statistic) / (sum(t) * (min(dim(t)) - 1))),
Decisión = decide(p), check.names = FALSE)
}
chi_tab <- bind_rows(lapply(SEL_CUALI, prueba_chi))
tabla(chi_tab %>% mutate(`Valor p` = pval(`Valor p`)),
"Pruebas de independencia entre cada covariable categórica y la rotación.",
digits = c(0, 0, 3, 0, 0, 1, 3))| Variable | Prueba | Chi-cuadrado | gl | Valor p | Mín. esperado | V de Cramér | Decisión |
|---|---|---|---|---|---|---|---|
| Horas_Extra | Chi-cuadrado | 89,044 | 1 | \(\lt 2\times10^{-16}\) | 67,1 | 0,246 | Se rechaza \(H_0\) |
| Estado_Civil | Chi-cuadrado | 46,164 | 2 | \(9,46\times10^{-11}\) | 52,7 | 0,177 | Se rechaza \(H_0\) |
| Viaje_Negocios | Chi-cuadrado | 24,182 | 2 | \(5,61\times10^{-6}\) | 24,2 | 0,128 | Se rechaza \(H_0\) |
# Cochran-Armitage para la tendencia en Viaje_Negocios (puntajes 1, 2, 3)
tv <- table(datos$Viaje_Negocios, datos$y)
ca <- prop.trend.test(x = tv[, "1"], n = rowSums(tv), score = 1:3)Las tres covariables categóricas cumplen la condición de Cochran (todas las frecuencias esperadas son mayores que 5; la mínima es 24,2), así que se usa la prueba \(\chi^2\) de independencia sin corrección y no hace falta la de Fisher. Las tres rechazan la independencia (Tabla 6.2). Por la V de Cramér, la asociación más fuerte es la de las horas extra (\(V=0,246\)).
Prueba de tendencia de Cochran-Armitage (Armitage, 1955)
\(H_0\): la proporción de rotación no cambia linealmente con la frecuencia de viaje, \(\beta_1=0\) en \(\pi_k=\beta_0+\beta_1 s_k\) con puntajes \(s_k=1,\,2,\,3\); \(H_1\): \(\beta_1\neq0\). El estadístico es \[ Z^2=\frac{\left[\sum_k s_k\,(y_k-n_k\bar p)\right]^2}{\bar p(1-\bar p)\sum_k n_k(s_k-\bar s)^2}\ \overset{a}{\sim}\ \chi^2_1 , \] donde \(y_k\) es el número de rotaciones en el nivel \(k\), \(\bar p\) la proporción global y \(\bar s=\sum_k n_ks_k/n\).
Por qué esta prueba. Viaje_Negocios es ordinal. La \(\chi^2\) general ignora el orden y reparte la evidencia en 2 gl; esta prueba la concentra en la dirección monótona que plantea la hipótesis, y por eso es más potente cuando la asociación es monótona. Los puntajes se fijan a priori por el orden natural de los niveles.
Para Viaje_Negocios, que es ordinal, la prueba principal es la de tendencia de Cochran-Armitage con puntajes \(1,\,2,\,3\): \(\chi^2_1=23,712\), \(p=1,12\times10^{-6}\). La proporción de rotación crece con la frecuencia de los viajes, de 8,0 % entre quienes no viajan a 24,9 % entre quienes viajan con frecuencia. La prueba de tendencia usa un solo grado de libertad y es más potente que la \(\chi^2\) general cuando la asociación es monótona (Agresti, 2013, §3.4.6).
res_aj <- bind_rows(lapply(SEL_CUALI, function(v) {
ch <- suppressWarnings(chisq.test(table(datos[[v]], datos$y), correct = FALSE))
data.frame(Variable = v, Nivel = rownames(ch$stdres),
`Residuo (Y = 0)` = ch$stdres[, "0"], `Residuo (Y = 1)` = ch$stdres[, "1"],
check.names = FALSE)
})) %>% mutate(Variable = ifelse(duplicated(Variable), "", Variable))
tabla(res_aj, "Residuos estandarizados ajustados de las tablas de contingencia.",
digits = c(0, 0, 2, 2))| Variable | Nivel | Residuo (Y = 0) | Residuo (Y = 1) |
|---|---|---|---|
| Horas_Extra | No | 9,44 | -9,44 |
| Si | -9,44 | 9,44 | |
| Estado_Civil | Casado | 3,49 | -3,49 |
| Divorciado | 3,36 | -3,36 | |
| Soltero | -6,73 | 6,73 | |
| Viaje_Negocios | No_Viaja | 2,85 | -2,85 |
| Raramente | 1,90 | -1,90 | |
| Frecuentemente | -4,41 | 4,41 |
Los residuos estandarizados ajustados siguen aproximadamente una \(N(0,\,1)\) bajo independencia, así que \(|r|>1{,}96\) señala las celdas que generan la asociación (Agresti, 2013, §2.4.5). En la Tabla 6.3, las celdas con exceso de rotación son las horas extra, los solteros y los viajes frecuentes; los divorciados y los casados tienen rotación por debajo de la esperada, y quienes no viajan también.
Prueba de Mann-Whitney (Mann y Whitney, 1947)
\(H_0\): \(A=P(X_1>X_0)+\tfrac12P(X_1=X_0)=\tfrac12\), es decir, el valor de la covariable de un empleado que rota no tiende a ser mayor ni menor que el de uno que no rota; \(H_1\): \(A\neq\tfrac12\). El estadístico es \(U=\sum_{i\in G_1}R_i-n_1(n_1+1)/2\), con \(R_i\) los rangos en la muestra combinada; bajo \(H_0\), \(z=(U-n_1n_0/2)/\sigma_U\overset{a}{\sim}N(0,\,1)\), con \(\sigma_U\) corregida por empates. Se rechaza \(H_0\) si \(p<0{,}05\).
Por qué esta prueba. Las tres covariables no son normales (Tabla 5.3), tienen atípicos y, en Antigüedad_Cargo, muchos empates; la prueba \(t\) compararía medias, que son sensibles a las colas. Mann-Whitney solo usa el orden de los datos y exige dos muestras independientes, lo que se cumple porque cada empleado está en un solo grupo. Con \(n_1=237\) y \(n_0=1.233\) la aproximación normal es precisa.
mw_tab <- bind_rows(lapply(SEL_CUANTI, function(v) {
x1 <- datos[[v]][datos$y == 1]; x0 <- datos[[v]][datos$y == 0]
wt <- wilcox.test(x1, x0, alternative = "two.sided", exact = FALSE)
A <- unname(wt$statistic) / (length(x1) * length(x0))
data.frame(Variable = v, `Mediana (Y = 1)` = median(x1), `Mediana (Y = 0)` = median(x0),
U = unname(wt$statistic), `Valor p` = wt$p.value, `A estimado` = A,
`r biserial` = 2 * A - 1, Decisión = decide(wt$p.value), check.names = FALSE)
}))
tabla(mw_tab %>% mutate(`Valor p` = pval(`Valor p`)),
"Prueba de Mann-Whitney para las covariables cuantitativas según la rotación.",
digits = c(0, 1, 1, 0, 0, 3, 3))| Variable | Mediana (Y = 1) | Mediana (Y = 0) | U | Valor p | A estimado | r biserial | Decisión |
|---|---|---|---|---|---|---|---|
| Edad | 32 | 36 | 106.855 | \(5,28\times10^{-11}\) | 0,366 | -0,269 | Se rechaza \(H_0\) |
| Ingreso_Mensual | 3.202 | 5.204 | 100.620 | \(2,95\times10^{-14}\) | 0,344 | -0,311 | Se rechaza \(H_0\) |
| Antigüedad_Cargo | 2 | 3 | 105.214 | \(4,43\times10^{-12}\) | 0,360 | -0,280 | Se rechaza \(H_0\) |
Se usa la prueba de Mann-Whitney bilateral, por la asimetría y los atípicos de las tres variables. El estadístico \(U\) se calcula para el grupo \(Y=1\), de modo que \(\hat A=U/(n_1n_0)\) estima \(P(X_1>X_0)+\tfrac12P(X_1=X_0)\); la prueba contrasta la dominancia estocástica (\(A=\tfrac12\)), no la igualdad de medianas, porque las formas de las dos distribuciones pueden diferir. El tamaño de efecto es la correlación biserial de rangos \(r_{rb}=2\hat A-1\in[-1;1]\): un valor negativo indica valores menores entre quienes rotan. Las tres pruebas rechazan \(H_0\) con efectos negativos y de magnitud similar (Tabla 6.4): quienes rotan son más jóvenes, ganan menos y llevan menos tiempo en el cargo. La mediana del ingreso de quienes rotan es 3.202 frente a 5.204 de quienes no rotan.
datos %>% select(y, all_of(SEL_CUANTI)) %>%
pivot_longer(-y, names_to = "var", values_to = "x") %>%
mutate(var = factor(var, levels = SEL_CUANTI),
g = factor(ifelse(y == 1, "Rotó", "No rotó"), levels = c("No rotó", "Rotó"))) %>%
ggplot(aes(g, x, fill = g)) +
geom_boxplot(width = 0.55, colour = COL_TXT, outlier.size = 0.6, outlier.colour = "grey60") +
scale_fill_manual(values = c("No rotó" = COL_GRIS, "Rotó" = COL_NAR)) +
facet_wrap(~ var, scales = "free_y", nrow = 1) +
labs(title = "Quienes rotan son más jóvenes, ganan menos y llevan menos tiempo en el cargo",
x = NULL, y = NULL)Figura 6.2: Distribución de las covariables cuantitativas según la rotación.
Prueba de razón de verosimilitudes en cada regresión logística simple
\(H_0\): \(\beta_1=0\) (o \(\beta_1=\beta_2=0\) en los factores de tres niveles) en \(\operatorname{logit}\pi_i=\beta_0+\beta_1x_i\); \(H_1\): algún coeficiente es distinto de cero. El estadístico es la diferencia de devianzas entre el modelo nulo y el simple, \(G^2=D_0-D_1\overset{a}{\sim}\chi^2_{\text{gl}}\). Se prefiere a la de Wald porque es invariante a la parametrización y más fiable en muestras finitas (Hauck y Donner, 1977). Además de la significancia, el modelo simple da el signo del efecto, que el enunciado pide interpretar.
simple <- function(term, v) {
f <- reformulate(term, "y")
m1 <- glm(f, binomial, datos); m0 <- glm(y ~ 1, binomial, datos)
ic <- suppressMessages(confint(m1))
lr <- anova(m0, m1, test = "LRT")
cf <- coef(m1)[-1]
data.frame(Variable = v, Coeficiente = etiqueta(names(cf)), beta = cf,
OR = exp(cf), `IC 2,5 %` = exp(ic[-1, 1]), `IC 97,5 %` = exp(ic[-1, 2]),
`p (LR del término)` = c(lr$`Pr(>Chi)`[2], rep(NA, length(cf) - 1)),
check.names = FALSE, row.names = NULL)
}
TERMINOS <- c(Horas_Extra = "Horas_Extra", Estado_Civil = "Estado_Civil",
Viaje_Negocios = "Viaje_Negocios", Edad = "Edad",
Ingreso_Mensual = "log(Ingreso_Mensual)", Antigüedad_Cargo = "Antigüedad_Cargo")
sl <- bind_rows(Map(simple, TERMINOS, names(TERMINOS)))
tabla(sl %>% mutate(Variable = ifelse(duplicated(Variable), "", Variable),
`p (LR del término)` = ifelse(is.na(`p (LR del término)`), "",
pval(`p (LR del término)`))) %>%
rename(`$\\hat\\beta$` = beta),
"Regresiones logísticas simples: coeficiente, OR con IC 95 % por verosimilitud perfilada y prueba de razón de verosimilitudes.",
digits = c(0, 0, 4, 3, 3, 3, 0), font = 8.5)| Variable | Coeficiente | \(\hat\beta\) | OR | IC 2,5 % | IC 97,5 % | p (LR del término) |
|---|---|---|---|---|---|---|
| Horas_Extra | Horas extra: Sí vs. No | 1,3274 | 3,771 | 2,832 | 5,032 | \(\lt 2\times10^{-16}\) |
| Estado_Civil | Estado civil: Divorciado vs. Casado | -0,2395 | 0,787 | 0,508 | 1,194 | \(2,79\times10^{-10}\) |
| Estado civil: Soltero vs. Casado | 0,8772 | 2,404 | 1,769 | 3,281 | ||
| Viaje_Negocios | Viaje: Raramente vs. No viaja | 0,7044 | 2,023 | 1,139 | 3,931 | \(6,93\times10^{-6}\) |
| Viaje: Frecuentemente vs. No viaja | 1,3389 | 3,815 | 2,061 | 7,641 | ||
| Edad | Edad (por año) | -0,0523 | 0,949 | 0,933 | 0,965 | \(3,25\times10^{-10}\) |
| Ingreso_Mensual | ln(Ingreso mensual) | -0,9053 | 0,404 | 0,317 | 0,512 | \(4,44\times10^{-15}\) |
| Antigüedad_Cargo | Antigüedad en el cargo (por año) | -0,1463 | 0,864 | 0,823 | 0,905 | \(6,38\times10^{-11}\) |
Cada modelo simple es \(\operatorname{logit}\pi_i=\beta_0+\beta_1x_i\) (o con indicadoras para los factores), y la prueba de razón de verosimilitudes compara la devianza con la del modelo nulo. Los signos coinciden con los de las pruebas anteriores (Tabla 6.5):
La regresión logística supone que el logit es lineal en cada covariable cuantitativa. Se evalúa de dos formas: con el logit empírico por deciles, \(\ln\{\bar y_g/(1-\bar y_g)\}\), y con la prueba de Box-Tidwell, que añade el término \(x\ln x\) al modelo de las seis covariables (con \(x\) en la escala en que entra la variable) y lo contrasta con una prueba de razón de verosimilitudes (Hosmer et al., 2013, §4.2). En Antigüedad_Cargo, que tiene ceros, se usa \(x+1\).
le <- bind_rows(lapply(SEL_CUANTI, function(v) {
g <- cut(datos[[v]], unique(quantile(datos[[v]], 0:10 / 10)), include.lowest = TRUE)
datos %>% mutate(g = g) %>% group_by(g) %>%
summarise(x = mean(.data[[v]]), p = mean(y), .groups = "drop") %>%
mutate(var = v, logit = qlogis(p))
})) %>% mutate(var = factor(var, levels = SEL_CUANTI),
col = unname(COL_FAMILIA[FAMILIA[as.character(var)]]))
ggplot(le, aes(x, logit)) +
geom_smooth(method = "lm", se = FALSE, colour = COL_GRIS, linewidth = 0.8, formula = y ~ x) +
geom_line(aes(colour = col), linewidth = 0.5) + geom_point(aes(colour = col), size = 1.6) +
scale_colour_identity() +
facet_wrap(~ var, scales = "free_x", nrow = 1) +
labs(title = "El riesgo cae rápido en los valores bajos y se aplana después",
x = NULL, y = "Logit empírico")Figura 6.3: Logit empírico de la rotación por deciles de cada covariable cuantitativa. Cada punto es un grupo; la línea es un ajuste lineal de referencia.
Prueba de Box-Tidwell
\(H_0\): \(\gamma=0\) en \(\operatorname{logit}\pi=\mathbf x^\top\boldsymbol\beta+\gamma\,z\ln z\), es decir, el logit es lineal en \(z\); \(H_1\): \(\gamma\neq0\) (el logit es curvo en \(z\)). El estadístico es \(G^2=D_{\text{sin }z\ln z}-D_{\text{con }z\ln z}\overset{a}{\sim}\chi^2_1\).
Por qué esta prueba. La linealidad en el logit es el supuesto de forma de la regresión logística para las covariables cuantitativas. El término \(z\ln z\) es la derivada de \(z^\lambda\) respecto a \(\lambda\) en \(\lambda=1\), así que la prueba detecta si una potencia distinta de 1 ajusta mejor (Box y Tidwell, 1962). Requiere \(z>0\); por eso en la antigüedad en el cargo se usa \(x+1\).
# Comparación de la escala del ingreso en el modelo completo
m_ing_lin <- update(modelo, . ~ . - log(Ingreso_Mensual) + Ingreso_Mensual)
aic_lin <- AIC(m_ing_lin); aic_log <- AIC(modelo)
# Box-Tidwell: se añade z ln z al modelo base que contiene a z en esa escala
bt <- function(v, desplaza = 0, escala = identity, nombre = v, base = modelo) {
z <- escala(datos[[v]] + desplaza)
d2 <- datos %>% mutate(bt_ = z * log(z))
m2 <- update(base, . ~ . + bt_, data = d2)
G <- deviance(base) - deviance(m2)
data.frame(Variable = nombre,
`Modelo base` = if (identical(base, modelo)) "ln(ingreso)" else "ingreso lineal",
`Chi-cuadrado LR` = G, `Valor p` = pchisq(G, 1, lower.tail = FALSE),
Decisión = decide(pchisq(G, 1, lower.tail = FALSE)), check.names = FALSE)
}
bt_tab <- bind_rows(bt("Edad"),
bt("Ingreso_Mensual", nombre = "Ingreso_Mensual (escala original)", base = m_ing_lin),
bt("Ingreso_Mensual", escala = log, nombre = "ln(Ingreso_Mensual)"),
bt("Antigüedad_Cargo", desplaza = 1, nombre = "Antigüedad_Cargo + 1"))
tabla(bt_tab %>% mutate(`Valor p` = pval(`Valor p`)),
"Prueba de Box-Tidwell en el modelo de las seis covariables.",
digits = c(0, 0, 3, 0))| Variable | Modelo base | Chi-cuadrado LR | Valor p | Decisión |
|---|---|---|---|---|
| Edad | ln(ingreso) | 8,566 | 0,003 | Se rechaza \(H_0\) |
| Ingreso_Mensual (escala original) | ingreso lineal | 7,269 | 0,007 | Se rechaza \(H_0\) |
| ln(Ingreso_Mensual) | ln(ingreso) | 6,080 | 0,014 | Se rechaza \(H_0\) |
| Antigüedad_Cargo + 1 | ln(ingreso) | 8,050 | 0,005 | Se rechaza \(H_0\) |
Decisión metodológica
El ingreso entra en escala logarítmica. Con el ingreso en su escala original el modelo tiene AIC \(=1.100,4\); con \(\ln(\text{Ingreso})\), AIC \(=1.092,6\), con el mismo número de parámetros. La escala logarítmica también corresponde a la asimetría de la variable y permite una lectura en términos relativos (efecto de duplicar el ingreso).
La linealidad es una aproximación. Box-Tidwell rechaza la linealidad de la edad, de la antigüedad en el cargo y del ingreso en las dos escalas (Tabla 6.6); la escala logarítmica reduce la curvatura del ingreso pero no la elimina. El logit empírico muestra la forma de cada variable (Figura 6.3). En el ingreso y la antigüedad en el cargo la tendencia global es decreciente, con irregularidades locales; en la edad, el logit empírico baja hasta cerca de los 40 años y luego se estabiliza o sube, así que la relación no es monótona y el coeficiente lineal resume solo la tendencia promedio. Se conserva la forma lineal para que los coeficientes sean interpretables, como pide el enunciado, y en la Sección 7.6 se ajusta un modelo con splines para verificar que las conclusiones no dependen de esta decisión.
dir_obs <- c(
Horas_Extra = sign(sl$beta[sl$Coeficiente == "Horas extra: Sí vs. No"]),
Estado_Civil = sign(sl$beta[sl$Coeficiente == "Estado civil: Soltero vs. Casado"]),
Viaje_Negocios = sign(glm(y ~ as.integer(Viaje_Negocios), binomial, datos)$coef[2]),
Edad = sign(mw_tab$`r biserial`[1]),
Ingreso_Mensual = sign(mw_tab$`r biserial`[2]),
Antigüedad_Cargo = sign(mw_tab$`r biserial`[3]))
p_princ <- c(chi_tab$`Valor p`[1:2], ca$p.value, mw_tab$`Valor p`)
b_tend <- coef(glm(y ~ as.integer(Viaje_Negocios), binomial, datos))[2]
efecto <- c(paste0("V = ", num(chi_tab$`V de Cramér`[1:2], 3)),
paste0("OR por nivel = ", num(exp(b_tend), 2)),
paste0("r = ", num(mw_tab$`r biserial`, 3)))
veredicto <- function(p, s_obs, s_esp) ifelse(p >= 0.05, "No confirmada",
ifelse(s_obs == s_esp, "Confirmada", "Significativa en sentido contrario"))
contraste <- data.frame(
Variable = HIP$Variable,
Prueba = c("Chi-cuadrado", "Chi-cuadrado", "Cochran-Armitage", rep("Mann-Whitney", 3)),
`Dirección esperada` = HIP$`Dirección esperada`,
`Dirección observada` = c(
paste0("OR = ", num(sl$OR[sl$Coeficiente == "Horas extra: Sí vs. No"], 2)),
paste0("OR (Soltero) = ", num(sl$OR[sl$Coeficiente == "Estado civil: Soltero vs. Casado"], 2)),
ifelse(dir_obs[3] > 0, "Creciente", "Decreciente"),
ifelse(dir_obs[4:6] < 0, "Negativa", "Positiva")),
`Valor p` = pval(p_princ), `p (Holm)` = pval(p.adjust(p_princ, "holm")),
Efecto = efecto,
Veredicto = veredicto(p_princ, dir_obs, HIP$Signo), check.names = FALSE)
tabla(contraste, "Contraste de las hipótesis con los resultados del análisis bivariado.",
font = 7.5) %>% column_spec(2, width = "2cm") %>% column_spec(3, width = "2cm") %>%
column_spec(4, width = "2.2cm") %>% column_spec(8, width = "1.7cm")| Variable | Prueba | Dirección esperada | Dirección observada | Valor p | p (Holm) | Efecto | Veredicto |
|---|---|---|---|---|---|---|---|
| Horas_Extra | Chi-cuadrado | Sí > No | OR = 3,77 | \(\lt 2\times10^{-16}\) | \(\lt 2\times10^{-16}\) | V = 0,246 | Confirmada |
| Estado_Civil | Chi-cuadrado | Soltero > Casado | OR (Soltero) = 2,40 | \(9,46\times10^{-11}\) | \(1,89\times10^{-10}\) | V = 0,177 | Confirmada |
| Viaje_Negocios | Cochran-Armitage | Creciente con la frecuencia | Creciente | \(1,12\times10^{-6}\) | \(1,12\times10^{-6}\) | OR por nivel = 1,92 | Confirmada |
| Edad | Mann-Whitney | Negativa | Negativa | \(5,28\times10^{-11}\) | \(1,58\times10^{-10}\) | r = -0,269 | Confirmada |
| Ingreso_Mensual | Mann-Whitney | Negativa | Negativa | \(2,95\times10^{-14}\) | \(1,48\times10^{-13}\) | r = -0,311 | Confirmada |
| Antigüedad_Cargo | Mann-Whitney | Negativa | Negativa | \(4,43\times10^{-12}\) | \(1,77\times10^{-11}\) | r = -0,280 | Confirmada |
n_conf_biv <- sum(contraste$Veredicto == "Confirmada")
p_solt <- summary(glm(y ~ Estado_Civil, binomial, datos))$coefficients["Estado_CivilSoltero", 4]
r_solt <- suppressWarnings(chisq.test(table(datos$Estado_Civil, datos$y), correct = FALSE))$stdres["Soltero", "1"]Las 6 hipótesis se confirman en el análisis bivariado (Tabla 6.7). La decisión principal usa \(\alpha=0{,}05\) sin ajuste, porque cada hipótesis tiene un mecanismo y una dirección definidos a priori; ahora bien, la selección de las covariables se hizo después del análisis exploratorio (Sección 3.5), así que las seis hipótesis no son estrictamente preespecificadas y su confirmación no es independiente de esa selección; los valores \(p\) ajustados por Holm (1979), que controlan la tasa de error por familia de los seis contrastes, llevan a la misma decisión. En Estado_Civil la prueba \(\chi^2\) es ómnibus (2 gl); la hipótesis direccional se verifica con el contraste específico de solteros frente a casados, que en la regresión simple tiene \(p=2,54\times10^{-8}\) (Wald), y con el residuo ajustado de la celda Soltero–rotó, \(r=6,73\). Para los divorciados no se planteó dirección.
Sea \(Y_i\in\{0,\,1\}\) la rotación del empleado \(i\) y \(\pi_i=P(Y_i=1\mid\mathbf x_i)\). Las \(Y_i\) se suponen Bernoulli independientes con
\[
\operatorname{logit}\pi_i=\ln\frac{\pi_i}{1-\pi_i}=\mathbf x_i^\top\boldsymbol\beta ,
\qquad
\pi_i=\frac{1}{1+e^{-\mathbf x_i^\top\boldsymbol\beta}},
\]
donde \(\mathbf x_i\) contiene el intercepto, las indicadoras de Horas_Extra (Sí), Estado_Civil (Divorciado, Soltero) y Viaje_Negocios (Raramente, Frecuentemente), y las variables Edad, \(\ln(\texttt{Ingreso\_Mensual})\) y Antigüedad_Cargo. El estimador de máxima verosimilitud maximiza
\[
\ell(\boldsymbol\beta)=\sum_{i=1}^{n}\left[y_i\ln\pi_i+(1-y_i)\ln(1-\pi_i)\right],
\]
cuyo gradiente \(\nabla\ell=\mathbf X^\top(\mathbf y-\boldsymbol\pi)\) no tiene raíz en forma cerrada; se resuelve por mínimos cuadrados iterativamente reponderados (IRLS), equivalente al método de Newton-Raphson con el enlace canónico (Agresti, 2013, §4.6). Con enlace logit, \(e^{\beta_j}\) es el OR asociado a un aumento unitario de \(x_j\) con las demás covariables fijas.
Decisión metodológica
El modelo inferencial se estima con la base completa (\(n=1.470\)), porque es el que se interpreta y el que usa toda la información disponible. La partición en entrenamiento y prueba se reserva para evaluar el poder predictivo (Sección 8). La relación entre eventos y parámetros es \(\text{EPV}=237/8=29,6\), por encima del umbral de 10 de Peduzzi et al. (1996).
Pruebas sobre los coeficientes del modelo
Por qué estas pruebas. La de Wald contrasta cada nivel por separado, pero un factor de tres niveles necesita una prueba conjunta de sus dos coeficientes. Para ella se usa la razón de verosimilitudes, que además es más fiable que la de Wald cuando un coeficiente es grande (Hauck y Donner, 1977). Se usa el tipo II porque el modelo no tiene interacciones: cada término se contrasta después de los demás y el resultado no depende del orden en que entran. Los IC de los OR se obtienen por verosimilitud perfilada, coherentes con la prueba de razón de verosimilitudes. Se rechaza \(H_0\) si \(p<0{,}05\).
cf <- summary(modelo)$coefficients
icp <- suppressMessages(confint(modelo)) # IC por verosimilitud perfilada
coef_tab <- data.frame(
Término = etiqueta(rownames(cf)), beta = cf[, 1], EE = cf[, 2], z = cf[, 3],
`p (Wald)` = pval(cf[, 4]), OR = exp(cf[, 1]),
`IC 2,5 %` = exp(icp[rownames(cf), 1]), `IC 97,5 %` = exp(icp[rownames(cf), 2]),
check.names = FALSE, row.names = NULL)
tabla(coef_tab %>% rename(`$\\hat\\beta$` = beta),
"Coeficientes del modelo logístico: log-odds, error estándar, prueba de Wald, OR e IC 95 % por verosimilitud perfilada.",
digits = c(0, 4, 4, 3, 0, 3, 3, 3), font = 8.5)| Término | \(\hat\beta\) | EE | z | p (Wald) | OR | IC 2,5 % | IC 97,5 % |
|---|---|---|---|---|---|---|---|
| Intercepto | 3,0273 | 1,2014 | 2,520 | 0,012 | 20,641 | 1,990 | 222,147 |
| Horas extra: Sí vs. No | 1,4685 | 0,1593 | 9,216 | \(\lt 2\times10^{-16}\) | 4,343 | 3,183 | 5,948 |
| Estado civil: Divorciado vs. Casado | -0,2968 | 0,2305 | -1,288 | 0,198 | 0,743 | 0,468 | 1,158 |
| Estado civil: Soltero vs. Casado | 0,7920 | 0,1721 | 4,602 | \(4,18\times10^{-6}\) | 2,208 | 1,578 | 3,101 |
| Viaje: Raramente vs. No viaja | 0,6800 | 0,3317 | 2,050 | 0,040 | 1,974 | 1,069 | 3,962 |
| Viaje: Frecuentemente vs. No viaja | 1,3333 | 0,3542 | 3,764 | \(1,67\times10^{-4}\) | 3,793 | 1,955 | 7,914 |
| Edad (por año) | -0,0239 | 0,0100 | -2,384 | 0,017 | 0,976 | 0,957 | 0,995 |
| ln(Ingreso mensual) | -0,6036 | 0,1547 | -3,902 | \(9,55\times10^{-5}\) | 0,547 | 0,402 | 0,739 |
| Antigüedad en el cargo (por año) | -0,0884 | 0,0277 | -3,187 | 0,001 | 0,915 | 0,866 | 0,966 |
av <- Anova(modelo, type = "II", test.statistic = "LR")
av_tab <- data.frame(Término = c("Horas_Extra", "Estado_Civil", "Viaje_Negocios", "Edad",
"ln(Ingreso_Mensual)", "Antigüedad_Cargo"),
`Chi-cuadrado LR` = av$`LR Chisq`, gl = av$Df,
`Valor p` = av$`Pr(>Chisq)`, check.names = FALSE, row.names = NULL) %>%
mutate(`Chi-cuadrado - gl` = `Chi-cuadrado LR` - gl) %>%
arrange(desc(`Chi-cuadrado - gl`)) %>% mutate(Rango = row_number(), Decisión = decide(`Valor p`))
tabla(av_tab %>% mutate(`Valor p` = pval(`Valor p`)),
"Pruebas de razón de verosimilitudes tipo II por término, ordenadas por importancia.",
digits = c(0, 3, 0, 0, 3, 0))| Término | Chi-cuadrado LR | gl | Valor p | Chi-cuadrado - gl | Rango | Decisión |
|---|---|---|---|---|---|---|
| Horas_Extra | 86,470 | 1 | \(\lt 2\times10^{-16}\) | 85,470 | 1 | Se rechaza \(H_0\) |
| Estado_Civil | 32,573 | 2 | \(8,45\times10^{-8}\) | 30,573 | 2 | Se rechaza \(H_0\) |
| Viaje_Negocios | 20,132 | 2 | \(4,25\times10^{-5}\) | 18,132 | 3 | Se rechaza \(H_0\) |
| ln(Ingreso_Mensual) | 15,696 | 1 | \(7,44\times10^{-5}\) | 14,696 | 4 | Se rechaza \(H_0\) |
| Antigüedad_Cargo | 10,737 | 1 | 0,001 | 9,737 | 5 | Se rechaza \(H_0\) |
| Edad | 5,881 | 1 | 0,015 | 4,881 | 6 | Se rechaza \(H_0\) |
lr_glob <- modelo$null.deviance - modelo$deviance
gl_glob <- modelo$df.null - modelo$df.residual
p_glob <- pchisq(lr_glob, gl_glob, lower.tail = FALSE)
mcf <- 1 - as.numeric(logLik(modelo)) / as.numeric(logLik(update(modelo, . ~ 1)))Significancia global. La prueba de razón de verosimilitudes frente al modelo nulo da \(G^2=224,00\) con 8 grados de libertad (\(p<2\times10^{-16}\)): al menos una covariable está asociada con la rotación. El pseudo-\(R^2\) de McFadden es \(0,1725\); en la escala de la log-verosimilitud, valores entre 0,2 y 0,4 se consideran ya un ajuste muy bueno (McFadden, 1979), así que el valor obtenido es moderado.
Significancia por término. Las pruebas de Wald (Tabla 7.1) evalúan cada coeficiente por separado; para los factores de tres niveles se requiere la prueba conjunta de razón de verosimilitudes tipo II, que compara el modelo con y sin el término (Tabla 7.2). Los seis términos son significativos al 5 %. Dentro de Estado_Civil, solo el contraste Soltero vs. Casado es significativo (\(p=4,18\times10^{-6}\)); el de Divorciado vs. Casado no lo es (\(p=0,198\)).
Todas las interpretaciones se hacen con las demás covariables constantes y describen asociaciones, no efectos causales.
El enunciado pide identificar qué factores inciden “en mayor proporción”. Comparar coeficientes crudos no sirve, porque dependen de las unidades de cada variable y los factores de tres niveles tienen dos coeficientes. Se usan dos criterios complementarios. El primero es \(\chi^2_{LR}-\text{gl}\) por término (Harrell, 2015, §5.4), que descuenta el valor esperado del estadístico bajo \(H_0\) y así corrige la ventaja de los términos con más grados de libertad (Tabla 7.2). El segundo expresa las cuantitativas como el OR entre el tercer y el primer cuartil, para ponerlas en una escala comparable con los contrastes entre niveles de los factores.
or_ric <- bind_rows(lapply(SEL_CUANTI, function(v) {
q <- quantile(datos[[v]], c(.25, .75))
cb <- if (v == "Ingreso_Mensual") "log(Ingreso_Mensual)" else v
delta <- if (v == "Ingreso_Mensual") diff(log(q)) else diff(q)
data.frame(Variable = v, Q1 = q[1], Q3 = q[2],
`OR (Q3 vs. Q1)` = exp(b[cb] * delta),
`IC 2,5 %` = exp(icp[cb, 1] * delta), `IC 97,5 %` = exp(icp[cb, 2] * delta),
check.names = FALSE, row.names = NULL)
}))
tabla(or_ric, "OR entre el tercer y el primer cuartil de las covariables cuantitativas.",
digits = c(0, 0, 0, 3, 3, 3))| Variable | Q1 | Q3 | OR (Q3 vs. Q1) | IC 2,5 % | IC 97,5 % |
|---|---|---|---|---|---|
| Edad | 30 | 43 | 0,733 | 0,565 | 0,943 |
| Ingreso_Mensual | 2.911 | 8.379 | 0,528 | 0,382 | 0,726 |
| Antigüedad_Cargo | 2 | 7 | 0,643 | 0,487 | 0,840 |
Por \(\chi^2_{LR}-\text{gl}\) el orden es: Horas_Extra, Estado_Civil, Viaje_Negocios, ln(Ingreso_Mensual), Antigüedad_Cargo, Edad. Las horas extra son, con amplia diferencia, el factor de mayor peso. Entre las cuantitativas, pasar del primer al tercer cuartil del ingreso multiplica los odds por 0,528, y de la antigüedad en el cargo, por 0,643 (Tabla 7.3). El IC del OR por RIC se obtiene escalando el IC perfilado de \(\beta\), lo cual es exacto porque la transformación es monótona.
comp <- data.frame(Término = etiqueta(names(b)[-1]),
`OR simple` = sl$OR[match(etiqueta(names(b)[-1]), sl$Coeficiente)],
`OR ajustado` = OR[-1],
`Cambio (%)` = 100 * (OR[-1] / sl$OR[match(etiqueta(names(b)[-1]), sl$Coeficiente)] - 1),
`p (Wald, ajustado)` = pval(cf[-1, 4]),
check.names = FALSE, row.names = NULL)
tabla(comp, "OR de las regresiones simples frente a los OR del modelo ajustado.",
digits = c(0, 3, 3, 1, 0))| Término | OR simple | OR ajustado | Cambio (%) | p (Wald, ajustado) |
|---|---|---|---|---|
| Horas extra: Sí vs. No | 3,771 | 4,343 | 15,1 | \(\lt 2\times10^{-16}\) |
| Estado civil: Divorciado vs. Casado | 0,787 | 0,743 | -5,6 | 0,198 |
| Estado civil: Soltero vs. Casado | 2,404 | 2,208 | -8,2 | \(4,18\times10^{-6}\) |
| Viaje: Raramente vs. No viaja | 2,023 | 1,974 | -2,4 | 0,040 |
| Viaje: Frecuentemente vs. No viaja | 3,815 | 3,793 | -0,6 | \(1,67\times10^{-4}\) |
| Edad (por año) | 0,949 | 0,976 | 2,9 | 0,017 |
| ln(Ingreso mensual) | 0,404 | 0,547 | 35,2 | \(9,55\times10^{-5}\) |
| Antigüedad en el cargo (por año) | 0,864 | 0,915 | 6,0 | 0,001 |
Ningún coeficiente cambia de signo al ajustar (0 cambios; Tabla 7.4). En la escala de \(\beta\), los mayores cambios están en la edad, la antigüedad en el cargo y el ingreso, cuyos coeficientes se acercan a 0: es compatible con confusión parcial, porque están correlacionadas entre sí (\(r_s=0,472\) entre la edad y el ingreso). El cambio no se explica solo por confusión: el OR no es colapsable, así que el OR ajustado puede alejarse de 1 sin que haya confusión, como ocurre con las horas extra (Greenland, Robins y Pearl, 1999). La edad sigue siendo significativa en el modelo ajustado, aunque con el menor peso de los seis términos.
gv <- car::vif(modelo) # rms también exporta vif(); se usa la de car (GVIF)
tabla(data.frame(Término = rownames(gv), GVIF = gv[, 1], gl = gv[, 2],
`$\\mathrm{GVIF}^{1/(2\\,\\mathrm{gl})}$` = gv[, 3], check.names = FALSE, row.names = NULL) %>%
mutate(Término = sub("log\\(Ingreso_Mensual\\)", "ln(Ingreso_Mensual)", Término)),
"Factores de inflación de varianza generalizados.", digits = c(0, 3, 0, 3))| Término | GVIF | gl | \(\mathrm{GVIF}^{1/(2\,\mathrm{gl})}\) |
|---|---|---|---|
| Horas_Extra | 1,036 | 1 | 1,018 |
| Estado_Civil | 1,030 | 2 | 1,007 |
| Viaje_Negocios | 1,013 | 2 | 1,003 |
| Edad | 1,269 | 1 | 1,126 |
| ln(Ingreso_Mensual) | 1,403 | 1 | 1,185 |
| Antigüedad_Cargo | 1,154 | 1 | 1,074 |
Colinealidad. Para términos con varios grados de libertad se usa el GVIF de Fox y Monette (1992), y \(\text{GVIF}^{1/(2\,\text{gl})}\) se compara con \(\sqrt{5}\approx2{,}24\), el equivalente del umbral \(\text{VIF}=5\) para un solo coeficiente. El máximo es 1,185 (Tabla 7.5): no hay colinealidad preocupante.
celdas_min <- min(sapply(SEL_CUALI, function(v) min(table(datos[[v]], datos$y))))
max_ee <- max(cf[-1, 2])Separación. Hay separación completa o cuasicompleta cuando una combinación lineal de covariables predice perfectamente la respuesta; entonces el estimador de máxima verosimilitud no existe y los errores estándar crecen sin límite. El algoritmo convergió en 5 iteraciones, la celda menos poblada de los cruces entre factores y respuesta tiene 12 casos y el mayor error estándar de las pendientes es 0,354. No hay indicios de separación.
Prueba de Hosmer-Lemeshow
\(H_0\): el modelo está bien calibrado, \(E(Y\mid\mathbf x)=\pi(\mathbf x;\boldsymbol\beta)\); \(H_1\): no lo está. Las observaciones se ordenan por \(\hat\pi\) y se agrupan en \(g\) grupos de tamaño similar; el estadístico es \[ \hat C=\sum_{k=1}^{g}\frac{(O_k-E_k)^2}{E_k\,(1-E_k/n_k)}\ \overset{a}{\sim}\ \chi^2_{g-2}, \] donde \(O_k\) y \(E_k\) son las rotaciones observadas y esperadas en el grupo \(k\), y \(n_k\) su tamaño.
Por qué esta prueba. Con covariables continuas casi cada empleado tiene un patrón de covariables único, y entonces la devianza y la \(\chi^2\) de Pearson no tienen una distribución \(\chi^2\) de referencia; agrupar por \(\hat\pi\) resuelve ese problema. Como el resultado depende de \(g\), se reporta con tres valores.
hosmer <- function(y, p, g) {
cortes <- unique(quantile(p, probs = seq(0, 1, length.out = g + 1)))
grp <- cut(p, cortes, include.lowest = TRUE)
o1 <- tapply(y, grp, sum); e1 <- tapply(p, grp, sum); ng <- tapply(y, grp, length)
C <- sum((o1 - e1)^2 / (e1 * (1 - e1 / ng)))
gg <- nlevels(grp)
data.frame(Grupos = gg, `C de Hosmer-Lemeshow` = C, gl = gg - 2,
`Valor p` = pchisq(C, gg - 2, lower.tail = FALSE),
Decisión = decide(pchisq(C, gg - 2, lower.tail = FALSE)), check.names = FALSE)
}
hl_tab <- bind_rows(lapply(c(8, 10, 12), function(g) hosmer(datos$y, fitted(modelo), g)))
tabla(hl_tab %>% mutate(`Valor p` = pval(`Valor p`)),
"Prueba de Hosmer-Lemeshow con distinto número de grupos.", digits = c(0, 3, 0, 0))| Grupos | C de Hosmer-Lemeshow | gl | Valor p | Decisión |
|---|---|---|---|---|
| 8 | 21,776 | 6 | 0,001 | Se rechaza \(H_0\) |
| 10 | 20,951 | 8 | 0,007 | Se rechaza \(H_0\) |
| 12 | 24,480 | 10 | 0,006 | Se rechaza \(H_0\) |
Bondad de ajuste. El estadístico de Hosmer-Lemeshow agrupa las observaciones por cuantiles de \(\hat\pi\) y compara eventos observados y esperados; bajo \(H_0\) sigue aproximadamente una \(\chi^2_{g-2}\) (Hosmer et al., 2013, §5.2.2). Su resultado depende del número de grupos, por eso se reporta con \(g\in\{8,\,10,\,12\}\) (Tabla 7.6). Con \(g=10\), \(p=0,007\). La prueba rechaza el ajuste con los tres números de grupos.
cb10 <- unique(quantile(fitted(modelo), seq(0, 1, 0.1)))
g10 <- cut(fitted(modelo), cb10, include.lowest = TRUE)
hl_grp <- data.frame(Decil = seq_len(nlevels(g10)), n = as.integer(table(g10)),
`Observados` = as.numeric(tapply(datos$y, g10, sum)),
`Esperados` = as.numeric(tapply(fitted(modelo), g10, sum)),
check.names = FALSE) %>%
mutate(`Observados - esperados` = Observados - Esperados)Las rotaciones observadas y esperadas por decil (Anexo B) muestran de dónde viene la falta de ajuste: el modelo subestima la rotación en los deciles extremos y la sobreestima en los intermedios. Es el patrón que produce un logit que no es lineal en las covariables, y coincide con lo que mostró Box-Tidwell. La prueba tiene además mucha potencia con \(n=1.470\), así que detecta desvíos de magnitud moderada (Hosmer et al., 2013, §5.2.2). La magnitud práctica de la descalibración se examina con la curva de calibración de la Sección 8, y el modelo con splines (Sección 7.6) indica cuánto se corrige al relajar la linealidad.
cook <- cooks.distance(modelo); hat <- hatvalues(modelo); dfb <- dfbetas(modelo)
p_c <- length(b)
infl <- data.frame(
Medida = c("Distancia de Cook $> 4/n$", "Distancia de Cook $> 1$",
"Apalancamiento $> 2p/n$", "Algún $|\\mathrm{DFBETAS}| > 2/\\sqrt{n}$"),
Umbral = c(4 / N, 1, 2 * p_c / N, 2 / sqrt(N)),
Observaciones = c(sum(cook > 4 / N), sum(cook > 1), sum(hat > 2 * p_c / N),
sum(apply(abs(dfb) > 2 / sqrt(N), 1, any))))
dfb_tab <- data.frame(Término = etiqueta(colnames(dfb)),
`Máximo |DFBETAS|` = apply(abs(dfb), 2, max),
`Observaciones con |DFBETAS| > 2/raíz(n)` = colSums(abs(dfb) > 2 / sqrt(N)),
check.names = FALSE, row.names = NULL)
rot_cook <- mean(datos$y[cook > 4 / N] == 1)
pi_cook <- median(fitted(modelo)[cook > 4 / N & datos$y == 1])
alto_h <- hat > 2 * p_c / NInfluencia. Ninguna observación tiene distancia de Cook mayor que 1 (la máxima es 0,0158), el umbral usual de influencia grave. Con el umbral más exigente, \(4/n\), se marcan 115 observaciones; el 95,7 % son rotaciones de bajo riesgo estimado, cuyo residuo es grande por construcción en un evento poco frecuente. El máximo \(|\mathrm{DFBETAS}|\) es 0,268: ninguna observación desplaza un coeficiente ni un tercio de su error estándar, y se conservan todas. El detalle está en el Anexo C.
modelo_ns <- glm(y ~ Horas_Extra + Estado_Civil + Viaje_Negocios +
splines::ns(Edad, 3) + splines::ns(log(Ingreso_Mensual), 3) +
splines::ns(Antigüedad_Cargo, 3), binomial, datos)
lr_nl <- deviance(modelo) - deviance(modelo_ns)
gl_nl <- modelo$df.residual - modelo_ns$df.residual
p_nl <- pchisq(lr_nl, gl_nl, lower.tail = FALSE)
auc_lin <- as.numeric(auc(roc(datos$y, fitted(modelo), levels = c(0, 1), direction = "<", quiet = TRUE)))
auc_ns <- as.numeric(auc(roc(datos$y, fitted(modelo_ns), levels = c(0, 1), direction = "<", quiet = TRUE)))
hl_ns <- bind_rows(lapply(c(8, 10, 12), function(g) hosmer(datos$y, fitted(modelo_ns), g)))
base_pred <- data.frame(Horas_Extra = factor("No", levels(datos$Horas_Extra)),
Estado_Civil = factor("Casado", levels(datos$Estado_Civil)),
Viaje_Negocios = factor("Raramente", levels(datos$Viaje_Negocios)),
Edad = median(datos$Edad), Ingreso_Mensual = median(datos$Ingreso_Mensual),
Antigüedad_Cargo = median(datos$Antigüedad_Cargo))
curvas <- bind_rows(lapply(SEL_CUANTI, function(v) {
r <- quantile(datos[[v]], c(0.02, 0.98))
g <- base_pred[rep(1, 80), ]; g[[v]] <- seq(r[1], r[2], length.out = 80)
bind_rows(data.frame(var = v, x = g[[v]], eta = predict(modelo, g), Modelo = "Lineal"),
data.frame(var = v, x = g[[v]], eta = predict(modelo_ns, g), Modelo = "Splines"))
})) %>% mutate(var = factor(var, levels = SEL_CUANTI))
edad_min <- curvas %>% filter(var == "Edad", Modelo == "Splines") %>%
slice_min(eta, n = 1) %>% pull(x)Como Box-Tidwell rechazó la linealidad, se reajustó el modelo reemplazando cada cuantitativa por un spline cúbico natural con 3 grados de libertad (Anexo D). Los splines mejoran el ajuste (\(G^2=24,91\), 6 gl, \(p=3,55\times10^{-4}\)), pero la ganancia en discriminación es pequeña: el AUC aparente pasa de 0,778 a 0,785. El ingreso y la antigüedad en el cargo conservan un efecto decreciente; la edad tiene forma de U, con mínimo cerca de los 43 años. El modelo lineal se mantiene como principal por su interpretabilidad; su limitación es que promedia un efecto que no es uniforme a lo largo del rango.
La base se divide de forma estratificada por \(Y\) en entrenamiento (70 %) y prueba (30 %), con semilla 2026, para que las dos partes conserven la prevalencia de rotación. El modelo de la Sección 7 se reajusta solo con entrenamiento y se evalúa en prueba, que el modelo no vio.
tabla(data.frame(Conjunto = c("Entrenamiento", "Prueba", "Total"),
n = c(nrow(entren), nrow(prueba), N),
Rotaciones = c(sum(entren$y), sum(prueba$y), N1),
Prevalencia = c(mean(entren$y), mean(prueba$y), N1 / N)),
"Partición estratificada de la base.", digits = c(0, 0, 0, 4))| Conjunto | n | Rotaciones | Prevalencia |
|---|---|---|---|
| Entrenamiento | 1.029 | 166 | 0,1613 |
| Prueba | 441 | 71 | 0,1610 |
| Total | 1.470 | 237 | 0,1612 |
En entrenamiento hay 166 rotaciones, así que \(\text{EPV}=166/8=20,8\), también por encima de 10.
Para un corte \(c\), la sensibilidad es \(P(\hat\pi\ge c\mid Y=1)\) y la especificidad es \(P(\hat\pi<c\mid Y=0)\). La curva ROC grafica la sensibilidad frente a \(1-\text{especificidad}\) al recorrer todos los cortes. El área bajo ella tiene una lectura probabilística directa: \[ \text{AUC}=P\left(\hat\pi_{(1)}>\hat\pi_{(0)}\right)+\tfrac12\,P\left(\hat\pi_{(1)}=\hat\pi_{(0)}\right), \] donde \(\hat\pi_{(1)}\) y \(\hat\pi_{(0)}\) son las probabilidades estimadas de un rotante y de un no rotante elegidos al azar. Es el mismo \(\hat A\) de Mann-Whitney aplicado a \(\hat\pi\), y no depende del corte (Fawcett, 2006).
ETQ_TR <- paste0("Entrenamiento (AUC = ", num(auc(roc_tr), 3), ")")
ETQ_TE <- paste0("Prueba (AUC = ", num(auc(roc_te), 3), ")")
roc_df <- bind_rows(
data.frame(fpr = 1 - roc_tr$specificities, tpr = roc_tr$sensitivities, Conjunto = ETQ_TR),
data.frame(fpr = 1 - roc_te$specificities, tpr = roc_te$sensitivities, Conjunto = ETQ_TE)) %>%
arrange(Conjunto, fpr, tpr)
pt_y <- data.frame(fpr = 1 - m_youden_te$Esp, tpr = m_youden_te$Sens)
ggplot(roc_df, aes(fpr, tpr, colour = Conjunto)) +
geom_abline(linetype = "dashed", colour = "grey75") +
geom_step(linewidth = 0.8) +
geom_point(data = pt_y, aes(fpr, tpr), inherit.aes = FALSE, colour = COL_NAR, size = 2.6) +
scale_colour_manual(values = setNames(c(COL_TEAL, COL_AZUL), c(ETQ_TR, ETQ_TE)), name = NULL) +
coord_equal() +
theme(legend.position = "bottom", legend.text = element_text(size = 8),
legend.key.size = unit(0.4, "cm")) +
theme(plot.title.position = "plot") +
labs(title = "Discriminación aceptable en datos nuevos",
x = "1 - especificidad", y = "Sensibilidad")Figura 8.1: Curvas ROC del modelo en entrenamiento y en prueba. El punto marca el corte de Youden elegido en entrenamiento.
ic_tr <- ci.auc(roc_tr, method = "delong")
# Bootstrap de optimismo (Harrell, 2015, §5.3) sobre la base completa
fit_lrm <- lrm(FORMULA, data = datos, x = TRUE, y = TRUE)
set.seed(2026)
val <- validate(fit_lrm, method = "boot", B = 500)
auc_orig <- 0.5 + val["Dxy", "index.orig"] / 2
slope_corr <- val["Slope", "index.corrected"]
auc_corr <- 0.5 + val["Dxy", "index.corrected"] / 2
optim_auc <- val["Dxy", "optimism"] / 2
tabla(data.frame(
Estimación = c("AUC en entrenamiento", "AUC en prueba",
"AUC aparente (base completa)", "AUC corregido por optimismo (bootstrap, B = 500)"),
AUC = c(auc(roc_tr), auc(roc_te), auc_orig, auc_corr),
`IC 95 % (DeLong)` = c(paste0(num(ic_tr[1], 3), " – ", num(ic_tr[3], 3)),
paste0(num(ic_te[1], 3), " – ", num(ic_te[3], 3)), "—", "—"),
check.names = FALSE),
"Poder discriminante del modelo.", digits = c(0, 4, 0))| Estimación | AUC | IC 95 % (DeLong) |
|---|---|---|
| AUC en entrenamiento | 0,7918 | 0,751 – 0,833 |
| AUC en prueba | 0,7402 | 0,671 – 0,810 |
| AUC aparente (base completa) | 0,7776 | — |
| AUC corregido por optimismo (bootstrap, B = 500) | 0,7700 | — |
El AUC en prueba es 0,740, con IC 95 % de DeLong 0,671–0,810 (Tabla 8.2). El IC de DeLong et al. (1988) se basa en la teoría de los estadísticos \(U\) y no supone ninguna distribución para \(\hat\pi\). Según la escala de Hosmer et al. (2013, §5.2.4), la discriminación es aceptable (entre 0,7 y 0,8): si se elige al azar un empleado que rotó y uno que no, el modelo asigna mayor probabilidad al primero en el 74,0 % de los casos.
El AUC de prueba (0,740) es 0,052 menor que el de entrenamiento (0,792). Esa brecha mezcla el sobreajuste con la variabilidad de una sola partición: la muestra de prueba tiene solo 71 rotaciones, y el IC de su AUC (0,671–0,810) contiene al AUC de entrenamiento. Para separar los dos efectos se usa el bootstrap de optimismo, que no depende de una partición particular: se reajusta el modelo en 500 remuestras, se mide cuánto se degrada su AUC en la base original y se descuenta ese optimismo del AUC aparente. El optimismo estimado es 0,0077 y el AUC corregido, 0,770: con 8 parámetros y 237 eventos, el modelo casi no está sobreajustado, y la brecha observada en la partición se debe sobre todo al azar de la división. El AUC corregido es la mejor estimación del desempeño esperado en datos nuevos de la misma población.
La discriminación no garantiza que las probabilidades sean correctas en magnitud, y el corte por costos de la Sección 9 necesita que lo sean. La calibración se evalúa en prueba con el puntaje de Brier, la curva por deciles y la recta de calibración \(\operatorname{logit}P(Y=1)=a+b\,\operatorname{logit}\hat\pi\), cuyo ideal es \(a=0\), \(b=1\).
brier <- function(y, p) mean((p - y)^2)
br_te <- brier(prueba$y, p_te); br_ref <- mean(prueba$y) * (1 - mean(prueba$y))
cal <- glm(prueba$y ~ qlogis(p_te), family = binomial)
cal_int <- glm(prueba$y ~ 1, offset = qlogis(p_te), family = binomial) # a con b = 1
tabla(data.frame(
Medida = c("Brier del modelo (prueba)", "Brier de referencia: $\\bar y(1-\\bar y)$",
"Brier escalado: $1-B/B_{\\mathrm{ref}}$",
"Pendiente de calibración $b$ (prueba)",
"Pendiente corregida por optimismo (bootstrap, base completa)",
"Intercepto de calibración $a$ (con $b=1$)"),
Valor = c(br_te, br_ref, 1 - br_te / br_ref, coef(cal)[2], slope_corr, coef(cal_int)[1])),
"Medidas de calibración (muestra de prueba y bootstrap en la base completa).", digits = 4)| Medida | Valor |
|---|---|
| Brier del modelo (prueba) | 0,1142 |
| Brier de referencia: \(\bar y(1-\bar y)\) | 0,1351 |
| Brier escalado: \(1-B/B_{\mathrm{ref}}\) | 0,1542 |
| Pendiente de calibración \(b\) (prueba) | 0,7979 |
| Pendiente corregida por optimismo (bootstrap, base completa) | 0,9639 |
| Intercepto de calibración \(a\) (con \(b=1\)) | 0,0147 |
gcal <- cut(p_te, unique(quantile(p_te, 0:10 / 10)), include.lowest = TRUE)
cal_df <- data.frame(y = prueba$y, p = p_te, g = gcal) %>% group_by(g) %>%
summarise(pm = mean(p), obs = mean(y), n = n(), k = sum(y), .groups = "drop") %>%
rowwise() %>%
mutate(li = prop.test(k, n, correct = FALSE)$conf.int[1],
ls = prop.test(k, n, correct = FALSE)$conf.int[2]) %>% ungroup()
lim <- max(c(cal_df$ls, cal_df$pm)) * 1.05
ggplot(cal_df, aes(pm, obs)) +
geom_abline(linetype = "dashed", colour = "grey70") +
geom_errorbar(aes(ymin = li, ymax = ls), width = 0, colour = COL_GRIS, linewidth = 0.9) +
geom_line(colour = COL_TEAL, linewidth = 0.7) + geom_point(colour = COL_TEAL, size = 2) +
coord_equal(xlim = c(0, lim), ylim = c(0, lim)) +
labs(title = "Las probabilidades estimadas siguen a las observadas",
x = "Probabilidad estimada (media del decil)", y = "Proporción observada")Figura 8.2: Curva de calibración en la muestra de prueba: proporción observada de rotación frente a la probabilidad media estimada, por decil de probabilidad, con IC 95 % de Wilson.
El puntaje de Brier en prueba es 0,1142, frente a 0,1351 de un modelo que asignara a todos la prevalencia: una reducción de 15,4 % (Tabla 8.3). La pendiente de calibración es 0,798, menor que 1: en esta partición las predicciones son algo más extremas de lo que deberían, lo que concuerda con la brecha de AUC de la misma partición; el intercepto de calibración, 0,015, indica que el nivel medio de riesgo está bien estimado. La pendiente corregida por optimismo sobre la base completa es 0,964, más cercana a 1 que la de esta partición, lo que es compatible con que el valor de 0,798 refleje sobre todo el azar de la división. Las dos no son estrictamente comparables: la pendiente corregida corresponde al modelo de la base completa (\(n=1.470\)) y la de prueba, al ajustado solo con entrenamiento (\(n=1.029\)), que se espera algo más optimista. Los intervalos de 8 de los 10 deciles contienen la probabilidad estimada (Figura 8.2). La falta de ajuste que detectó Hosmer-Lemeshow en la base completa es, en la escala de la decisión, moderada: las probabilidades son utilizables para fijar cortes por costos.
El AUC, el puntaje de Brier y la calibración evalúan las probabilidades sin fijar un corte. Para clasificar a un empleado como “rota” o “no rota” hay que fijar uno, y entonces el desempeño se resume en la matriz de confusión: verdaderos positivos (VP, rotantes señalados), falsos positivos (FP, no rotantes señalados), verdaderos negativos (VN) y falsos negativos (FN, rotantes que el modelo deja escapar). La clase positiva es la rotación (\(Y=1\)). Se comparan dos cortes en la muestra de prueba: el convencional de 0,5 y el de Youden (\(c_J=0,25\)), elegido en entrenamiento; la justificación del corte está en la Sección 9.
cm_tab <- function(m, etiqueta) data.frame(
Corte = c(etiqueta, "", ""), Predicción = c("Rota (1)", "No rota (0)", "Total"),
`Real: rota (1)` = c(m$cm["VP"], m$cm["FN"], m$cm["VP"] + m$cm["FN"]),
`Real: no rota (0)` = c(m$cm["FP"], m$cm["VN"], m$cm["FP"] + m$cm["VN"]),
Total = c(m$cm["VP"] + m$cm["FP"], m$cm["FN"] + m$cm["VN"], sum(m$cm)),
check.names = FALSE, row.names = NULL)
tabla(bind_rows(cm_tab(met_05, "c = 0,50 (convencional)"),
cm_tab(met_yt, paste0("c = ", num(c_youden, 2), " (Youden)"))),
"Matrices de confusión en la muestra de prueba con los dos cortes.") %>%
row_spec(c(3, 6), italic = TRUE)| Corte | Predicción | Real: rota (1) | Real: no rota (0) | Total |
|---|---|---|---|---|
| c = 0,50 (convencional) | Rota (1) | 15 | 6 | 21 |
| No rota (0) | 56 | 364 | 420 | |
| Total | 71 | 370 | 441 | |
| c = 0,25 (Youden) | Rota (1) | 36 | 55 | 91 |
| No rota (0) | 35 | 315 | 350 | |
| Total | 71 | 370 | 441 |
cm_largo <- bind_rows(lapply(list(met_05, met_yt), function(m) {
et <- if (m$corte == 0.5) "c = 0,50 (convencional)" else paste0("c = ", num(m$corte, 2), " (Youden)")
data.frame(Corte = et,
Real = factor(c("Rota", "No rota", "Rota", "No rota"), levels = c("Rota", "No rota")),
Pred = factor(c("Rota", "Rota", "No rota", "No rota"), levels = c("No rota", "Rota")),
tipo = c("VP", "FP", "FN", "VN"),
n = unname(m$cm[c("VP", "FP", "FN", "VN")]))
})) %>% group_by(Corte, Real) %>% mutate(pc = n / sum(n)) %>% ungroup() %>%
mutate(acierto = tipo %in% c("VP", "VN"), Corte = factor(Corte, levels = unique(Corte)))
ggplot(cm_largo, aes(Real, Pred, fill = acierto, alpha = pc)) +
geom_tile(colour = "white", linewidth = 1) +
geom_text(aes(label = paste0(tipo, "\n", ent(n), " (", pct(pc, 0), ")")), alpha = 1,
size = 3.4, colour = COL_TXT, lineheight = 0.9) +
scale_fill_manual(values = c(`TRUE` = COL_AZUL, `FALSE` = COL_NAR)) +
scale_alpha(range = c(0.25, 0.85)) +
facet_wrap(~ Corte) +
labs(title = "El corte de Youden recupera más rotaciones a cambio de más falsas alarmas",
x = "Clase real", y = "Clase predicha") +
theme(panel.grid = element_blank())Figura 8.3: Matrices de confusión en la muestra de prueba. Cada celda muestra el conteo y el porcentaje de su columna (clase real): en la columna «Rota», la diagonal es la sensibilidad; en la columna «No rota», la especificidad.
Con el corte de 0,5 el modelo señala solo 21 de los 441 empleados de prueba y deja escapar 56 de las 71 rotaciones (Tabla 8.4, Figura 8.3). Con el corte de Youden detecta 36 rotaciones, 21 más, a cambio de 49 falsas alarmas adicionales.
Pruebas sobre la clasificación
Los intervalos de la exactitud son de Clopper y Pearson (1934), exactos, y los de la sensibilidad, la especificidad y los valores predictivos son de Wilson (1927), adecuados para proporciones cercanas a 0 o a 1. El kappa de Cohen (1960) corrige la exactitud por el acuerdo esperado por azar, \(p_e\), y el coeficiente de Matthews (1975) es la correlación de Pearson entre la clase real y la predicha.
f_ic <- function(campo) function(m) paste0(num(m[[campo]], 3), " [", num(m[[paste0(campo, "_ic")]][1], 3),
"; ", num(m[[paste0(campo, "_ic")]][2], 3), "]")
f_v <- function(campo, d = 3) function(m) num(m[[campo]], d)
FILAS <- list(
list("Exactitud (*accuracy*)", "$(VP+VN)/n$", f_ic("acc")),
list("Tasa de no información (NIR)", "$\\max(\\bar y,\\,1-\\bar y)$", f_v("nir")),
list("Valor p: exactitud > NIR", "Binomial exacta unilateral", function(m) pval(m$p_nir)),
list("Kappa de Cohen", "$(\\text{Exact.}-p_e)/(1-p_e)$", f_v("kappa")),
list("Sensibilidad (*recall*, TVP)", "$VP/(VP+FN)$", f_ic("sens")),
list("Especificidad (TVN)", "$VN/(VN+FP)$", f_ic("esp")),
list("Precisión (VPP)", "$VP/(VP+FP)$", f_ic("vpp")),
list("Valor predictivo negativo (VPN)", "$VN/(VN+FN)$", f_ic("vpn")),
list("$F_1$", "$2\\,\\text{VPP}\\cdot\\text{Sens}/(\\text{VPP}+\\text{Sens})$", f_v("f1")),
list("Exactitud balanceada", "$(\\text{Sens}+\\text{Esp})/2$", f_v("bal")),
list("Índice de Youden $J$", "$\\text{Sens}+\\text{Esp}-1$", f_v("J")),
list("Correlación de Matthews (MCC)",
"$\\dfrac{VP\\cdot VN-FP\\cdot FN}{\\sqrt{(VP+FP)(VP+FN)(VN+FP)(VN+FN)}}$", f_v("mcc")),
list("Tasa de falsos positivos", "$FP/(FP+VN)$", f_v("fpr")),
list("Tasa de falsos negativos", "$FN/(FN+VP)$", f_v("fnr")),
list("Razón de verosimilitud positiva $LR^+$", "$\\text{Sens}/(1-\\text{Esp})$", f_v("lrp", 2)),
list("Razón de verosimilitud negativa $LR^-$", "$(1-\\text{Sens})/\\text{Esp}$", f_v("lrn")),
list("Prevalencia", "$(VP+FN)/n$", f_v("prev")),
list("Tasa de detección", "$VP/n$", f_v("detec")),
list("Proporción señalada", "$(VP+FP)/n$", f_v("senal")),
list("Valor p de McNemar", "$H_0:\\ P(FP)=P(FN)$", function(m) pval(m$p_mcnemar)))
met_tab <- bind_rows(lapply(FILAS, function(f)
data.frame(Métrica = f[[1]], Definición = f[[2]],
a = f[[3]](met_05), b = f[[3]](met_yt), c = f[[3]](met_ytr))))
names(met_tab)[3:5] <- c("Prueba, c = 0,50", paste0("Prueba, c = ", num(c_youden, 2)),
paste0("Entrenamiento, c = ", num(c_youden, 2)))
met_tab$Métrica <- gsub("\\*", "", met_tab$Métrica)
tabla(met_tab, "Métricas de clasificación con IC 95 % (Clopper-Pearson para la exactitud; Wilson para las proporciones condicionales).",
align = c("l", "l", "r", "r", "r")) %>%
column_spec(3:5, extra_css = "white-space: nowrap;") %>%
pack_rows("Exactitud global", 1, 4) %>%
pack_rows("Desempeño por clase", 5, 8) %>%
pack_rows("Medidas resumen para clases desbalanceadas", 9, 12) %>%
pack_rows("Errores y razones de verosimilitud", 13, 16) %>%
pack_rows("Prevalencia y sesgo del clasificador", 17, 20)| Métrica | Definición | Prueba, c = 0,50 | Prueba, c = 0,25 | Entrenamiento, c = 0,25 |
|---|---|---|---|---|
| Exactitud global | ||||
| Exactitud (accuracy) | \((VP+VN)/n\) | 0,859 [0,823; 0,890] | 0,796 [0,755; 0,833] | 0,825 [0,800; 0,848] |
| Tasa de no información (NIR) | \(\max(\bar y,\,1-\bar y)\) | 0,839 | 0,839 | 0,839 |
| Valor p: exactitud > NIR | Binomial exacta unilateral | 0,135 | 0,993 | 0,890 |
| Kappa de Cohen | \((\text{Exact.}-p_e)/(1-p_e)\) | 0,273 | 0,322 | 0,426 |
| Desempeño por clase | ||||
| Sensibilidad (recall, TVP) | \(VP/(VP+FN)\) | 0,211 [0,132; 0,320] | 0,507 [0,393; 0,620] | 0,614 [0,539; 0,685] |
| Especificidad (TVN) | \(VN/(VN+FP)\) | 0,984 [0,965; 0,993] | 0,851 [0,811; 0,884] | 0,866 [0,841; 0,887] |
| Precisión (VPP) | \(VP/(VP+FP)\) | 0,714 [0,500; 0,862] | 0,396 [0,301; 0,498] | 0,468 [0,403; 0,534] |
| Valor predictivo negativo (VPN) | \(VN/(VN+FN)\) | 0,867 [0,831; 0,896] | 0,900 [0,864; 0,927] | 0,921 [0,900; 0,938] |
| Medidas resumen para clases desbalanceadas | ||||
| \(F_1\) | \(2\,\text{VPP}\cdot\text{Sens}/(\text{VPP}+\text{Sens})\) | 0,326 | 0,444 | 0,531 |
| Exactitud balanceada | \((\text{Sens}+\text{Esp})/2\) | 0,598 | 0,679 | 0,740 |
| Índice de Youden \(J\) | \(\text{Sens}+\text{Esp}-1\) | 0,195 | 0,358 | 0,480 |
| Correlación de Matthews (MCC) | \(\dfrac{VP\cdot VN-FP\cdot FN}{\sqrt{(VP+FP)(VP+FN)(VN+FP)(VN+FN)}}\) | 0,337 | 0,325 | 0,432 |
| Errores y razones de verosimilitud | ||||
| Tasa de falsos positivos | \(FP/(FP+VN)\) | 0,016 | 0,149 | 0,134 |
| Tasa de falsos negativos | \(FN/(FN+VP)\) | 0,789 | 0,493 | 0,386 |
| Razón de verosimilitud positiva \(LR^+\) | \(\text{Sens}/(1-\text{Esp})\) | 13,03 | 3,41 | 4,57 |
| Razón de verosimilitud negativa \(LR^-\) | \((1-\text{Sens})/\text{Esp}\) | 0,802 | 0,579 | 0,445 |
| Prevalencia y sesgo del clasificador | ||||
| Prevalencia | \((VP+FN)/n\) | 0,161 | 0,161 | 0,161 |
| Tasa de detección | \(VP/n\) | 0,034 | 0,082 | 0,099 |
| Proporción señalada | \((VP+FP)/n\) | 0,048 | 0,206 | 0,212 |
| Valor p de McNemar | \(H_0:\ P(FP)=P(FN)\) | \(4,88\times10^{-10}\) | 0,045 | \(1,44\times10^{-4}\) |
La exactitud no discrimina entre modelos con este desbalance. Con el corte de 0,5 la exactitud es 85,9 %, pero la tasa de no información es 83,9 %: la diferencia no es significativa (\(p=0,135\)). Con el corte de Youden la exactitud baja a 79,6 %, por debajo de la NIR, aunque el clasificador es más útil: detecta más del doble de rotaciones. La exactitud premia acertar en la clase mayoritaria, que es la que menos interesa a la empresa, y por eso no se usa para elegir el corte.
Métricas por clase. El corte de 0,5 tiene alta precisión (71,4 % de los señalados rotan) pero una sensibilidad de solo 21,1 %. El de Youden invierte el balance: sensibilidad de 50,7 % (IC 95 %: 39,3 %–62,0 %) y precisión de 39,6 %, que sigue siendo 2,5 veces la prevalencia. El VPN es alto con ambos cortes (86,7 % y 90,0 %): un empleado no señalado tiene baja probabilidad de rotar.
Medidas resumen. El \(F_1\) sube de 0,326 a 0,444, la exactitud balanceada de 0,598 a 0,679 y el kappa de 0,273 a 0,322. El MCC es casi igual con los dos cortes (0,337 y 0,325): resume la correlación entre la clase real y la predicha, y en este caso los dos cortes intercambian errores sin cambiar mucho esa correlación. La elección entre ellos depende, entonces, del costo relativo de cada error, que se analiza en la Sección 9.4.
Sesgo del clasificador. La prueba de McNemar rechaza la simetría de errores con el corte de 0,5 (\(p=4,88\times10^{-10}\)): hay 56 falsos negativos frente a 6 falsos positivos, así que ese corte subestima sistemáticamente la rotación. Con el corte de Youden el desequilibrio se invierte (55 FP frente a 35 FN, \(p=0,045\)): el modelo señala a más empleados de los que rotan, que es el sesgo buscado cuando perder a un empleado cuesta más que intervenirlo.
Entrenamiento frente a prueba. Con el mismo corte, las métricas de entrenamiento son algo mejores (sensibilidad 61,4 % frente a 50,7 %; \(F_1\) 0,531 frente a 0,444), en línea con la brecha de AUC de la Sección 8, que el bootstrap atribuye sobre todo al azar de la partición.
ll <- function(y, p) -mean(y * log(p) + (1 - y) * log(1 - p))
ks <- function(r) max(r$sensitivities + r$specificities - 1)
p_auc <- function(y, p) wilcox.test(p[y == 1], p[y == 0], alternative = "greater")$p.value
prob_tab <- data.frame(
Métrica = c("AUC [IC 95 % de DeLong]", "Coeficiente de Gini", "Estadístico KS",
"Puntaje de Brier", "Pérdida logarítmica (*log-loss*)", "Valor p: $H_0$ AUC = 0,5"),
Definición = c("$P(\\hat\\pi_{(1)}>\\hat\\pi_{(0)})$", "$2\\,\\text{AUC}-1$",
"$\\max_c\\,[\\text{Sens}(c)+\\text{Esp}(c)-1]$",
"$\\frac1n\\sum(\\hat\\pi_i-y_i)^2$",
"$-\\frac1n\\sum[y_i\\ln\\hat\\pi_i+(1-y_i)\\ln(1-\\hat\\pi_i)]$",
"Mann-Whitney unilateral sobre $\\hat\\pi$"),
Entrenamiento = c(paste0(num(auc(roc_tr), 3), " [", num(ic_tr[1], 3), "; ", num(ic_tr[3], 3), "]"),
num(2 * auc(roc_tr) - 1, 3), num(ks(roc_tr), 3), num(brier(entren$y, p_tr), 4),
num(ll(entren$y, p_tr), 4), pval(p_auc(entren$y, p_tr))),
Prueba = c(paste0(num(auc(roc_te), 3), " [", num(ic_te[1], 3), "; ", num(ic_te[3], 3), "]"),
num(2 * auc(roc_te) - 1, 3), num(ks(roc_te), 3), num(br_te, 4),
num(ll(prueba$y, p_te), 4), pval(p_auc(prueba$y, p_te))),
check.names = FALSE)
prob_tab$Métrica <- gsub("\\*", "", prob_tab$Métrica)
tabla(prob_tab, "Métricas que no dependen del corte, en entrenamiento y en prueba.",
align = c("l", "l", "r", "r"))| Métrica | Definición | Entrenamiento | Prueba |
|---|---|---|---|
| AUC [IC 95 % de DeLong] | \(P(\hat\pi_{(1)}\gt \hat\pi_{(0)})\) | 0,792 [0,751; 0,833] | 0,740 [0,671; 0,810] |
| Coeficiente de Gini | \(2\,\text{AUC}-1\) | 0,584 | 0,480 |
| Estadístico KS | \(\max_c\,[\text{Sens}(c)+\text{Esp}(c)-1]\) | 0,485 | 0,396 |
| Puntaje de Brier | \(\frac1n\sum(\hat\pi_i-y_i)^2\) | 0,1072 | 0,1142 |
| Pérdida logarítmica (log-loss) | \(-\frac1n\sum[y_i\ln\hat\pi_i+(1-y_i)\ln(1-\hat\pi_i)]\) | 0,3580 | 0,3854 |
| Valor p: \(H_0\) AUC = 0,5 | Mann-Whitney unilateral sobre \(\hat\pi\) | \(\lt 2\times10^{-16}\) | \(7,03\times10^{-11}\) |
Las métricas que no dependen del corte completan la evaluación (Tabla 8.6). El AUC rechaza con holgura la hipótesis de discriminación aleatoria (\(p=7,03\times10^{-11}\) en prueba). El coeficiente de Gini, \(2\,\text{AUC}-1=0,480\), es la misma información en escala \([0,\,1]\), y el estadístico de Kolmogorov-Smirnov (0,396) es la máxima separación entre las distribuciones acumuladas de \(\hat\pi\) en los dos grupos, que coincide con el máximo del índice de Youden sobre la curva ROC. El puntaje de Brier y la pérdida logarítmica miden el error de las probabilidades; ambas penalizan más las predicciones seguras y equivocadas, y la pérdida logarítmica es la devianza media del modelo.
tabla(data.frame(
Covariable = c("Edad", "Ingreso_Mensual", "Antigüedad_Cargo", "Horas_Extra", "Estado_Civil",
"Viaje_Negocios", "Probabilidad estimada de rotar", "IC 95 %"),
Valor = c(ent(PERFIL$Edad), ent(PERFIL$Ingreso_Mensual), ent(PERFIL$Antigüedad_Cargo),
as.character(PERFIL$Horas_Extra), as.character(PERFIL$Estado_Civil),
as.character(PERFIL$Viaje_Negocios), num(pi_hat, 4),
paste0(num(pi_ic[1], 4), " – ", num(pi_ic[2], 4))),
`Posición en los datos` = c(
paste0("Percentil ", num(100 * mean(datos$Edad <= PERFIL$Edad), 0)),
paste0("Percentil ", num(100 * mean(datos$Ingreso_Mensual <= PERFIL$Ingreso_Mensual), 0)),
paste0("Percentil ", num(100 * mean(datos$Antigüedad_Cargo <= PERFIL$Antigüedad_Cargo), 0)),
paste0(pct(mean(datos$Horas_Extra == "No")), " de la base"),
paste0(pct(mean(datos$Estado_Civil == "Soltero")), " de la base"),
paste0(pct(mean(datos$Viaje_Negocios == "Raramente")), " de la base"), "", ""),
check.names = FALSE),
"Perfil del empleado hipotético y probabilidad estimada de rotación.")| Covariable | Valor | Posición en los datos |
|---|---|---|
| Edad | 32 | Percentil 35 |
| Ingreso_Mensual | 2.500 | Percentil 15 |
| Antigüedad_Cargo | 1 | Percentil 20 |
| Horas_Extra | No | 71,7 % de la base |
| Estado_Civil | Soltero | 32,0 % de la base |
| Viaje_Negocios | Raramente | 71,0 % de la base |
| Probabilidad estimada de rotar | 0,2541 | |
| IC 95 % | 0,2003 – 0,3165 |
El perfil corresponde a un empleado joven que empieza en su cargo, con un ingreso en la parte baja de la distribución, soltero, sin horas extra y que viaja raramente (Tabla 9.1). Todos los valores están dentro del rango observado, así que la predicción es una interpolación. Se calcula con el modelo de la base completa. El IC se construye en la escala del predictor lineal, \(\hat\eta\pm z_{0,975}\,\widehat{\mathrm{EE}}(\hat\eta)\) con \(\widehat{\mathrm{EE}}(\hat\eta)=\sqrt{\mathbf x_0^\top\widehat{\mathrm{Var}}(\hat{\boldsymbol\beta})\mathbf x_0}\), y se transforma con \(\pi=1/(1+e^{-\eta})\); así el intervalo queda dentro de \([0,\,1]\), lo que no garantiza el método delta en la escala de \(\pi\). Resulta \(\hat\pi_0=0,2541\), con IC 95 % \([0,2003;\ 0,3165]\): 1,58 veces la prevalencia de la base.
Se evalúan los cortes \(c\in\{0{,}05;0{,}06;\dots;0{,}95\}\). Para cada uno se calculan la sensibilidad, la especificidad, el valor predictivo positivo (VPP: proporción de rotantes entre los señalados), el negativo (VPN), la exactitud y \(F_1=2\,\mathrm{VPP}\cdot\mathrm{Sens}/(\mathrm{VPP}+\mathrm{Sens})\), en entrenamiento y en prueba.
R_COSTOS <- c(2, 5, 10)
c_cost <- 1 / R_COSTOS # c* = C_I / C_L (Elkan, 2001)
rej_te %>% select(corte, Sens, Esp, VPP, F1) %>%
pivot_longer(-corte, names_to = "Métrica", values_to = "v") %>%
ggplot(aes(corte, v, colour = Métrica)) +
geom_vline(xintercept = c_cost, colour = "grey80", linetype = "dashed") +
geom_vline(xintercept = c_youden, colour = COL_NAR, linewidth = 0.8) +
geom_line(linewidth = 0.8) +
geom_text(data = . %>% group_by(Métrica) %>% filter(abs(corte - 0.60) < 1e-9),
aes(label = Métrica), vjust = -0.6, size = 3.2, show.legend = FALSE) +
scale_colour_manual(values = c(Sens = COL_AZUL, Esp = COL_JAV, VPP = COL_TEAL, F1 = COL_NAR)) +
scale_x_continuous(breaks = seq(0, 1, 0.1)) +
labs(title = "Subir el corte gana especificidad a costa de dejar escapar rotaciones",
x = "Punto de corte c", y = NULL)Figura 9.1: Métricas de clasificación en la muestra de prueba según el punto de corte. Las líneas verticales marcan el corte de Youden (naranja) y los cortes por costos (gris).
CORTES_SEL <- sort(unique(round(c(0.10, 0.15, 0.20, 0.30, 0.40, 0.50, c_youden, c_cost), 3)))
sel <- bind_rows(lapply(CORTES_SEL, function(c)
bind_cols(metricas(entren$y, p_tr, c) %>% select(corte, Sens, Esp) %>%
rename(`Sens (ent.)` = Sens, `Esp (ent.)` = Esp),
metricas(prueba$y, p_te, c) %>% select(-corte, -Youden))))
tabla(sel %>% rename(Corte = corte), "Métricas de clasificación en cortes seleccionados, en entrenamiento (ent.) y en prueba.",
digits = c(3, 3, 3, 3, 3, 3, 3, 3, 3), font = 8)| Corte | Sens (ent.) | Esp (ent.) | Sens | Esp | VPP | VPN | Exactitud | F1 |
|---|---|---|---|---|---|---|---|---|
| 0,100 | 0,831 | 0,562 | 0,761 | 0,532 | 0,238 | 0,921 | 0,569 | 0,362 |
| 0,150 | 0,753 | 0,710 | 0,662 | 0,695 | 0,294 | 0,915 | 0,689 | 0,407 |
| 0,200 | 0,669 | 0,793 | 0,549 | 0,784 | 0,328 | 0,901 | 0,746 | 0,411 |
| 0,250 | 0,614 | 0,866 | 0,507 | 0,851 | 0,396 | 0,900 | 0,796 | 0,444 |
| 0,300 | 0,506 | 0,908 | 0,451 | 0,903 | 0,471 | 0,895 | 0,830 | 0,460 |
| 0,400 | 0,325 | 0,951 | 0,310 | 0,962 | 0,611 | 0,879 | 0,857 | 0,411 |
| 0,500 | 0,217 | 0,978 | 0,211 | 0,984 | 0,714 | 0,867 | 0,859 | 0,326 |
La Tabla 9.2 y la Figura 9.1 muestran el intercambio entre sensibilidad y especificidad. Con el corte convencional de 0,5 el modelo detecta solo el 21,1 % de las rotaciones de prueba, aunque su exactitud (85,9 %) apenas supera el 83,9 % que se obtendría clasificando a todos como “No”. Es la consecuencia del desbalance anticipada en la Sección 5. El corte de 0,5 no es arbitrario —equivale a una razón de costos \(R=2\), como se ve en la Sección 9.4—, pero con esa exactitud alta y esa sensibilidad baja resulta poco útil si perder a un empleado cuesta bastante más que retenerlo.
El índice \(J(c)=\text{Sens}(c)+\text{Esp}(c)-1\) es la distancia vertical entre la curva ROC y la diagonal; su máximo da el corte que mejor separa los grupos si los dos errores pesan igual (Youden, 1950). El corte se elige en entrenamiento y se evalúa en prueba, para que las métricas reportadas no estén sesgadas a favor del corte.
El corte de Youden en entrenamiento es \(c_J=0,25\) (\(J=0,480\)). En prueba detecta el 50,7 % de las rotaciones con una especificidad del 85,1 % (Tabla 8.4; métricas completas en la Tabla 8.5). El VPP es 39,6 %: de cada 100 empleados señalados, unos 40 efectivamente rotan, frente a 16 si se eligieran al azar.
Sean \(C_I\) el costo de una acción de retención y \(C_L\) el costo de perder a un empleado. Se supone que la intervención evita la rotación (\(e=1\)) y que su costo se paga siempre, rote o no el empleado. Si \(\hat\pi\) está calibrada, intervenir tiene costo esperado \(C_I\) y no intervenir, \(\hat\pi\,C_L\). Conviene intervenir cuando \[ \hat\pi\,C_L\ \ge\ C_I \iff \hat\pi\ \ge\ c^*=\frac{C_I}{C_L}=\frac{1}{R}, \qquad R=\frac{C_L}{C_I} . \] Con eficacia parcial \(e<1\), el corte es \(c^*=1/(eR)\) (Elkan, 2001). El supuesto es que perder a un empleado cuesta más que intervenirlo (\(R>1\)). Como \(R\) no se conoce, se evalúa \(R\in\{2,\,5,\,10\}\), que da \(c^*=0,500;\ 0,200;\ 0,100\). Con \(R=2\) el corte coincide con el convencional de 0,5: usar 0,5 equivale a suponer que perder a un empleado cuesta solo el doble que intervenirlo. La calibración verificada en la Sección 8 respalda el uso de este criterio.
costo_tab <- bind_rows(lapply(seq_along(R_COSTOS), function(i) {
m <- metricas(prueba$y, p_te, c_cost[i])
data.frame(R = R_COSTOS[i], Corte = c_cost[i], Sens = m$Sens, Esp = m$Esp, VPP = m$VPP,
`% de empleados intervenidos` = 100 * mean(p_te >= c_cost[i]), check.names = FALSE)
}))
tabla(costo_tab, "Cortes por costos y su desempeño en la muestra de prueba.",
digits = c(0, 3, 3, 3, 3, 1))| R | Corte | Sens | Esp | VPP | % de empleados intervenidos |
|---|---|---|---|---|---|
| 2 | 0,500 | 0,211 | 0,984 | 0,714 | 4,8 |
| 5 | 0,200 | 0,549 | 0,784 | 0,328 | 27,0 |
| 10 | 0,100 | 0,761 | 0,532 | 0,238 | 51,5 |
A mayor \(R\), más bajo el corte: se interviene a más empleados para no perder rotaciones. Con \(R=10\) se detecta el 76,1 % de las rotaciones, pero se interviene al 51,5 % de la plantilla (Tabla 9.3).
CRIT <- data.frame(Criterio = c("Convencional", "Youden", paste0("Costos, R = ", R_COSTOS)),
Corte = c(0.5, c_youden, c_cost))
pi_hat_tr <- unname(predict(modelo_tr, PERFIL, type = "response"))
dec <- CRIT %>% mutate(
`pi estimada` = pi_hat,
Decisión = ifelse(pi_hat >= Corte, "Intervenir", "No intervenir"),
`pi entrenamiento` = pi_hat_tr,
`Decisión (entren.)` = ifelse(pi_hat_tr >= Corte, "Intervenir", "No intervenir"),
`Estable en c ± 0,05` = ifelse(abs(pi_hat - Corte) > 0.05 & abs(pi_hat_tr - Corte) > 0.05,
"Sí", "No"),
`IC contiene c` = ifelse(pi_ic[1] <= Corte & Corte <= pi_ic[2], "Sí (incierta)", "No"))
names(dec)[c(3, 5)] <- c("$\\hat\\pi_0$ (completa)", "$\\hat\\pi_0$ (entren.)")
tabla(dec, "Decisión de intervenir al empleado hipotético según cada criterio de corte, con los dos modelos.",
digits = c(0, 3, 4, 0, 4, 0, 0, 0), font = 8)| Criterio | Corte | \(\hat\pi_0\) (completa) | Decisión | \(\hat\pi_0\) (entren.) | Decisión (entren.) | Estable en c ± 0,05 | IC contiene c |
|---|---|---|---|---|---|---|---|
| Convencional | 0,500 | 0,2541 | No intervenir | 0,2425 | No intervenir | Sí | No |
| Youden | 0,250 | 0,2541 | Intervenir | 0,2425 | No intervenir | No | Sí (incierta) |
| Costos, R = 2 | 0,500 | 0,2541 | No intervenir | 0,2425 | No intervenir | Sí | No |
| Costos, R = 5 | 0,200 | 0,2541 | Intervenir | 0,2425 | Intervenir | No | No |
| Costos, R = 10 | 0,100 | 0,2541 | Intervenir | 0,2425 | Intervenir | Sí | No |
La decisión depende del criterio (Tabla 9.4). Una decisión es estable si \(|\hat\pi_0-c|>0{,}05\) con los dos modelos (base completa y entrenamiento), es decir, si no cambia al mover el corte en \(\pm0{,}05\) con ninguno de ellos; es incierta si el IC de \(\hat\pi_0\) contiene al corte, porque entonces los datos no permiten afirmar de qué lado está la probabilidad verdadera.
El corte de Youden se elige con el modelo de entrenamiento, mientras que \(\hat\pi_0\) se calcula con el de la base completa. Con el modelo de entrenamiento, \(\hat\pi_0=0,2425\): las decisiones coinciden en los cortes por costos, pero no en el de Youden, que el empleado supera con un modelo y no con el otro. La decisión con ese criterio depende, entonces, de qué modelo se use, y no solo del corte; la recomendación se apoya en el criterio de costos.
Con \(\hat\pi_0=0,254\), el empleado supera el corte de costos con \(R=5\), aunque el límite inferior de su IC (0,2003) queda casi sobre el corte (0,20); no alcanza el de \(R=2\), que es el corte convencional de 0,5. Intervenir conviene cuando \(R\ge 1/\hat\pi_0\), es decir, cuando perder al empleado cuesta al menos 3,9 veces lo que cuesta la intervención (4,1 con el modelo de entrenamiento). Se recomienda intervenirlo si la empresa acepta esa razón de costos: su riesgo es 1,58 veces el promedio, y lo explican variables modificables (un ingreso bajo y poco tiempo en el cargo) y una no modificable (ser soltero). Si la empresa estima que la rotación cuesta menos de esa razón —por ejemplo, el doble que la intervención—, la recomendación se invierte. La acción sugerida es la que corresponde a sus factores de riesgo: revisar su trayectoria salarial y darle un plan de desarrollo en el cargo.
p_mod <- av$`Pr(>Chisq)`
s_mod <- sign(c(b["Horas_ExtraSi"], b["Estado_CivilSoltero"], b["Viaje_NegociosFrecuentemente"],
b["Edad"], b["log(Ingreso_Mensual)"], b["Antigüedad_Cargo"]))
balance <- data.frame(Variable = HIP$Variable, `Dirección esperada` = HIP$`Dirección esperada`,
`Bivariado` = contraste$Veredicto,
`Modelo ajustado` = veredicto(p_mod, s_mod, HIP$Signo),
`p (LR, modelo)` = pval(p_mod),
`Rango de importancia` = match(c("Horas_Extra", "Estado_Civil", "Viaje_Negocios",
"Edad", "ln(Ingreso_Mensual)", "Antigüedad_Cargo"),
av_tab$Término),
check.names = FALSE)
tabla(balance, "Balance de las hipótesis en el análisis bivariado y en el modelo ajustado.", font = 8.5)| Variable | Dirección esperada | Bivariado | Modelo ajustado | p (LR, modelo) | Rango de importancia |
|---|---|---|---|---|---|
| Horas_Extra | Sí > No | Confirmada | Confirmada | \(\lt 2\times10^{-16}\) | 1 |
| Estado_Civil | Soltero > Casado | Confirmada | Confirmada | \(8,45\times10^{-8}\) | 2 |
| Viaje_Negocios | Creciente con la frecuencia | Confirmada | Confirmada | \(4,25\times10^{-5}\) | 3 |
| Edad | Negativa | Confirmada | Confirmada | 0,015 | 6 |
| Ingreso_Mensual | Negativa | Confirmada | Confirmada | \(7,44\times10^{-5}\) | 4 |
| Antigüedad_Cargo | Negativa | Confirmada | Confirmada | 0,001 | 5 |
Las 6 hipótesis se confirman en ambos análisis (Tabla 10.1): ninguna resultó significativa en sentido contrario ni dejó de ser significativa al ajustar. La única precisión es que el efecto del estado civil corresponde a los solteros; los divorciados no difieren de los casados.
Las acciones siguen el orden de importancia del modelo. Solo se proponen sobre variables significativas, y se distingue entre las que la empresa puede modificar y las que solo sirven para focalizar.
El modelo permite, además, priorizar la intervención: calcular \(\hat\pi\) para cada empleado y aplicar el corte que corresponda a la razón de costos que estime la empresa (Sección 9).
Rotación. La variable no distingue entre un cambio interno de cargo y un retiro de la empresa, que podrían tener determinantes distintos.Agresti, A. (2013). Categorical Data Analysis (3.ª ed.). Wiley.
Anderson, T. W. y Darling, D. A. (1954). A test of goodness of fit. Journal of the American Statistical Association, 49(268), 765–769.
Armitage, P. (1955). Tests for linear trends in proportions and frequencies. Biometrics, 11(3), 375–386.
Box, G. E. P. y Tidwell, P. W. (1962). Transformation of the independent variables. Technometrics, 4(4), 531–550.
Clopper, C. J. y Pearson, E. S. (1934). The use of confidence or fiducial limits illustrated in the case of the binomial. Biometrika, 26(4), 404–413.
Cohen, J. (1960). A coefficient of agreement for nominal scales. Educational and Psychological Measurement, 20(1), 37–46.
DeLong, E. R., DeLong, D. M. y Clarke-Pearson, D. L. (1988). Comparing the areas under two or more correlated receiver operating characteristic curves: A nonparametric approach. Biometrics, 44(3), 837–845.
Elkan, C. (2001). The foundations of cost-sensitive learning. Proceedings of the Seventeenth International Joint Conference on Artificial Intelligence (IJCAI), 973–978.
Fawcett, T. (2006). An introduction to ROC analysis. Pattern Recognition Letters, 27(8), 861–874.
Fox, J. y Monette, G. (1992). Generalized collinearity diagnostics. Journal of the American Statistical Association, 87(417), 178–183.
Greenland, S., Robins, J. M. y Pearl, J. (1999). Confounding and collapsibility in causal inference. Statistical Science, 14(1), 29–46.
Harrell, F. E. (2015). Regression Modeling Strategies (2.ª ed.). Springer.
Hauck, W. W. y Donner, A. (1977). Wald’s test as applied to hypotheses in logit analysis. Journal of the American Statistical Association, 72(360), 851–853.
Holm, S. (1979). A simple sequentially rejective multiple test procedure. Scandinavian Journal of Statistics, 6(2), 65–70.
Hosmer, D. W., Lemeshow, S. y Sturdivant, R. X. (2013). Applied Logistic Regression (3.ª ed.). Wiley.
Joanes, D. N. y Gill, C. A. (1998). Comparing measures of sample skewness and kurtosis. Journal of the Royal Statistical Society, Series D, 47(1), 183–189.
Mann, H. B. y Whitney, D. R. (1947). On a test of whether one of two random variables is stochastically larger than the other. The Annals of Mathematical Statistics, 18(1), 50–60.
Matthews, B. W. (1975). Comparison of the predicted and observed secondary structure of T4 phage lysozyme. Biochimica et Biophysica Acta, 405(2), 442–451.
McFadden, D. (1979). Quantitative methods for analysing travel behaviour of individuals: Some recent developments. En D. A. Hensher y P. R. Stopher (Eds.), Behavioural Travel Modelling (pp. 279–318). Croom Helm.
McNemar, Q. (1947). Note on the sampling error of the difference between correlated proportions or percentages. Psychometrika, 12(2), 153–157.
Peduzzi, P., Concato, J., Kemper, E., Holford, T. R. y Feinstein, A. R. (1996). A simulation study of the number of events per variable in logistic regression analysis. Journal of Clinical Epidemiology, 49(12), 1373–1379.
Shapiro, S. S. y Wilk, M. B. (1965). An analysis of variance test for normality (complete samples). Biometrika, 52(3–4), 591–611.
Tukey, J. W. (1977). Exploratory Data Analysis. Addison-Wesley.
Weiers, R. M. (2006). Introducción a la estadística para negocios (5.ª ed.). Thomson.
Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. Journal of the American Statistical Association, 22(158), 209–212.
Youden, W. J. (1950). Index for rating diagnostic tests. Cancer, 3(1), 32–35.
install.packages("devtools") # solo la primera vez
devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
install.packages(c("dplyr", "tidyr", "ggplot2", "kableExtra", "car", "pROC",
"rms", "e1071", "nortest", "gridExtra", "bookdown", "rmarkdown"))El código de cada tabla y de cada figura se despliega con el botón Code que aparece a su derecha; el menú Code del encabezado los muestra u oculta todos a la vez. La compilación del archivo .Rmd reproduce todas las cifras con la semilla 2026.
tabla(hl_grp, "Rotaciones observadas y esperadas por decil de probabilidad estimada.",
digits = c(0, 0, 0, 1, 1))| Decil | n | Observados | Esperados | Observados - esperados |
|---|---|---|---|---|
| 1 | 147 | 7 | 2,8 | 4,2 |
| 2 | 147 | 9 | 5,4 | 3,6 |
| 3 | 147 | 11 | 8,0 | 3,0 |
| 4 | 147 | 9 | 10,7 | -1,7 |
| 5 | 147 | 7 | 13,9 | -6,9 |
| 6 | 147 | 17 | 18,0 | -1,0 |
| 7 | 147 | 19 | 24,0 | -5,0 |
| 8 | 147 | 24 | 32,7 | -8,7 |
| 9 | 147 | 51 | 45,2 | 5,8 |
| 10 | 147 | 83 | 76,3 | 6,7 |
La Tabla 11.1 desagrega la prueba de Hosmer-Lemeshow con \(g=10\). En los tres primeros deciles y en los dos últimos se observan más rotaciones de las esperadas; en los deciles intermedios, menos.
| Medida | Umbral | Observaciones |
|---|---|---|
| Distancia de Cook \(\gt 4/n\) | 0,0027 | 115 |
| Distancia de Cook \(\gt 1\) | 1,0000 | 0 |
| Apalancamiento \(\gt 2p/n\) | 0,0122 | 125 |
| Algún \(|\mathrm{DFBETAS}| \gt 2/\sqrt{n}\) | 0,0522 | 364 |
| Término | Máximo |DFBETAS| | Observaciones con |DFBETAS| > 2/raíz(n) |
|---|---|---|
| Intercepto | 0,147 | 121 |
| Horas extra: Sí vs. No | 0,097 | 202 |
| Estado civil: Divorciado vs. Casado | 0,140 | 98 |
| Estado civil: Soltero vs. Casado | 0,090 | 197 |
| Viaje: Raramente vs. No viaja | 0,268 | 35 |
| Viaje: Frecuentemente vs. No viaja | 0,247 | 74 |
| Edad (por año) | 0,235 | 132 |
| ln(Ingreso mensual) | 0,150 | 123 |
| Antigüedad en el cargo (por año) | 0,222 | 104 |
Con el umbral \(4/n\) se marcan 115 observaciones (Tabla 11.2); el 95,7 % son rotaciones, con una mediana de \(\hat\pi\) de 0,149: empleados que rotaron con un perfil de bajo riesgo, no errores de registro. Las 125 observaciones con apalancamiento mayor que \(2p/n\) corresponden a combinaciones poco frecuentes de las covariables; su apalancamiento máximo es 0,0341, y ninguna de ellas tiene distancia de Cook cercana a 1. El DFBETAS mide el cambio en \(\hat\beta_j\) al excluir la observación, en unidades de su error estándar; el máximo por coeficiente está en la Tabla 11.3.
Nota
Excluir en bloque las observaciones con Cook \(>4/n\) no es un análisis de sensibilidad válido aquí: casi todas son rotaciones, así que eliminarlas vaciaría categorías con pocos eventos (en No_Viaja solo rotaron 12 empleados) y produciría separación cuasicompleta, con OR artificialmente grandes.
tabla(data.frame(Modelo = c("Lineal (principal)", "Splines naturales"),
Parámetros = c(length(coef(modelo)), length(coef(modelo_ns))),
AIC = c(AIC(modelo), AIC(modelo_ns)),
`AUC aparente` = c(auc_lin, auc_ns), check.names = FALSE),
"Comparación del modelo lineal con el modelo con splines.", digits = c(0, 0, 1, 4))| Modelo | Parámetros | AIC | AUC aparente |
|---|---|---|---|
| Lineal (principal) | 9 | 1.092,6 | 0,7776 |
| Splines naturales | 15 | 1.079,7 | 0,7847 |
ggplot(curvas, aes(x, eta, colour = Modelo)) + geom_line(linewidth = 0.8) +
scale_colour_manual(values = c(Lineal = COL_VIO, Splines = COL_TEAL)) +
facet_wrap(~ var, scales = "free_x", nrow = 1) +
labs(title = "Misma dirección; con splines, el riesgo se concentra en valores bajos",
x = NULL, y = "Logit estimado")Figura 11.1: Efecto parcial de cada covariable cuantitativa sobre el logit, en el modelo lineal (violeta) y en el modelo con splines (verde azulado). Las demás covariables se fijan en su mediana o en su nivel de referencia.
El modelo alternativo reemplaza cada cuantitativa por un spline cúbico natural con 3 grados de libertad (splines::ns). Mejora el ajuste (\(G^2=24,91\), 6 gl, \(p=3,55\times10^{-4}\)), pero la ganancia en discriminación es pequeña (Tabla 11.4). Las curvas del ingreso y de la antigüedad en el cargo son decrecientes; la de la edad tiene forma de U, con mínimo cerca de los 43 años y un aumento del riesgo en las edades mayores, compatible con retiros (Figura 11.1). La dirección lineal negativa de la edad describe la tendencia promedio, no un efecto monótono. Con los splines, la prueba de Hosmer-Lemeshow da \(p=0,101\), \(p=0,005\) y \(p=0,148\) para \(g=8,\,10,\,12\): la no linealidad explica parte de la falta de ajuste del modelo lineal, aunque con \(g=10\) la prueba sigue rechazando.
si <- sessionInfo()
paq <- c("paqueteMODELOS", "dplyr", "tidyr", "ggplot2", "kableExtra", "car", "pROC",
"rms", "e1071", "nortest")
tabla(data.frame(
Elemento = c("Versión de R", "Plataforma", "Semilla global",
paste("Paquete", paq)),
Valor = c(si$R.version$version.string, si$R.version$platform, "2026",
sapply(paq, function(p) as.character(packageVersion(p)))),
row.names = NULL),
"Entorno de cómputo utilizado.")| Elemento | Valor |
|---|---|
| Versión de R | R version 4.5.2 (2025-10-31 ucrt) |
| Plataforma | x86_64-w64-mingw32 |
| Semilla global | 2026 |
| Paquete paqueteMODELOS | 0.1.0 |
| Paquete dplyr | 1.2.1 |
| Paquete tidyr | 1.3.2 |
| Paquete ggplot2 | 4.0.3 |
| Paquete kableExtra | 1.4.0 |
| Paquete car | 3.1.3 |
| Paquete pROC | 1.19.0.1 |
| Paquete rms | 8.1.1 |
| Paquete e1071 | 1.7.17 |
| Paquete nortest | 1.0.4 |
Oriana Giraldo Arcia · Luis Javier Rubio Hernández
Pontificia Universidad Javeriana Cali