Esta clase desarrolla las páginas físicas 40–74 de
Unidad_1_Series_de_tiempo.pdf.
Al terminar podrá reconocer los componentes de una serie, justificar un esquema aditivo o multiplicativo, calcular una media móvil y comparar una descomposición clásica con STL sin confundir sus escalas ni sus limitaciones.
Guarde el archivo en UTF-8, ábralo en RStudio y seleccione
Knit → Knit to HTML. Ejecute los bloques en orden. Se
utilizan R base, stats, datasets y
knitr; la compilación necesita rmarkdown y
Pandoc.
AirPassengers contiene 144 totales mensuales de
pasajeros aéreos internacionales, de enero de 1949 a diciembre de 1960,
expresados en miles de pasajeros. Es un conjunto
histórico incluido en R.
y <- datasets::AirPassengers
stopifnot(length(y) == 144, frequency(y) == 12,
!anyNA(y), all(is.finite(y)), all(y > 0))
tabla(data.frame(inicio = "1949-01", final = "1960-12",
observaciones = length(y), frecuencia = frequency(y),
unidad = "Miles de pasajeros"))| inicio | final | observaciones | frecuencia | unidad |
|---|---|---|---|---|
| 1949-01 | 1960-12 | 144 | 12 | Miles de pasajeros |
plot(y, col = colores[1], lwd = 2, xlab = "Año",
ylab = "Miles de pasajeros", main = "AirPassengers · datos históricos")Antes de calcular, responda:
anio <- floor(time(y) + 1e-8)
por_anio <- split(as.numeric(y), anio)
resumen_anual <- do.call(rbind, lapply(names(por_anio), function(a) {
v <- por_anio[[a]]
data.frame(anio = as.integer(a), nivel_medio = mean(v),
amplitud = max(v) - min(v),
amplitud_relativa = (max(v) - min(v)) / mean(v))
}))
tabla(resumen_anual)| anio | nivel_medio | amplitud | amplitud_relativa |
|---|---|---|---|
| 1949 | 126,667 | 44 | 0,347 |
| 1950 | 139,667 | 56 | 0,401 |
| 1951 | 170,167 | 54 | 0,317 |
| 1952 | 197,000 | 71 | 0,360 |
| 1953 | 225,000 | 92 | 0,409 |
| 1954 | 238,917 | 114 | 0,477 |
| 1955 | 284,000 | 131 | 0,461 |
| 1956 | 328,250 | 142 | 0,433 |
| 1957 | 368,417 | 166 | 0,451 |
| 1958 | 381,000 | 195 | 0,512 |
| 1959 | 428,333 | 217 | 0,507 |
| 1960 | 476,167 | 232 | 0,487 |
boxplot(as.numeric(y) ~ factor(cycle(y), levels = 1:12, labels = meses),
xlab = "Mes", ylab = "Miles de pasajeros",
col = "#E8EFEA", main = "Distribución por mes en todos los años")La amplitud relativa facilita la comparación entre niveles distintos. Estos resúmenes son exploratorios: el rango anual también recoge tendencia y ruido. El diagrama por mes mezcla años de niveles distintos y no estima por sí solo un efecto estacional depurado de tendencia.
En la representación conceptual aditiva:
\[y_t=T_t+C_t+S_t+R_t=M_t+S_t+R_t,\qquad M_t=T_t+C_t.\]
| Componente | Qué describe | Qué debe evitarse |
|---|---|---|
| Tendencia | Evolución sostenida del nivel | Suponer que siempre es una recta |
| Ciclo | Oscilaciones sin duración fija de calendario | Equipararlo a una estación del año |
| Estacionalidad | Patrón ligado al calendario | Afirmar que toda oscilación es estacional |
| Irregularidad o resto | Lo que el procedimiento no explica | Dar por demostrada su independencia |
La separación de tendencia y ciclo exige supuestos adicionales.
decompose() y stl() devuelven una componente
suavizada llamada trend; en esta clase se interpreta como
tendencia-ciclo, no como una identificación separada de
ambos.
set.seed(2040)
t <- 1:120
T_real <- 100 + 1.2 * t
# La fase cambia: esta oscilación no sigue un ciclo anual fijo.
C_real <- 12 * sin(2 * pi * (t / 45 + 0.000035 * t^2))
S_real <- 10 * sin(2 * pi * t / 12)
R_real <- rnorm(length(t), sd = 3)
y_sim <- ts(T_real + C_real + S_real + R_real,
start = c(2015, 1), frequency = 12)
componentes_reales <- lapply(list(as.numeric(y_sim), T_real + C_real,
S_real, R_real),
ts, start = c(2015, 1), frequency = 12)
paneles(componentes_reales,
c("Serie simulada", "Tendencia y ciclo conocidos",
"Estacionalidad anual conocida", "Irregularidad simulada"),
rep("Unidades", 4), c(NA, NA, 0, 0))Explique: conocemos los componentes porque los construimos. Con una serie real observamos su suma y debemos estimarlos; diferentes métodos pueden producir descomposiciones distintas del mismo registro.
Compare:
\[\text{Aditivo: } y_t=M_t+S_t+R_t,\qquad \text{Multiplicativo: } y_t=M_tS_tR_t.\]
En el esquema aditivo todos los componentes conservan la unidad de la serie. En el multiplicativo, \(M_t\) conserva esa unidad y \(S_t,R_t\) son factores. Para aplicar logaritmos se requieren valores estrictamente positivos.
t60 <- 1:60
nivel <- 100 + 2 * t60
serie_A <- nivel + 10 * sin(2 * pi * t60 / 12)
serie_B <- nivel * (1 + 0.10 * sin(2 * pi * t60 / 12))
matplot(t60, cbind(serie_A, serie_B), type = "l", lty = c(1, 2), lwd = 2,
col = colores[1:2], xlab = "Mes", ylab = "Unidades",
main = "Ejercicio de las diapositivas · amplitud absoluta y relativa")
legend("topleft", c("A: +/-10 unidades", "B: +/-10 % del nivel"),
col = colores[1:2], lty = c(1, 2), bty = "n")Preguntas: elija un esquema para A y otro para B.
Justifique la elección y explique qué revisaría antes de usar
log(). Un cero no se reemplaza por un número arbitrario
solo para poder transformar la serie.
Desestacionalizar significa retirar \(S_t\):
\[y_t^{SA}=y_t-S_t\quad\text{o}\quad y_t^{SA}=y_t/S_t.\]
M_a <- 200; S_a <- 30; R_a <- -5
observado_a <- M_a + S_a + R_a
ajustado_a <- observado_a - S_a
M_m <- 200; S_m <- 1.20; R_m <- .95
observado_m <- M_m * S_m * R_m
ajustado_m <- observado_m / S_m
tabla(data.frame(esquema = c("Aditivo", "Multiplicativo"),
observado = c(observado_a, observado_m),
desestacionalizado = c(ajustado_a, ajustado_m)))| esquema | observado | desestacionalizado |
|---|---|---|
| Aditivo | 225 | 195 |
| Multiplicativo | 228 | 190 |
stopifnot(observado_a == 225, ajustado_a == 195,
abs(observado_m - 228) < 1e-10, abs(ajustado_m - 190) < 1e-10)El desestacionalizado es 195 o 190, no 200: conserva el resto además de la tendencia-ciclo. Un factor de 1,20 representa una contribución estacional de 20 % sobre la base multiplicativa; el valor observado también depende de \(R_t\).
\[\widehat M_t=\frac{y_{t-1}+y_t+y_{t+1}}{3}.\]
x_corto <- c(10, 13, 16, 13, 10)
mm3 <- stats::filter(x_corto, filter = rep(1/3, 3), sides = 2)
tabla(data.frame(t = 1:5, y = x_corto, media_centrada_3 = as.numeric(mm3)))| t | y | media_centrada_3 |
|---|---|---|
| 1 | 10 | NA |
| 2 | 13 | 13 |
| 3 | 16 | 14 |
| 4 | 13 | 13 |
| 5 | 10 | NA |
En \(t=3\), \((13+16+13)/3=14\). Los extremos son
NA porque no se dispone de vecinos suficientes. No son
ceros ni se completan automáticamente con la media.
suave3 <- stats::filter(y_sim, rep(1/3, 3), sides = 2)
suave13 <- stats::filter(y_sim, rep(1/13, 13), sides = 2)
ts.plot(y_sim, suave3, suave13, col = c("gray70", colores[1:2]),
lty = c(1, 1, 2), xlab = "Año", ylab = "Unidades",
main = "Simulación · suavizar cambia el detalle visible")
legend("topleft", c("Serie", "Ventana 3", "Ventana 13"),
col = c("gray70", colores[1:2]), lty = c(1, 1, 2), bty = "n")Una ventana mayor suaviza más y pierde más observaciones en los bordes. La media uniforme de 13 términos de este ejemplo no es el filtro 2×12 utilizado a continuación.
El promedio de 12 meses se sitúa entre dos instantes. Al promediar dos medias contiguas de 12 términos se obtiene una media centrada 2×12:
\[\widehat M_t=\frac{y_{t-6}}{24} +\frac{1}{12}\sum_{j=-5}^{5}y_{t+j} +\frac{y_{t+6}}{24}.\]
pesos_2x12 <- c(.5, rep(1, 11), .5) / 12
stopifnot(length(pesos_2x12) == 13, abs(sum(pesos_2x12) - 1) < 1e-12)
M_2x12 <- stats::filter(y, filter = pesos_2x12, sides = 2)
pos <- 20L
manual <- mean(c(mean(y[(pos - 6):(pos + 5)]),
mean(y[(pos - 5):(pos + 6)])))
tabla(data.frame(posicion = pos, calculo_manual = manual,
filtro = as.numeric(M_2x12[pos]),
extremos_sin_estimacion = sum(is.na(M_2x12))))| posicion | calculo_manual | filtro | extremos_sin_estimacion |
|---|---|---|---|
| 20 | 143,167 | 143,167 | 12 |
Hay seis valores sin estimar al principio y seis al final. El filtro usa datos posteriores a \(t\): sirve para descripción retrospectiva, pero no como si fuera una estimación disponible en tiempo real.
La normalización identifica una escala de referencia: \(\sum_{j=1}^{s}S_j=0\) en el aditivo y \(s^{-1}\sum_{j=1}^{s}S_j=1\) en el multiplicativo clásico.
efecto <- c(-15, 5, 20, -10)
factor <- c(.85, 1.05, 1.20, .90)
tabla(data.frame(trimestre = paste0("T", 1:4), efecto_aditivo = efecto,
factor_multiplicativo = factor,
contribucion_pct = 100 * (factor - 1)))| trimestre | efecto_aditivo | factor_multiplicativo | contribucion_pct |
|---|---|---|---|
| T1 | -15 | 0,85 | -15 |
| T2 | 5 | 1,05 | 5 |
| T3 | 20 | 1,20 | 20 |
| T4 | -10 | 0,90 | -10 |
stopifnot(sum(efecto) == 0, abs(mean(factor) - 1) < 1e-12)
# Ampliación: normalizar estimaciones preliminares.
preliminar_a <- c(-12, 8, 23, -7)
preliminar_m <- c(.90, 1.10, 1.25, .95)
tabla(data.frame(trimestre = 1:4,
aditivo_normalizado = preliminar_a - mean(preliminar_a),
multiplicativo_normalizado = preliminar_m / mean(preliminar_m)))| trimestre | aditivo_normalizado | multiplicativo_normalizado |
|---|---|---|
| 1 | -15 | 0,857 |
| 2 | 5 | 1,048 |
| 3 | 20 | 1,190 |
| 4 | -10 | 0,905 |
Primero se estima \(M_t\), luego se calcula \(y_t/M_t\), se promedia por mes, se normalizan los factores y se obtiene \(R_t=y_t/(\widehat M_t\widehat S_t)\).
dc <- stats::decompose(y, type = "multiplicative")
y_sa <- y / dc$seasonal
paneles(list(y, dc$trend, dc$seasonal, dc$random),
c("Serie observada", "Tendencia-ciclo clásica",
"Factor estacional", "Resto multiplicativo"),
c("Miles de pasajeros", "Miles de pasajeros", "Factor", "Factor"),
c(NA, NA, 1, 1))cociente <- y / M_2x12
factor_preliminar <- tapply(as.numeric(cociente), cycle(y), mean, na.rm = TRUE)
factor_manual <- factor_preliminar / mean(factor_preliminar)
stopifnot(max(abs(factor_manual - dc$figure)) < 1e-10,
max(abs(M_2x12 - dc$trend), na.rm = TRUE) < 1e-10,
max(abs(y - dc$trend * dc$seasonal * dc$random), na.rm = TRUE) < 1e-10)
tabla(data.frame(mes = meses, factor = as.numeric(dc$figure),
contribucion_estacional_pct = 100 * (as.numeric(dc$figure) - 1)))| mes | factor | contribucion_estacional_pct |
|---|---|---|
| Ene | 0,910 | -8,977 |
| Feb | 0,884 | -11,637 |
| Mar | 1,007 | 0,737 |
| Abr | 0,976 | -2,409 |
| May | 0,981 | -1,862 |
| Jun | 1,113 | 11,278 |
| Jul | 1,227 | 22,656 |
| Ago | 1,220 | 21,991 |
| Sep | 1,060 | 6,049 |
| Oct | 0,922 | -7,824 |
| Nov | 0,801 | -19,882 |
| Dic | 0,899 | -10,118 |
barplot(dc$figure, names.arg = meses, col = colores[1],
ylab = "Factor estacional", xlab = "Mes", ylim = c(0, 1.5),
main = "AirPassengers · patrón mensual clásico")
abline(h = 1, lty = 2)La media de los 12 factores es 1. El patrón se repite cada año por construcción. Un factor superior a 1 es una contribución estacional positiva, pero no obliga a que cada observación supere \(M_t\), porque también interviene el resto.
ts.plot(y, y_sa, col = colores[1:2], lty = c(1, 2), lwd = 2,
xlab = "Año", ylab = "Miles de pasajeros",
main = "Desestacionalizar conserva la tendencia")
legend("topleft", c("Observada", "Desestacionalizada"),
col = colores[1:2], lty = c(1, 2), bty = "n")Lectura: random es el nombre del
componente en R; no prueba aleatoriedad independiente. El resto
multiplicativo se interpreta alrededor de 1. Los NA de sus
extremos provienen de la tendencia-ciclo no estimada.
\[\log(y_t)=\log(M_t)+\log(S_t)+\log(R_t).\]
z <- log(y)
ajuste <- stats::stl(z, s.window = "periodic", robust = TRUE)
componentes <- ajuste$time.series
paneles(list(z, componentes[, "trend"], componentes[, "seasonal"],
componentes[, "remainder"]),
c("Logaritmo de AirPassengers", "Tendencia-ciclo STL",
"Estacionalidad STL", "Resto STL"),
rep("Escala log", 4), c(NA, NA, 0, 0))| seasonal | trend | remainder |
|---|---|---|
| -0,083882194 | 4,803064 | -0,0006826642 |
| -0,142365972 | 4,806942 | 0,1061089448 |
| 0,025284297 | 4,810820 | 0,0466980517 |
| -0,012521162 | 4,815287 | 0,0570465061 |
| -0,007673679 | 4,819755 | -0,0162903219 |
| 0,114808208 | 4,824574 | -0,0341070611 |
Aquí s.window = "periodic" impone una estacionalidad
fija. Aunque STL puede permitir que cambie, esta configuración no lo
hace. robust = TRUE reduce la influencia de observaciones
atípicas durante el ajuste; no las elimina del registro.
factor_stl <- exp(componentes[, "seasonal"])
y_sa_stl <- exp(z - componentes[, "seasonal"])
tabla(data.frame(fecha = seq(as.Date("1949-01-01"), by = "month", length.out = 6),
observado = as.numeric(y[1:6]),
factor_stl = as.numeric(factor_stl[1:6]),
ajustado_stl = as.numeric(y_sa_stl[1:6])))| fecha | observado | factor_stl | ajustado_stl |
|---|---|---|---|
| 1949-01-01 | 112 | 0,920 | 121,800 |
| 1949-02-01 | 118 | 0,867 | 136,054 |
| 1949-03-01 | 132 | 1,026 | 128,704 |
| 1949-04-01 | 129 | 0,988 | 130,625 |
| 1949-05-01 | 121 | 0,992 | 121,932 |
| 1949-06-01 | 135 | 1,122 | 120,358 |
Al exponenciar, los efectos logarítmicos se vuelven factores. Su
normalización no debe confundirse con la media aritmética igual a 1 de
decompose(). Esta es una identidad de descomposición
observada, no un pronóstico de la media: exponenciar un pronóstico
logarítmico requeriría considerar el sesgo de la transformación
inversa.
set.seed(2067)
tt <- 1:180
base_log <- 4.5 + .003 * tt
estacional_log <- (.06 + .0005 * tt) * sin(2 * pi * tt / 12)
log_sim <- base_log + estacional_log + rnorm(180, sd = .015)
log_sim[95] <- log_sim[95] + .65
log_sim <- ts(log_sim, start = c(2000, 1), frequency = 12)
stl_fijo <- stl(log_sim, s.window = "periodic", robust = TRUE)
stl_flexible <- stl(log_sim, s.window = 7, robust = TRUE)
stl_no_robusto <- stl(log_sim, s.window = 7, robust = FALSE)
paneles(list(stl_fijo$time.series[, "seasonal"],
stl_flexible$time.series[, "seasonal"],
stl_flexible$time.series[, "remainder"]),
c("Patrón fijo estimado", "Patrón variable estimado",
"El atípico permanece visible en el resto robusto"),
rep("Escala log", 3), c(0, 0, 0))tabla(data.frame(metodo = c("Flexible robusto", "Flexible no robusto"),
tendencia_en_95 = c(stl_flexible$time.series[95, "trend"],
stl_no_robusto$time.series[95, "trend"]),
tendencia_conocida = base_log[95]))| metodo | tendencia_en_95 | tendencia_conocida |
|---|---|---|
| Flexible robusto | 4,782 | 4,785 |
| Flexible no robusto | 4,836 | 4,785 |
La ventana estacional numérica trabaja sobre subseries de la misma
estación; 7 no significa un promedio móvil simple de siete
meses consecutivos. Compare la estimación con la estructura conocida,
sin concluir que una opción será siempre mejor para cualquier serie.
da <- decompose(y, type = "additive")
dm <- decompose(y, type = "multiplicative")
paneles(list(da$random, dm$random), c("Resto aditivo", "Resto multiplicativo"),
c("Miles de pasajeros", "Factor"), c(0, 1))Pregunta ¿son comparables las escalas de los dos restos?
No se debe elegir el método con la varianza numéricamente menor: uno está en miles de pasajeros y el otro es un factor sin unidades.
base_aditiva <- da$trend + da$seasonal
rel_a <- da$random / base_aditiva
rel_m <- dm$random - 1
rel_stl <- exp(componentes[, "remainder"]) - 1
comunes <- is.finite(rel_a) & is.finite(rel_m) & is.finite(rel_stl)
stopifnot(all(base_aditiva[comunes] > 0))
tabla(data.frame(metodo = c("Clásico aditivo", "Clásico multiplicativo", "STL log"),
n_comun = sum(comunes),
mediana_abs_resto_relativo =
c(median(abs(rel_a[comunes])), median(abs(rel_m[comunes])),
median(abs(rel_stl[comunes])))))| metodo | n_comun | mediana_abs_resto_relativo |
|---|---|---|
| Clásico aditivo | 132 | 0,042 |
| Clásico multiplicativo | 132 | 0,017 |
| STL log | 132 | 0,016 |
Ahora se describen desviaciones relativas respecto de bases estimadas por cada método, en las mismas fechas. La tabla no mide precisión predictiva ni prueba independencia: la flexibilidad de cada ajuste también afecta su resto.
En el periodo 20 todavía no conocemos el 21.
historia <- as.numeric(y[1:36])
historia_alterada <- historia
historia_alterada[21] <- historia_alterada[21] + 90
filtrar <- function(v, lados) as.numeric(stats::filter(v, rep(1/3, 3), sides = lados))
comparacion_filtro <- data.frame(
filtro = c("Centrado", "Solo presente y pasado"),
valor_t20 = c(filtrar(historia, 2)[20], filtrar(historia, 1)[20]),
t20_tras_cambiar_t21 = c(filtrar(historia_alterada, 2)[20],
filtrar(historia_alterada, 1)[20]))
tabla(comparacion_filtro)| filtro | valor_t20 | t20_tras_cambiar_t21 |
|---|---|---|
| Centrado | 166 | 196 |
| Solo presente y pasado | 163 | 163 |
stopifnot(abs(diff(as.numeric(comparacion_filtro[2, 2:3]))) < 1e-10,
abs(comparacion_filtro[1, 3] - comparacion_filtro[1, 2] - 30) < 1e-10)Cambiar un dato futuro altera el suavizado centrado del pasado. Para evaluar predicciones hay que ajustar cada transformación usando solo la muestra de entrenamiento disponible en ese origen. El filtro unilateral sí utiliza información disponible, pero tiene propiedades y retraso diferentes.
y_practica <- datasets::AirPassengers
practica_a <- decompose(y_practica, type = "additive")
practica_m <- decompose(y_practica, type = "multiplicative")
tabla(data.frame(mes = meses, efecto_aditivo = practica_a$figure,
factor_multiplicativo = practica_m$figure))| mes | efecto_aditivo | factor_multiplicativo |
|---|---|---|
| Ene | -24,749 | 0,910 |
| Feb | -36,188 | 0,884 |
| Mar | -2,241 | 1,007 |
| Abr | -8,037 | 0,976 |
| May | -4,506 | 0,981 |
| Jun | 35,403 | 1,113 |
| Jul | 63,831 | 1,227 |
| Ago | 62,823 | 1,220 |
| Sep | 16,520 | 1,060 |
| Oct | -20,643 | 0,922 |
| Nov | -53,593 | 0,801 |
| Dic | -28,620 | 0,899 |
practica_stl <- stl(log(y_practica), s.window = "periodic", robust = TRUE)
sa_clasica <- y_practica / practica_m$seasonal
sa_stl <- exp(log(y_practica) - practica_stl$time.series[, "seasonal"])
ts.plot(sa_clasica, sa_stl, col = colores[1:2], lty = c(1, 2),
xlab = "Año", ylab = "Miles de pasajeros",
main = "Práctica · series desestacionalizadas")
legend("topleft", c("Clásica multiplicativa", "STL log"),
col = colores[1:2], lty = c(1, 2), bty = "n")Responda las preguntas de la página 68: ¿el resto conserva oscilaciones?, ¿parece estable el patrón estacional?, ¿los extremos tienen menos información?, ¿cambian las conclusiones al transformar?
Modificación adicional: repita STL con
s.window = 7. Explique qué supuesto cambió y por qué no
debe decidir únicamente por el tamaño del resto.
set.seed(2074)
tr <- 1:40
ventas_tr <- ts(200 + 3 * tr + rep(c(-15, 5, 20, -10), 10) +
rnorm(40, sd = 2), start = c(2016, 1), frequency = 4)
ajuste_tr <- decompose(ventas_tr, type = "additive")
tabla(data.frame(trimestre = paste0("T", 1:4),
efecto_real = c(-15, 5, 20, -10),
efecto_estimado = ajuste_tr$figure))| trimestre | efecto_real | efecto_estimado |
|---|---|---|
| T1 | -15 | -16,017 |
| T2 | 5 | 6,143 |
| T3 | 20 | 19,734 |
| T4 | -10 | -9,860 |
Explique por qué el efecto estimado puede diferir del utilizado en la simulación. Cambie la desviación del ruido de 2 a 8 manteniendo la semilla; compare las estimaciones. Conservar el mismo mecanismo permite estudiar el efecto del ruido.
Documentación para consultar métodos y datos:
#> R version 4.5.1 (2025-06-13 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26200)
#>
#> Matrix products: default
#> LAPACK version 3.12.1
#>
#> locale:
#> [1] LC_COLLATE=Spanish_Ecuador.utf8 LC_CTYPE=Spanish_Ecuador.utf8
#> [3] LC_MONETARY=Spanish_Ecuador.utf8 LC_NUMERIC=C
#> [5] LC_TIME=Spanish_Ecuador.utf8
#>
#> time zone: America/Guayaquil
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> loaded via a namespace (and not attached):
#> [1] digest_0.6.39 R6_2.6.1 fastmap_1.2.0 xfun_0.58 cachem_1.1.0
#> [6] knitr_1.51 htmltools_0.5.9 rmarkdown_2.31 lifecycle_1.0.5 cli_3.6.6
#> [11] sass_0.4.10 jquerylib_0.1.4 compiler_4.5.1 rstudioapi_0.18.0 tools_4.5.1
#> [16] evaluate_1.0.5 bslib_0.11.0 yaml_2.3.12 otel_0.2.0 jsonlite_2.0.0
#> [21] rlang_1.2.0