Analítica de Personas · Semestre otoño 2026 · Semana 8 · Prof. René Gempp
En las clases anteriores trabajamos casi siempre con datos transversales: una fila = una persona, sin tiempo. Pero la pregunta que abre la clase de hoy —«¿cuántos días estamos demorando en llenar una vacante?»— es una pregunta sobre duraciones. Y eso significa restar fechas.
Restar fechas en R parece simple, pero esconde tres problemas que arruinan el análisis si los pasamos por alto:
"2025-04-30" es una fecha. La trata como texto. Si intentas restarle otra fecha, te tira un error."30/04/2025", "April 30, 2025", "2025-04-30T09:30:00". Cada planilla del SIRH viene distinta.El paquete lubridate, parte del tidyverse, resuelve los tres problemas con una sintaxis tan ordenada que terminamos olvidándonos de que las fechas eran complicadas.
library(tidyverse) en versiones recientes.
El nombre de la familia de funciones que vamos a usar más es muy mnemotécnica: ymd(), dmy(), mdy(). La idea es: las letras te dicen el orden en que aparecen año, mes y día en el texto.
library(lubridate)
# Formato ISO (año-mes-día), que es el que viene en innovaco_postulaciones.csv
ymd("2025-04-30")
# [1] "2025-04-30"
# Formato chileno típico (día/mes/año)
dmy("30/04/2025")
# [1] "2025-04-30"
# Formato gringo (mes-día-año)
mdy("April 30, 2025")
# [1] "2025-04-30"
Las funciones aceptan vectores y son tolerantes con separadores: barras, guiones, puntos, espacios. Lo único que les importa es el orden.
# Mismo orden, separadores distintos: todos parsean
dmy(c("30/04/2025", "30-04-2025", "30.04.2025", "30 abr 2025"))
# [1] "2025-04-30" "2025-04-30" "2025-04-30" "2025-04-30"
dmy con mdy
Si un dataset chileno trae fechas como "05/04/2025" y por descuido aplicas mdy(), R te va a dar como resultado el 4 de mayo en lugar del 5 de abril, sin avisarte. Antes de parsear, mira algunas filas y pregúntate: ¿el primer número es el día (típico chileno/europeo) o el mes (típico estadounidense)?
En la práctica, casi nunca parseamos una fecha aislada. Lo que hacemos es leer un archivo y, en el mismo pipeline, transformar las columnas de fecha a tipo Date:
postulaciones <- read_csv("innovaco_postulaciones.csv") |>
mutate(
fecha_postulacion = ymd(fecha_postulacion),
fecha_oferta = ymd(fecha_oferta),
fecha_inicio = ymd(fecha_inicio)
)
Después de este mutate(), las tres columnas dejan de ser strings y pasan a ser objetos de clase Date. Eso desbloquea la aritmética y todos los extractores que vamos a ver a continuación.
read_csv ya reconoce las fechas en formato ISO, ¿no es redundante hacer ymd()?". A veces read_csv las reconoce, sí. Pero pasa muy seguido que un valor anómalo en la columna (un "NA" en texto, una celda vacía, una fecha en otro formato) hace que read_csv caiga al modo seguro y lea toda la columna como character. Aplicar ymd() explícitamente garantiza el resultado.
Una vez que las fechas son del tipo correcto, restarlas es directo, pero hay una sutileza:
fecha1 <- ymd("2025-04-30")
fecha2 <- ymd("2025-06-15")
fecha2 - fecha1
# Time difference of 46 days
El resultado es un objeto de tipo difftime, no un número. Si lo metes a un summarise() y luego intentas hacer aritmética con él, te puedes encontrar con sorpresas. La solución es convertirlo a número con as.numeric():
postulaciones |>
mutate(
dias_proceso = as.numeric(fecha_inicio - fecha_postulacion)
) |>
filter(acepto_oferta == 1) |>
summarise(
ttf_mediana = median(dias_proceso, na.rm = TRUE),
ttf_media = mean(dias_proceso, na.rm = TRUE)
)
ttf_mediana ttf_media 1 63 69.4
La columna dias_proceso que acabamos de crear es la que necesitamos para responder la pregunta de Macarena: en mediana, llenar una vacante en InnovaCo está tomando 63 días.
Cuando queremos agrupar postulaciones por mes, o ver si los lunes son distintos a los viernes, necesitamos extraer componentes de una fecha. Las funciones son intuitivas:
| Función | Devuelve | Ejemplo |
|---|---|---|
year(x) | Año (entero) | 2025 |
month(x) | Mes (entero 1-12) | 4 |
month(x, label = TRUE) | Mes como factor ordenado | abr |
day(x) | Día del mes | 30 |
wday(x, label = TRUE) | Día de la semana como factor | mié |
quarter(x) | Trimestre (1-4) | 2 |
week(x) | Número de semana (1-53) | 18 |
Las versiones con label = TRUE devuelven factores ordenados, lo que es ideal para ggplot. Si tu locale del sistema está en español, los nombres salen en español; si está en inglés, salen en inglés. Para forzar:
wday(ymd("2026-04-29"), label = TRUE, abbr = FALSE)
# [1] miércoles
wday(ymd("2026-04-29"), label = TRUE, abbr = TRUE, week_start = 1)
# [1] mié — semana que empieza en lunes (default tidyverse)
week_start = 1
Por defecto, R considera que la semana empieza el domingo (week_start = 7), siguiendo la convención estadounidense. En Chile y en la mayor parte del mundo la semana empieza en lunes. Acuérdate de poner week_start = 1 para que "lunes" aparezca como primer nivel del factor.
Una pregunta razonable de Macarena podría ser: «¿deberíamos publicar las vacantes los domingos por la noche, para capturar los postulantes del lunes?». La pregunta se vuelve respondible una vez que extraemos el día de la semana:
postulaciones |>
mutate(
dia_semana = wday(fecha_postulacion, label = TRUE,
abbr = TRUE, week_start = 1)
) |>
count(dia_semana) |>
mutate(prop = n / sum(n))
dia_semana n prop 1 lun 478 0.159 2 mar 459 0.153 3 mié 448 0.149 4 jue 434 0.145 5 vie 418 0.139 6 sáb 383 0.128 7 dom 380 0.127
El patrón es claro: lunes y martes concentran ~31% de las postulaciones; sábado y domingo solo el 25%. Es un dato chico pero útil para decidir cuándo publicar.
A veces tenemos el año, el mes y el día en columnas separadas y queremos armar la fecha. La función make_date() sirve para esto:
tibble(
ano = c(2025, 2025, 2026),
mes = c(1, 6, 4),
dia = c(15, 30, 29)
) |>
mutate(fecha = make_date(ano, mes, dia))
ano mes dia fecha 1 2025 1 15 2025-01-15 2 2025 6 30 2025-06-30 3 2026 4 29 2026-04-29
days(), months(), years()Cuando queremos calcular «la fecha 90 días después de la postulación», usamos las funciones de duración:
postulaciones |>
mutate(
fecha_evaluacion_90d = fecha_inicio + days(90),
fecha_evaluacion_12m = fecha_inicio + months(12),
fecha_evaluacion_1a = fecha_inicio + years(1)
)
Hay una sutileza importante con months() y years():
ymd("2025-01-31") + months(1)
# [1] NA ← porque "31 de febrero" no existe
ymd("2025-01-31") %m+% months(1)
# [1] "2025-02-28" ← lubridate ajusta al último día válido
%m+% es tu amigo
Cuando suméis meses o años, prefiere siempre %m+% y %m-% antes que + y -. Estos operadores manejan los casos borde como el 31 de enero + 1 mes, devolviendo el último día válido del mes destino en lugar de NA.
Combinar fechas con filter() es directo, una vez que las fechas están bien tipadas:
# Postulaciones del último trimestre (enero-marzo 2026)
postulaciones |>
filter(fecha_postulacion >= ymd("2026-01-01"),
fecha_postulacion <= ymd("2026-03-31"))
# Postulaciones del año corriente
postulaciones |>
filter(year(fecha_postulacion) == 2026)
# Postulaciones de los últimos 90 días desde una fecha de corte
fecha_corte <- ymd("2026-04-28")
postulaciones |>
filter(fecha_postulacion >= fecha_corte - days(90))
Cierro el apunte con un mini-recetario de las operaciones de fecha que aparecen prácticamente en cualquier análisis de RRHH. Si memorizas estas cinco, el 90% de las preguntas con fechas las resuelves.
empleados |>
mutate(
antiguedad_anios = as.numeric(today() - fecha_ingreso) / 365.25
)
El 365.25 en lugar de 365 es para promediar el efecto de los años bisiestos sobre periodos largos.
empleados |>
mutate(
edad_al_ingreso = as.numeric(fecha_ingreso - fecha_nacimiento) / 365.25
)
empleados |>
mutate(
tenure_meses = interval(fecha_ingreso, fecha_salida) / months(1)
)
La función interval() crea un objeto de intervalo, que dividido por months(1) da el número exacto de meses respetando los días reales de cada mes.
empleados |>
mutate(
cohorte = format(fecha_ingreso, "%Y-%m")
) |>
count(cohorte)
postulaciones |>
filter(acepto_oferta == 1) |>
mutate(
ttf_dias = as.numeric(fecha_inicio - fecha_postulacion)
) |>
group_by(fuente) |>
summarise(
ttf_mediana = median(ttf_dias, na.rm = TRUE),
ttf_p90 = quantile(ttf_dias, 0.90, na.rm = TRUE)
)
Este apunte cubre las operaciones más frecuentes con fechas en RRHH, pero lubridate hace mucho más:
with_tz() y force_tz() — relevante si trabajas con datos de plataformas internacionales (Workday, Greenhouse) que guardan timestamps UTC.floor_date(), round_date() y ceiling_date(), útiles para construir series de tiempo agregadas (semanales, mensuales, trimestrales).Para profundizar, el capítulo Dates and times de Wickham, Çetinkaya-Rundel y Grolemund (2023) en R for Data Science, segunda edición, es la mejor referencia gratuita y está disponible en r4ds.hadley.nz/datetimes.