Apunte 18 — Trabajar con fechas en R: el paquete lubridate

Analítica de Personas · Semestre otoño 2026 · Semana 8 · Prof. René Gempp

1. ¿Por qué un apunte completo sobre fechas?

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:

  1. R no sabe, por defecto, que "2025-04-30" es una fecha. La trata como texto. Si intentas restarle otra fecha, te tira un error.
  2. Las fechas vienen escritas en mil formatos: "30/04/2025", "April 30, 2025", "2025-04-30T09:30:00". Cada planilla del SIRH viene distinta.
  3. La aritmética con fechas tiene trampas: "un mes" no es siempre lo mismo que 30 días, y "un año" a veces tiene 365 días y a veces 366.

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.

El nombre del paquete "Lubridate" viene de lubricate (lubricar): el objetivo del paquete es hacer que el trabajo con fechas en R sea suave, sin fricción. El paquete fue desarrollado por Garrett Grolemund y Hadley Wickham (2011), y se carga automáticamente con library(tidyverse) en versiones recientes.

2. El primer paso: parsear strings a fechas

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"
El error clásico: confundir 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)?

2.1 Parseo en pipeline: el patrón que vas a usar siempre

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.

Una pregunta razonable "Pero si 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.

3. Aritmética con fechas: lo que parece obvio y no lo es

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.

4. Extraer componentes: año, mes, día de la semana

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ónDevuelveEjemplo
year(x)Año (entero)2025
month(x)Mes (entero 1-12)4
month(x, label = TRUE)Mes como factor ordenadoabr
day(x)Día del mes30
wday(x, label = TRUE)Día de la semana como factormié
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)
El argumento 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.

4.1 Aplicación al embudo: ¿hay un día de la semana donde se postula más?

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.

5. Construir fechas a partir de partes

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

6. Sumar y restar duraciones: 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
El operador %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.

7. Filtrar por rangos de fecha

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))

8. Cookbook RRHH: las cinco operaciones que vas a repetir

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.

8.1 Antigüedad en años (con decimales)

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.

8.2 Edad al momento de un evento

empleados |>
  mutate(
    edad_al_ingreso = as.numeric(fecha_ingreso - fecha_nacimiento) / 365.25
  )

8.3 Tenure mensual para análisis de turnover

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.

8.4 Cohorte por año-mes de ingreso

empleados |>
  mutate(
    cohorte = format(fecha_ingreso, "%Y-%m")
  ) |>
  count(cohorte)

8.5 Time-to-fill como duración del proceso

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)
  )

9. Lo que dejamos fuera (por ahora)

Este apunte cubre las operaciones más frecuentes con fechas en RRHH, pero lubridate hace mucho más:

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.

Referencias