Analítica de Personas · Semestre otoño 2026 · Semana 9 · Prof. René Gempp
En el Apunte 23 aprendiste a comparar curvas de Kaplan-Meier entre grupos con el log-rank test. Pero hay un problema: el log-rank compara curvas sin controlar por otras variables.
Si la curva de Soporte Técnico cae más rápido que la de Desarrollo, ¿es porque el departamento en sí causa más rotación, o porque Soporte Técnico tiene empleados más jóvenes, menos satisfechos, con menores salarios? El log-rank no puede distinguir — es como comparar salarios brutos entre hombres y mujeres sin controlar por cargo ni antigüedad (Clase 5).
El modelo de riesgos proporcionales de Cox (Cox, 1972) resuelve esto: es el análogo de la regresión múltiple para datos de supervivencia. Permite estimar el efecto de cada covariable sobre el riesgo de salida, controlando por las demás.
coxph(): sintaxis y ajusteLa sintaxis de coxph() es idéntica a la de lm() y glm(), con la diferencia de que el lado izquierdo de la fórmula es un objeto Surv():
# Modelo de Cox
modelo_cox <- coxph(
Surv(tiempo_meses, evento) ~
satisfaccion_laboral +
evaluacion_desempeno +
departamento +
genero +
ingreso_mensual +
horas_capacitacion +
nivel_jerarquico,
data = datos_surv
)
# Resumen del modelo
summary(modelo_cox)
Call:
coxph(formula = Surv(tiempo_meses, evento) ~ satisfaccion_laboral + ...
n= 1200, number of events= 326
coef exp(coef) se(coef) z Pr(>|z|)
satisfaccion_laboral -0.1523 0.8588 0.0412 -3.696 0.000219 ***
evaluacion_desempeno -0.0847 0.9188 0.0398 -2.128 0.033345 *
departamentoFinanzas 0.0912 1.0955 0.2148 0.425 0.671120
departamentoMarketing 0.1574 1.1705 0.2534 0.621 0.534520
departamentoRRHH 0.3821 1.4654 0.1945 1.965 0.049420 *
departamentoSoporte 0.4265 1.5320 0.1621 2.631 0.008512 **
departamentoVentas 0.2041 1.2264 0.1589 1.284 0.199020
generoMujer -0.0312 0.9693 0.1145 -0.273 0.785190
ingreso_mensual -0.0001 0.9999 0.0001 -1.412 0.157900
...
Concordance= 0.621 (se = 0.016)
Likelihood ratio test= 42.3 on 11 df, p=1.5e-05Las columnas clave del output son:
| Columna | Qué es | Cómo se lee |
|---|---|---|
coef | Coeficiente β en escala log-hazard | Negativo = reduce riesgo; positivo = aumenta riesgo |
exp(coef) | Hazard ratio (HR) — la métrica que reportas | HR < 1 = protector; HR > 1 = factor de riesgo |
se(coef) | Error estándar del coeficiente | Menor = estimación más precisa |
z | Estadístico de Wald (coef / se) | Análogo del t del lm() |
Pr(>|z|) | p-valor | Significancia estadística |
Concordance | C-index (análogo del AUC) | Capacidad discriminativa del modelo (ver §7) |
El hazard ratio (HR) es la medida de efecto del modelo de Cox. Es el exponencial del coeficiente β: HR = exp(β). Para obtener los HR directamente con broom::tidy():
library(broom)
hr_tabla <- tidy(modelo_cox,
exponentiate = TRUE, # exp(β) = HR
conf.int = TRUE) |>
select(term, HR = estimate, IC_bajo = conf.low,
IC_alto = conf.high, p_valor = p.value)
print(hr_tabla)
| HR | Significado | Ejemplo |
|---|---|---|
| HR = 1,00 | Sin efecto | El género no cambia el riesgo de salida |
| HR = 1,53 | Riesgo 53 % mayor | Soporte Técnico tiene un riesgo instantáneo de salida 53 % mayor que la categoría de referencia |
| HR = 0,86 | Riesgo 14 % menor | Cada punto adicional de satisfacción reduce el riesgo instantáneo de salida en 14 % |
| HR = 2,00 | Riesgo doble | El factor duplica el riesgo de salida en cualquier momento |
| HR = 0,50 | Riesgo a la mitad | El factor protector reduce el riesgo a la mitad |
HR = 1,53 no significa que el empleado tiene "53 % de probabilidad de irse". Significa que su tasa instantánea de salida es 53 % más alta que la referencia, controlando por las demás variables. Es un multiplicador del riesgo, no una probabilidad.
Para una variable continua (como satisfaccion_laboral, escala 1–5), el HR se interpreta por cada unidad de aumento. Si HR = 0,86, cada punto adicional en la escala reduce el riesgo en un 14 %. Dos puntos de diferencia: 0,86² ≈ 0,74 → reduce el riesgo en un 26 %.
Para una variable categórica (como departamento), R automáticamente crea variables dummy. El HR compara cada categoría con la categoría de referencia (por defecto, la primera en orden alfabético). Verifica cuál es la referencia en tu output.
En la Clase 3 usaste odds ratios (OR) de la regresión logística. Ahora tienes hazard ratios (HR) del modelo de Cox. Son métricas conceptualmente distintas:
| Odds Ratio (OR) | Hazard Ratio (HR) | |
|---|---|---|
| Modelo | Regresión logística glm() | Regresión de Cox coxph() |
| Pregunta | ¿Cuántas veces más probable es que se vaya? | ¿Cuántas veces más rápido se va? |
| Incorpora el tiempo | No — es un snapshot (sí/no) | Sí — modela la trayectoria temporal |
| Relación con proporción | Aproxima el riesgo relativo solo si el evento es raro (< 10 %) | Estima directamente el riesgo relativo instantáneo |
En InnovaCo, la tasa de rotación es 27,2 %. Eso no es un evento raro. Por tanto, el OR sobreestimará el efecto relativo comparado con el HR. Para ver esto empíricamente, puedes estimar ambos modelos con las mismas covariables y comparar lado a lado:
# Modelo logístico (mismo que Clase 3)
modelo_log <- glm(evento ~ satisfaccion_laboral + evaluacion_desempeno +
departamento + genero + ingreso_mensual,
data = datos_surv, family = binomial)
# Tabla comparativa
or_tabla <- tidy(modelo_log, exponentiate = TRUE, conf.int = TRUE) |>
select(term, OR = estimate)
hr_tabla2 <- tidy(modelo_cox, exponentiate = TRUE, conf.int = TRUE) |>
select(term, HR = estimate)
left_join(or_tabla, hr_tabla2, by = "term")
El forest plot es la visualización estándar de los hazard ratios: una fila por variable, con un punto (HR) y una barra horizontal (IC 95 %). La línea vertical en HR = 1 es la referencia: si la barra cruza el 1, el efecto no es significativo al 5 %.
ggforest()
El paquete survminer incluye la función ggforest(modelo_cox, data = datos_surv) que genera un forest plot con un solo llamado. Sin embargo, tiene un bug documentado: cuando las variables categóricas tienen niveles con espacios ("Soporte Tecnico", "Recursos Humanos"), el merge interno falla con el error "los nombres no coinciden con los nombres anteriores". Este bug lleva abierto desde 2019 y no ha sido resuelto. Por eso, construimos el forest plot manualmente con ggplot2.
ggplot2 (recomendado)Partimos de la tabla de hazard ratios generada con broom::tidy() y la visualizamos directamente. Este enfoque es más robusto y nos da control total sobre la estética:
# 1. Limpiar los nombres de coeficientes para legibilidad
hr_plot <- hr_tabla |>
mutate(
etiqueta = term |>
str_replace("^departamento", "Depto: ") |>
str_replace("^nivel_jerarquico", "Nivel: ") |>
str_replace("^genero", "Género: ") |>
str_replace("satisfaccion_laboral", "Satisfacción laboral") |>
str_replace("evaluacion_desempeno", "Evaluación desempeño") |>
str_replace("ingreso_mensual", "Ingreso mensual") |>
str_replace("horas_capacitacion", "Horas capacitación"),
direccion = if_else(HR > 1, "Riesgo", "Protector")
) |>
mutate(etiqueta = fct_reorder(etiqueta, HR))
# 2. Forest plot
ggplot(hr_plot, aes(x = HR, y = etiqueta, color = direccion)) +
geom_point(size = 3) +
geom_errorbarh(aes(xmin = IC_bajo, xmax = IC_alto), height = 0.25) +
geom_vline(xintercept = 1, linetype = "dashed", color = "grey50") +
scale_color_manual(
values = c("Riesgo" = "#B85042", "Protector" = "#0D9488"),
guide = "none"
) +
labs(
title = "¿Qué factores aceleran o frenan la salida?",
subtitle = "Hazard ratios con IC 95 % — Modelo de Cox",
x = "Hazard Ratio", y = NULL,
caption = "HR > 1 = mayor riesgo · HR < 1 = menor riesgo"
) +
theme_minimal() +
theme(panel.grid.major.y = element_blank())
ggforest(), el enfoque manual te permite: (1) usar colores distintos para factores de riesgo vs. protectores, (2) limpiar las etiquetas para que el directorio las entienda, (3) filtrar solo las variables significativas, y (4) aplicar la paleta visual del curso. Es más trabajo inicial, pero el resultado es más profesional y reproducible.
cox.zph(): verificar proporcionalidadEl supuesto fundamental del modelo de Cox es que los hazard ratios son constantes en el tiempo. Si el HR de Soporte Técnico es 1,53, debe serlo a los 6 meses y a los 60 meses. Si el efecto cambia con el tiempo, el modelo viola su supuesto principal.
El test de Grambsch y Therneau (1994) evalúa esto formalmente:
# Test de proporcionalidad
test_ph <- cox.zph(modelo_cox)
print(test_ph)
chisq df p satisfaccion_laboral 5.23 1 0.022 evaluacion_desempeno 0.87 1 0.351 departamentoFinanzas 0.12 1 0.725 ... GLOBAL 14.5 11 0.205
Un p-valor significativo (p < 0,05) para una variable indica que su HR no es constante en el tiempo. El test GLOBAL evalúa el supuesto para todo el modelo conjuntamente.
# Visualizar los residuos de Schoenfeld
ggcoxzph(test_ph)
Los gráficos de residuos de Schoenfeld muestran el efecto estimado de cada variable a lo largo del tiempo. Si la línea ajustada es horizontal, el supuesto se cumple. Si tiene pendiente, el efecto cambia con el tiempo.
| Gravedad | Criterio | Acción |
|---|---|---|
| Leve | p entre 0,01 y 0,05 para una variable, test global no significativo | Reportar como limitación. El HR sigue siendo informativo como promedio del efecto temporal |
| Moderada | p < 0,01 para una variable importante | Considerar un modelo estratificado: strata(variable) permite que cada grupo tenga su propia función de riesgo base |
| Grave | Test global significativo, múltiples variables con p < 0,01 | Considerar un modelo paramétrico (Weibull, log-normal) o un modelo con interacción temporal — fuera del alcance de este curso |
# Ejemplo de modelo estratificado
# Si "departamento" viola proporcionalidad:
modelo_strat <- coxph(
Surv(tiempo_meses, evento) ~
satisfaccion_laboral + evaluacion_desempeno +
genero + ingreso_mensual +
strata(departamento), # ← no estima HR para depto, pero lo controla
data = datos_surv
)
summary(modelo_strat)
El C-index (o concordancia) es la medida de capacidad discriminativa del modelo de Cox. Es conceptualmente análogo al AUC de la regresión logística: mide qué tan bien el modelo distingue entre personas que se van rápido y personas que se quedan.
# Extraer el C-index
summary(modelo_cox)$concordance
| C-index | Interpretación |
|---|---|
| 0,50 | Discriminación al azar (el modelo no aporta nada) |
| 0,60–0,70 | Discriminación aceptable en HR analytics |
| 0,70–0,80 | Buena discriminación |
| > 0,80 | Excelente (raro en datos organizacionales) |
En datos de rotación, C-indexes de 0,60–0,65 son habituales: los factores que medimos (satisfacción, salario, departamento) explican parte de la rotación, pero hay factores no observables (ofertas externas, situación personal, relación con el jefe directo) que el modelo no captura.
Las curvas de Kaplan-Meier del Apunte 23 son "crudas": muestran la supervivencia de cada grupo sin controlar por nada. Pero a veces quieres ver la curva de un grupo después de controlar por las demás variables. survminer::ggadjustedcurves() hace esto:
# Curvas de supervivencia ajustadas por departamento
ggadjustedcurves(
modelo_cox,
data = datos_surv,
variable = "departamento",
method = "average"
) +
labs(
title = "Supervivencia ajustada por departamento",
subtitle = "Controlando por satisfacción, desempeño, salario y género",
x = "Tiempo (meses)",
y = "Probabilidad de permanencia"
)
La diferencia entre la curva cruda (Kaplan-Meier) y la ajustada (Cox) te dice cuánto de la diferencia entre grupos se explica por las covariables. Si las curvas ajustadas convergen (se acercan), las diferencias crudas se debían a confounders. Si siguen separadas, hay algo propio del grupo que no capturan las covariables.
El modelo de Cox es la herramienta estándar, pero la estadística de supervivencia moderna tiene extensiones poderosas que este curso no cubre. Las nombramos para que sepas dónde buscar si las necesitas:
| Extensión | Qué resuelve | Cuándo la necesitarías |
|---|---|---|
| Modelos paramétricos (Weibull, log-normal) | Asumen una distribución para el tiempo de supervivencia | Cuando tienes razones teóricas para una forma funcional específica (ej: fallas de componentes con desgaste) |
| Modelos de fragilidad (frailty) | Capturan heterogeneidad no observada (análogos a efectos aleatorios) | Cuando sospechas que hay variabilidad entre individuos que las covariables no explican |
| Riesgos competitivos (competing risks) | Distinguen tipos de evento (renuncia vs. despido vs. jubilación) | Cuando los tipos de salida tienen causas y consecuencias distintas |
| Covariables que varían en el tiempo | Permiten que las covariables cambien durante el seguimiento | Cuando un empleado recibe una promoción o cambio salarial a mitad del periodo |
coxph(). Para eso necesitas survfit(modelo_cox) o ggadjustedcurves().
cox.zph() después de ajustar un modelo de Cox. Si el supuesto se viola y no lo reportas, los HR que presentas al directorio pueden ser engañosos. Un HR "promedio" de 1,53 puede esconder que el riesgo es 2,0 en el primer año y 1,0 después — lo que cambia completamente la recomendación de intervención.
nivel_educacion está estructuralmente anidado en nivel_jerarquico en InnovaCo, lo que genera colinealidad. Incluye solo nivel_jerarquico.