Analítica de Personas · Semestre otoño 2026 · Semana 9 · Prof. René Gempp
En la Clase 3 construimos un modelo logístico para predecir si un empleado se va de InnovaCo. La variable era binaria: "Sí" o "No". Pero el directorio ahora quiere saber algo distinto: ¿cuánto tiempo se quedan? ¿Hay un momento crítico donde es más probable que se vayan? ¿Se van más rápido en Ventas que en Desarrollo?
El análisis de supervivencia es la familia de técnicas estadísticas diseñadas para estudiar el tiempo hasta que ocurre un evento. El nombre viene de la medicina —originalmente se usaba para estudiar cuánto tiempo sobrevivían pacientes después de un tratamiento— pero hoy se aplica en muchos campos:
| Campo | Evento | Tiempo |
|---|---|---|
| Medicina | Muerte, recurrencia de cáncer | Días/meses desde diagnóstico |
| Ingeniería | Falla de un componente | Horas de operación |
| People analytics | Salida del empleado | Meses desde ingreso |
| People analytics | Cierre de vacante (time-to-fill) | Días desde apertura |
| People analytics | Promoción | Meses en el cargo actual |
| Marketing | Cancelación de suscripción (churn) | Meses desde alta |
La semana pasada ya hiciste un primer análisis de "tiempo hasta evento" sin saberlo: cuando calculaste el time-to-fill con lubridate, estabas midiendo cuánto tardaba en cerrarse una vacante. La diferencia es que todas las vacantes del dataset estaban cerradas: no había observaciones incompletas. Hoy eso cambia.
Imagina que quieres responder: "¿Cuánto tiempo permanecen nuestros empleados?" Tu primer instinto podría ser calcular el promedio de antigüedad:
mean(empleados$antiguedad_anios)
# [1] 3.5
Pero hay un problema grave: ese promedio mezcla dos tipos de datos completamente distintos:
Los empleados que siguen tienen una antigüedad que es un límite inferior de su permanencia real. Incluirlos en el promedio como si fueran datos completos subestima la permanencia real.
mean(antiguedad_anios) mezclando empleados activos y desvinculados es estadísticamente incorrecto. Es como calcular la esperanza de vida promedio incluyendo personas que todavía están vivas: el resultado no tiene sentido.
En la jerga del análisis de supervivencia, un empleado activo es una observación censurada por la derecha (right-censored): sabemos que al menos lleva X años, pero no sabemos cuándo (ni si) experimentará el evento. La censura no es un dato perdido — es una observación parcial que aporta información valiosa.
| Tipo | Qué significa | Ejemplo en HR | Uso en este curso |
|---|---|---|---|
| Censura por la derecha | El evento no ha ocurrido al cierre del estudio | El empleado sigue activo al 1 de marzo de 2025 | Sí — es el caso dominante |
| Censura por la izquierda | El evento ya había ocurrido antes del inicio del estudio | Un empleado que se fue antes de que existiera el SIRH | No se cubre |
| Censura por intervalo | Solo sabemos que el evento ocurrió dentro de un rango | "Se fue entre enero y marzo, no sabemos el día exacto" | No se cubre |
Un diagrama de líneas de vida muestra cada empleado como una línea horizontal desde su ingreso hasta su salida (o hasta el cierre del estudio). Una × marca un evento (se fue); una → marca una observación censurada (sigue).
# Ejemplo con 8 empleados (concepto):
# Ana: |────────× (se fue a los 14 meses)
# Bruno: |──────────────────→ (sigue a los 24 meses: censurado)
# Carla: |───× (se fue a los 5 meses)
# Diego: |────────────→ (sigue a los 18 meses: censurado)
# Elena: |──────────────────────→ (sigue a los 30 meses: censurado)
# Felipe: |─────────× (se fue a los 12 meses)
# Gloria: |──× (se fue a los 3 meses)
# Hugo: |──────────────────────────→ (sigue a los 36 meses: censurado)
De estos 8 empleados, 4 se fueron (eventos) y 4 siguen (censurados). El Kaplan-Meier usa toda esta información para estimar la curva de permanencia.
survivalEl análisis de supervivencia en R se apoya en el paquete survival, escrito y mantenido por Terry Therneau (Mayo Clinic). Es uno de los paquetes más antiguos de R — existe desde la versión 2.x — y viene pre-instalado con la distribución estándar. No necesitas install.packages(), solo cargarlo:
library(survival) # Surv(), survfit(), coxph(), survdiff(), cox.zph()
library(survminer) # ggsurvplot(), ggforest(), ggcoxzph() — este SÍ hay que instalarlo
survminer no viene pre-instalado. Ejecútalo una vez: install.packages("survminer"). Proporciona visualizaciones basadas en ggplot2 para los objetos del paquete survival.
Surv(): crear el objeto de supervivenciaEl primer paso siempre es crear un objeto Surv, que combina dos piezas de información: el tiempo hasta el evento (o censura) y el indicador de evento (1 = ocurrió, 0 = censurado).
# Preparar los datos
datos_surv <- empleados |>
mutate(
evento = if_else(rotacion == "Sí", 1L, 0L),
tiempo_meses = antiguedad_anios * 12
)
# Crear el objeto Surv
surv_obj <- Surv(time = datos_surv$tiempo_meses,
event = datos_surv$evento)
# Inspeccionar
head(surv_obj, 10)
[1] 6 159.6 57.6 56.4+ 16.8+ 20.4+ 52.8 10.8 180 + 36+
Los números sin signo son tiempos de evento (el empleado se fue a los X meses). Los números con + son tiempos censurados (el empleado seguía activo a los X meses).
Surv() necesita un vector numérico para event: 1 = evento, 0 = censurado. Si le pasas "Sí"/"No", R tirará un error. Por eso creamos evento con if_else() antes.
survfit(): estimar la curva de Kaplan-MeierCon el objeto Surv listo, estimamos la curva de supervivencia con survfit(). La fórmula usa la misma notación que lm() y glm(): el lado izquierdo es el Surv() y el lado derecho dice cómo agrupar.
# Curva global (sin agrupar: ~ 1)
km_global <- survfit(Surv(tiempo_meses, evento) ~ 1, data = datos_surv)
# Ver el resumen
km_global
Call: survfit(formula = Surv(tiempo_meses, evento) ~ 1, data = datos_surv)
n events median 0.95LCL 0.95UCL
[1,] 1200 326 NA NA NAEl output te dice: hay 1.200 observaciones, 326 eventos (salidas), y la mediana de supervivencia es NA. ¿Por qué NA? Porque la curva nunca baja del 50 %: más de la mitad de los empleados sigue activo al cierre del periodo. La mediana solo se puede calcular si la curva cruza el 50 %. Que sea NA es, en sí mismo, un resultado informativo: la mayoría se queda.
Para ver el detalle paso a paso:
summary(km_global)
Esto imprime una tabla con cada "peldaño" de la escalera de Kaplan-Meier: el tiempo, el número en riesgo, los eventos, y la probabilidad acumulada de permanencia S(t).
ggsurvplot(): visualizar la curvaLa función estrella de survminer convierte un objeto survfit en una visualización completa con un solo llamado:
ggsurvplot(
km_global,
data = datos_surv,
conf.int = TRUE, # Banda de confianza al 95 %
risk.table = TRUE, # Tabla de n en riesgo debajo del gráfico
xlab = "Tiempo (meses)",
ylab = "Probabilidad de permanencia",
title = "Curva de supervivencia — InnovaCo",
ggtheme = theme_minimal(),
palette = "#B85042", # Color terracotta
surv.median.line = "hv", # Líneas de mediana (si existe)
break.x.by = 12 # Marcas cada 12 meses
)
ggsurvplot()| Parámetro | Qué hace | Valor típico |
|---|---|---|
conf.int | Muestra la banda de confianza | TRUE para curvas solas; FALSE si hay muchos grupos |
risk.table | Agrega tabla de "número en riesgo" debajo | TRUE — muy útil para reportes |
pval | Muestra el p-valor del log-rank test | TRUE cuando comparas grupos |
surv.median.line | Líneas que marcan la mediana | "hv" = horizontal + vertical |
break.x.by | Intervalo de marcas en el eje X | 12 si mides en meses |
xlim | Límites del eje X | c(0, 180) para 15 años en meses |
palette | Colores (uno por grupo) | Vector de hex o nombre de paleta |
legend.title | Título de la leyenda | "Departamento", "Género", etc. |
Para extraer probabilidades de permanencia a tiempos específicos, usa summary() con el argumento times:
# ¿Qué fracción sigue a los 12, 36 y 60 meses?
summary(km_global, times = c(12, 36, 60))
La columna survival te da S(t): la probabilidad estimada de que un empleado siga en la empresa después de t meses. Con las columnas lower y upper tienes el intervalo de confianza.
Para obtener una curva de Kaplan-Meier por cada nivel de una variable categórica, cambia el lado derecho de la fórmula:
# Curvas por departamento
km_depto <- survfit(Surv(tiempo_meses, evento) ~ departamento,
data = datos_surv)
ggsurvplot(
km_depto,
data = datos_surv,
conf.int = FALSE, # Sin IC para legibilidad con 6 curvas
pval = TRUE, # p-valor del log-rank test
risk.table = TRUE,
xlab = "Tiempo (meses)",
ylab = "Probabilidad de permanencia",
title = "Supervivencia por departamento",
ggtheme = theme_minimal(),
palette = c("#1B2A4A", "#B85042", "#A7BEAE",
"#0D9488", "#E8B96E", "#7B6B8D"),
legend.title = "Departamento"
)
# Medianas por grupo
km_depto
El pval = TRUE muestra automáticamente el p-valor del log-rank test sobre el gráfico. Si es menor que 0,05, hay al menos un departamento con supervivencia significativamente distinta.
survdiff(): el log-rank testEl log-rank test (Mantel, 1966) compara formalmente si dos o más curvas de supervivencia son iguales. Es el análogo de un chi-cuadrado: compara eventos observados vs. esperados en cada grupo en cada tiempo de evento.
# Log-rank test
survdiff(Surv(tiempo_meses, evento) ~ departamento, data = datos_surv)
Call:
survdiff(formula = Surv(tiempo_meses, evento) ~ departamento, data = datos_surv)
N Observed Expected (O-E)^2/E
departamento=Desarrollo de Software 417 93 113.5 3.71
departamento=Finanzas 120 29 32.6 0.40
departamento=Marketing 78 20 21.2 0.07
departamento=Recursos Humanos 133 42 36.2 0.94
departamento=Soporte Técnico 195 68 53.0 4.24
departamento=Ventas 257 74 69.8 0.26
Chisq= 9.6 on 5 degrees of freedom, p= 0.087La columna Expected muestra cuántos eventos esperarías en cada departamento si todos tuvieran la misma curva de supervivencia. La diferencia (O-E)^2/E indica cuánto se desvía cada grupo. Soporte Técnico tiene más eventos de los esperados (68 observados vs. 53 esperados).
survdiff(..., rho = 1). Con rho = 1, los tiempos tempranos (donde hay más gente en riesgo) reciben más peso. Esto no se cubre en clase pero queda como herramienta disponible.
peopleanalyticsdata::job_retentionEl paquete peopleanalyticsdata (Keith McNulty) incluye el dataset job_retention, diseñado para practicar análisis de supervivencia en HR. Tiene variables de supervivencia pre-construidas. Puedes usarlo para practicar de forma independiente:
# install.packages("peopleanalyticsdata") # solo una vez
library(peopleanalyticsdata)
# Cargar el dataset
data(job_retention)
glimpse(job_retention)
# Las variables clave son:
# - month: tiempo en meses
# - event: 1 = se fue, 0 = censurado
# - gender, field, level, sentiment: covariables
# Kaplan-Meier global
km_jr <- survfit(Surv(month, event) ~ 1, data = job_retention)
ggsurvplot(km_jr, data = job_retention, conf.int = TRUE,
xlab = "Meses", ylab = "S(t)",
title = "Supervivencia — job_retention")
# Kaplan-Meier por nivel
km_jr_level <- survfit(Surv(month, event) ~ level, data = job_retention)
ggsurvplot(km_jr_level, data = job_retention, pval = TRUE)
Este dataset es excelente para practicar porque ya viene en formato de supervivencia. Los datos de InnovaCo requieren la transformación previa (construir evento y tiempo_meses) que es, en sí misma, un paso pedagógico valioso.
NA. Tiene información: sabemos que el empleado al menos lleva X meses. Si eliminas las observaciones censuradas con filter(evento == 1), sesgas todo el análisis (solo describes a los que se fueron).
event en Surv()
Si escribes Surv(tiempo_meses) sin el segundo argumento, R asume que todos son eventos (nadie está censurado). El resultado será una curva de Kaplan-Meier que cae mucho más rápido de lo real.