Para este trabajo se selecciona el sector de la construcción en Colombia y se toma a Cementos Argos como empresa de referencia. La elección es coherente con la base suministrada por la asignatura, que contiene indicadores mensuales nacionales directamente vinculados con la actividad cementera y de construcción. De acuerdo con la información corporativa de Argos, la compañía produce y comercializa cemento y concreto y se presenta como líder del mercado colombiano. Fuente corporativa: https://colombia.argos.co/acerca-de-argos/
Aclaración metodológica: las variables utilizadas en este trabajo son indicadores sectoriales nacionales; no corresponden a datos internos de Cementos Argos. Por tanto, la empresa se utiliza como referencia para interpretar cómo un tomador de decisiones del sector podría aprovechar estas señales externas.
La pregunta empresarial que guía el análisis es:
¿Qué está ocurriendo con la actividad del sector construcción y cementero en Colombia, qué señales aportan las licencias, los despachos y la producción de cemento, y cómo podría utilizar Cementos Argos esta información para anticipar decisiones de producción, inventarios, logística y planeación comercial?
Según el catastro suministrado por la asignatura, se seleccionan las siguientes variables:
| Variable | Acrónimo | Unidad | Geografía | Fuente | Papel dentro del análisis |
|---|---|---|---|---|---|
| Licencias de construcción | LICC | Área aprobada | Colombia | DANE | Señal de proyectos formalmente autorizados y actividad potencial de edificación |
| Despachos de cemento | DECEM | Toneladas | Colombia | DANE | Aproximación a la demanda efectiva de cemento |
| Producción de cemento | PNCEM | Toneladas | Colombia | DANE | Respuesta productiva/oferta del sector |
La relación esperada es que una evolución favorable de la actividad constructora pueda reflejarse en mayores despachos y, de manera simultánea o posterior, en una respuesta de la producción. Sin embargo, no se asumirá que LICC es automáticamente un indicador adelantado de DECEM ni se afirmará causalidad sin evidencia. El objetivo es identificar patrones, señales, divergencias y coherencia económica entre los indicadores.
La decisión empresarial que se busca apoyar es la planeación de corto plazo: ajustar producción, inventarios, distribución y capacidad logística de acuerdo con la evolución de la demanda observada y las señales del sector.
library(readxl)
library(tseries)
library(forecast)
library(ggplot2)
library(plotly)
library(dplyr)
library(lmtest)
library(knitr)
# El código acepta cualquiera de los dos nombres usados durante el trabajo.
archivo_base <- if (file.exists("Base Caso1.xlsx")) {
"Base Caso1.xlsx"
} else {
"Base Caso1(1).xlsx"
}
base <- read_excel(archivo_base, sheet = "Caso2")
# Verificación básica
str(base)
## tibble [168 × 62] (S3: tbl_df/tbl/data.frame)
## $ FECHA : POSIXct[1:168], format: "2012-01-01" "2012-02-01" ...
## $ PNCEM : num [1:168] 868474 865408 998847 852138 919675 ...
## $ DECEM : num [1:168] 823284 846615 950453 789542 904691 ...
## $ CONCRETO : num [1:168] 526136 584897 634905 550290 639649 ...
## $ LICC : num [1:168] 1498909 1728147 1425267 1388134 1960736 ...
## $ POLLO : num [1:168] 91722 94142 88748 92013 93279 ...
## $ HUEVO : num [1:168] 52929 52870 52979 52633 52466 ...
## $ PNCAFE : num [1:168] 535 571 576 580 689 714 668 565 519 653 ...
## $ PICAFE : num [1:168] 874863 826220 727565 703033 670335 ...
## $ PECAFE : num [1:168] 256 246 226 215 210 ...
## $ XCAF : num [1:168] 197296 186693 203636 121442 167644 ...
## $ M : num [1:168] 4187750 4291881 4632763 4100547 5088029 ...
## $ X : num [1:168] 4785773 4999318 5712355 5010929 5403375 ...
## $ TRM : num [1:168] 1848 1787 1768 1774 1796 ...
## $ M_CEREAL : num [1:168] 130489 170760 150306 112970 171029 ...
## $ M_CERAMICO : num [1:168] 22663 22218 17682 19527 21924 ...
## $ M_FARM : num [1:168] 134163 144190 158930 151310 182270 ...
## $ X_COMB : num [1:168] 3313842 3319381 3870611 3522192 3529123 ...
## $ X_AZU : num [1:168] 63274 63131 78704 59114 53263 ...
## $ X_PREALIM : num [1:168] 28018 30393 29172 31688 28145 ...
## $ X_FARM : num [1:168] 30866 31202 35074 31236 47115 ...
## $ X_QUIM : num [1:168] 25716 27502 30545 31428 28830 ...
## $ X_PAPEL : num [1:168] 38768 32520 37830 31124 32192 ...
## $ X_CERAMICO : num [1:168] 11392 15168 14770 14160 14491 ...
## $ IPIR : num [1:168] 84.2 88 94.1 82.6 94.3 ...
## $ IPIR_PAPEL : num [1:168] NA NA NA NA NA NA NA NA NA NA ...
## $ IPIR_FARM : num [1:168] NA NA NA NA NA NA NA NA NA NA ...
## $ IPIR_PREALIM : num [1:168] NA NA NA NA NA NA NA NA NA NA ...
## $ MIN : num [1:168] 69 67.8 73.3 67 71 ...
## $ ICC : num [1:168] 32.7 26.8 24.4 26.6 26.5 20.6 23.2 18.1 25 25.6 ...
## $ VEH : num [1:168] NA NA NA NA NA NA NA NA NA NA ...
## $ CART : num [1:168] 2.80e+14 2.81e+14 2.84e+14 2.87e+14 2.91e+14 ...
## $ DAH : num [1:168] 1.26e+14 1.30e+14 1.28e+14 1.25e+14 1.27e+14 ...
## $ ENER : num [1:168] 1595 1545 1719 1575 1687 ...
## $ BRENT : num [1:168] 111 120 125 120 111 ...
## $ IPC : num [1:168] 76.8 77.2 77.3 77.4 77.7 ...
## $ TO : num [1:168] 59.7 60.5 61.5 61.2 61.5 ...
## $ TD : num [1:168] 12.8 12.1 10.5 11.1 11 ...
## $ ISE : num [1:168] 81.7 84.5 87.8 84.1 87.9 ...
## $ CAN : num [1:168] 1685584 1973654 2083470 1406868 1233631 ...
## $ AZUCAR : num [1:168] 148483 192196 202408 134532 107659 ...
## $ CEM_V : num [1:168] 61149 66969 72052 63652 71515 ...
## $ COR_V : num [1:168] 110 113 121 112 127 ...
## $ M_V : num [1:168] 3.55e+08 5.56e+08 4.09e+08 3.45e+08 4.12e+08 ...
## $ X_V : num [1:168] 1.60e+08 1.80e+08 1.91e+08 1.61e+08 1.99e+08 ...
## $ IPIR_V : num [1:168] 80.7 89.7 97.1 82.7 90.8 ...
## $ MIN_V : num [1:168] NA NA NA NA NA NA NA NA NA NA ...
## $ ICC_V : num [1:168] 20.6 16.2 21.4 35.5 28 ...
## $ VEH_V : num [1:168] 2379 2654 3155 2319 2507 ...
## $ PEAJE_V : num [1:168] 220673 215773 228878 196841 226518 ...
## $ ENER_V : num [1:168] 200 201 221 203 221 ...
## $ CART_V : num [1:168] 2.60e+13 2.61e+13 2.65e+13 2.67e+13 2.70e+13 ...
## $ POLLO_V : num [1:168] 13518 13580 13502 14575 14565 ...
## $ ENER_CALI : num [1:168] 69379734 68346783 69585382 67028317 68422194 ...
## $ LICC_CALI : num [1:168] 71805 60566 97722 95605 54599 ...
## $ VEH_CALI : num [1:168] NA NA NA NA NA NA NA NA NA NA ...
## $ X_CALI : num [1:168] 2.78e+09 3.33e+09 3.67e+09 3.10e+09 2.93e+09 ...
## $ OCUP_HOTEL_CALI: num [1:168] 41 48.2 52.8 46.2 51.7 ...
## $ ICC_CALI : num [1:168] 20.6 16.2 21.4 35.5 28 ...
## $ PEAJE_CALI : num [1:168] 27041 28333 29239 23982 27637 ...
## $ DAH_CALI : num [1:168] 4.07e+12 4.09e+12 3.94e+12 3.86e+12 3.99e+12 ...
## $ IPIR_CALI : num [1:168] 96.5 101.9 107.4 98.5 104.5 ...
summary(base[, c("LICC", "DECEM", "PNCEM")])
## LICC DECEM PNCEM
## Min. : 372737 Min. : 242414 Min. : 198925
## 1st Qu.:1744795 1st Qu.: 955555 1st Qu.:1007051
## Median :1964379 Median :1030239 Median :1081425
## Mean :2045773 Mean :1015369 Mean :1071410
## 3rd Qu.:2237034 3rd Qu.:1090649 3rd Qu.:1159832
## Max. :4893234 Max. :1257125 Max. :1341585
colSums(is.na(base[, c("FECHA", "LICC", "DECEM", "PNCEM")]))
## FECHA LICC DECEM PNCEM
## 0 0 0 0
La base contiene observaciones mensuales desde enero de 2012 hasta diciembre de 2025. Antes de modelar se verifica que las tres variables seleccionadas estén disponibles en todo el periodo y que la fecha esté correctamente ordenada.
# Declarar las variables como series mensuales
licc_ts <- ts(base$LICC, start = c(2012, 1), frequency = 12)
decem_ts <- ts(base$DECEM, start = c(2012, 1), frequency = 12)
pncem_ts <- ts(base$PNCEM, start = c(2012, 1), frequency = 12)
fechas <- seq.Date(from = as.Date("2012-01-01"), by = "month", length.out = length(decem_ts))
Siguiendo el procedimiento trabajado en clase, se utiliza STL decomposition para separar la serie original en tendencia, componente estacional y componente irregular. Se prioriza la interpretación de la tasa de crecimiento interanual (YoY) de la tendencia.
analizar_senal <- function(serie, nombre, unidad) {
descomp <- stl(serie, s.window = "periodic")
tendencia <- descomp$time.series[, "trend"]
estacional <- descomp$time.series[, "seasonal"]
irregular <- descomp$time.series[, "remainder"]
yoy_original <- (serie[13:length(serie)] / serie[1:(length(serie)-12)] - 1) * 100
yoy_tendencia <- (tendencia[13:length(tendencia)] / tendencia[1:(length(tendencia)-12)] - 1) * 100
fechas_yoy <- fechas[13:length(fechas)]
list(
nombre = nombre,
unidad = unidad,
descomp = descomp,
tendencia = tendencia,
estacional = estacional,
irregular = irregular,
yoy_original = yoy_original,
yoy_tendencia = yoy_tendencia,
fechas_yoy = fechas_yoy
)
}
licc_a <- analizar_senal(licc_ts, "Licencias de construcción", "Área aprobada")
decem_a <- analizar_senal(decem_ts, "Despachos de cemento", "Toneladas")
pncem_a <- analizar_senal(pncem_ts, "Producción de cemento", "Toneladas")
graficar_tendencia <- function(serie, analisis) {
df <- data.frame(
Fecha = fechas,
Original = as.numeric(serie),
Tendencia = as.numeric(analisis$tendencia)
)
p <- ggplot(df, aes(x = Fecha)) +
geom_line(aes(y = Original, color = "Serie original"), linewidth = 0.55) +
geom_line(aes(y = Tendencia, color = "Tendencia"), linewidth = 0.9) +
labs(title = paste0(analisis$nombre, ": serie original vs tendencia"),
x = "Tiempo", y = analisis$unidad, color = "") +
theme_minimal()
ggplotly(p)
}
graficar_tendencia(licc_ts, licc_a)
graficar_tendencia(decem_ts, decem_a)
graficar_tendencia(pncem_ts, pncem_a)
graficar_yoy <- function(analisis) {
df <- data.frame(
Fecha = analisis$fechas_yoy,
Serie_original = as.numeric(analisis$yoy_original),
Tendencia = as.numeric(analisis$yoy_tendencia)
)
p <- ggplot(df, aes(x = Fecha)) +
geom_hline(yintercept = 0, linetype = "dotted") +
geom_line(aes(y = Serie_original, color = "Serie original"), linewidth = 0.5) +
geom_line(aes(y = Tendencia, color = "Tendencia"), linewidth = 0.9) +
labs(title = paste0(analisis$nombre, ": crecimiento interanual (%)"),
x = "Tiempo", y = "% YoY", color = "") +
theme_minimal()
ggplotly(p)
}
graficar_yoy(licc_a)
graficar_yoy(decem_a)
graficar_yoy(pncem_a)
resumen_estacional <- function(analisis) {
meses <- rep(1:12, length.out = length(analisis$estacional))
nombres_meses <- c("Ene", "Feb", "Mar", "Abr", "May", "Jun",
"Jul", "Ago", "Sep", "Oct", "Nov", "Dic")
data.frame(
Mes_num = 1:12,
Mes = nombres_meses,
Efecto_estacional = as.numeric(tapply(analisis$estacional, meses, mean))
) %>% arrange(desc(Efecto_estacional))
}
est_licc <- resumen_estacional(licc_a)
est_decem <- resumen_estacional(decem_a)
est_pncem <- resumen_estacional(pncem_a)
kable(est_licc, digits = 2, caption = "Patrón estacional - Licencias")
| Mes_num | Mes | Efecto_estacional |
|---|---|---|
| 12 | Dic | 916347.16 |
| 9 | Sep | 87678.76 |
| 7 | Jul | 56886.12 |
| 8 | Ago | 6389.56 |
| 11 | Nov | -20885.02 |
| 5 | May | -65687.63 |
| 2 | Feb | -85842.18 |
| 10 | Oct | -95641.60 |
| 6 | Jun | -96856.37 |
| 4 | Abr | -199090.82 |
| 3 | Mar | -225019.09 |
| 1 | Ene | -278278.75 |
kable(est_decem, digits = 2, caption = "Patrón estacional - Despachos")
| Mes_num | Mes | Efecto_estacional |
|---|---|---|
| 10 | Oct | 70300.25 |
| 9 | Sep | 56103.74 |
| 7 | Jul | 42294.00 |
| 8 | Ago | 41646.62 |
| 11 | Nov | 39377.55 |
| 3 | Mar | 14527.84 |
| 12 | Dic | 1240.61 |
| 2 | Feb | -20096.80 |
| 5 | May | -27405.62 |
| 6 | Jun | -40904.94 |
| 4 | Abr | -75930.92 |
| 1 | Ene | -101152.32 |
kable(est_pncem, digits = 2, caption = "Patrón estacional - Producción")
| Mes_num | Mes | Efecto_estacional |
|---|---|---|
| 12 | Dic | 68629.10 |
| 10 | Oct | 66125.99 |
| 8 | Ago | 45610.96 |
| 9 | Sep | 40113.25 |
| 3 | Mar | 33397.05 |
| 7 | Jul | 27136.87 |
| 11 | Nov | 21840.48 |
| 5 | May | -18126.60 |
| 6 | Jun | -41738.36 |
| 2 | Feb | -43486.31 |
| 4 | Abr | -81422.90 |
| 1 | Ene | -118079.53 |
La tabla permite identificar los meses en los que el componente estacional aporta valores particularmente altos o bajos. La interpretación empresarial debe diferenciar estos movimientos repetitivos de los cambios de tendencia de largo plazo.
choques_irregulares <- function(analisis, n = 8) {
df <- data.frame(
Fecha = fechas,
Irregular = as.numeric(analisis$irregular),
Magnitud = abs(as.numeric(analisis$irregular))
)
df %>% arrange(desc(Magnitud)) %>% head(n)
}
kable(choques_irregulares(licc_a), digits = 2, caption = "Mayores movimientos irregulares - Licencias")
| Fecha | Irregular | Magnitud |
|---|---|---|
| 2019-12-01 | 1728837.4 | 1728837.4 |
| 2015-12-01 | 1585325.8 | 1585325.8 |
| 2023-12-01 | 1389799.6 | 1389799.6 |
| 2022-07-01 | 1157979.7 | 1157979.7 |
| 2020-04-01 | -1138227.4 | 1138227.4 |
| 2022-08-01 | 975170.6 | 975170.6 |
| 2013-12-01 | -934636.2 | 934636.2 |
| 2018-12-01 | -917806.7 | 917806.7 |
kable(choques_irregulares(decem_a), digits = 2, caption = "Mayores movimientos irregulares - Despachos")
| Fecha | Irregular | Magnitud |
|---|---|---|
| 2020-04-01 | -596843.2 | 596843.2 |
| 2021-05-01 | -234817.2 | 234817.2 |
| 2020-03-01 | -189293.0 | 189293.0 |
| 2020-05-01 | -182594.5 | 182594.5 |
| 2020-01-01 | 164036.4 | 164036.4 |
| 2024-04-01 | 130204.7 | 130204.7 |
| 2016-07-01 | -124212.3 | 124212.3 |
| 2019-12-01 | 122993.8 | 122993.8 |
kable(choques_irregulares(pncem_a), digits = 2, caption = "Mayores movimientos irregulares - Producción")
| Fecha | Irregular | Magnitud |
|---|---|---|
| 2020-04-01 | -681834.8 | 681834.8 |
| 2021-05-01 | -242105.2 | 242105.2 |
| 2020-01-01 | 185945.9 | 185945.9 |
| 2020-05-01 | -166741.6 | 166741.6 |
| 2016-07-01 | -152354.4 | 152354.4 |
| 2020-03-01 | -143999.9 | 143999.9 |
| 2016-04-01 | 129615.4 | 129615.4 |
| 2019-12-01 | 122412.2 | 122412.2 |
Importante: los choques detectados estadísticamente indican meses atípicos, pero su causa económica debe verificarse con fuentes externas antes de atribuirla a un evento específico.
Un mismo dato puede contar historias distintas dependiendo de si se observa el nivel del mes, la variación frente al mes inmediatamente anterior, la variación interanual o la tendencia extraída. Esta comparación es útil para evitar interpretar un movimiento estacional como si fuera un cambio estructural.
comparar_ultimo_mes <- function(serie, analisis, nombre) {
n <- length(serie)
data.frame(
Variable = nombre,
Ultimo_mes = as.numeric(serie[n]),
Variacion_mensual_pct = (as.numeric(serie[n]) / as.numeric(serie[n-1]) - 1) * 100,
Variacion_interanual_pct = (as.numeric(serie[n]) / as.numeric(serie[n-12]) - 1) * 100,
YoY_tendencia_pct = tail(as.numeric(analisis$yoy_tendencia), 1)
)
}
contraste_dic2025 <- bind_rows(
comparar_ultimo_mes(licc_ts, licc_a, "LICC"),
comparar_ultimo_mes(decem_ts, decem_a, "DECEM"),
comparar_ultimo_mes(pncem_ts, pncem_a, "PNCEM")
)
kable(contraste_dic2025, digits = 2,
caption = "Diciembre 2025: variación mensual, interanual y crecimiento YoY de la tendencia")
| Variable | Ultimo_mes | Variacion_mensual_pct | Variacion_interanual_pct | YoY_tendencia_pct |
|---|---|---|---|---|
| LICC | 1937568 | 36.59 | -24.70 | -18.40 |
| DECEM | 1071996 | -3.61 | 5.57 | 8.66 |
| PNCEM | 1200230 | -2.14 | 3.80 | 6.93 |
Qué buscamos aquí: si una variable aumenta frente a
noviembre, pero cae frente a diciembre del año anterior y su tendencia
interanual sigue negativa, el aumento mensual no debe presentarse como
una recuperación estructural. Esta distinción será especialmente
importante en LICC.
resumen_2025 <- data.frame(
Mes = tail(licc_a$fechas_yoy, 12),
LICC = tail(as.numeric(licc_a$yoy_tendencia), 12),
DECEM = tail(as.numeric(decem_a$yoy_tendencia), 12),
PNCEM = tail(as.numeric(pncem_a$yoy_tendencia), 12)
)
kable(resumen_2025, digits = 2,
caption = "Evolución mensual del crecimiento interanual de la tendencia durante 2025")
| Mes | LICC | DECEM | PNCEM |
|---|---|---|---|
| 2025-01-01 | -7.06 | -1.62 | -2.16 |
| 2025-02-01 | -4.16 | -0.42 | -1.13 |
| 2025-03-01 | -1.11 | 0.79 | -0.09 |
| 2025-04-01 | 0.39 | 2.10 | 1.00 |
| 2025-05-01 | 1.98 | 3.43 | 2.11 |
| 2025-06-01 | 1.59 | 4.62 | 3.13 |
| 2025-07-01 | 1.18 | 5.82 | 4.15 |
| 2025-08-01 | -2.38 | 6.75 | 5.03 |
| 2025-09-01 | -5.87 | 7.67 | 5.92 |
| 2025-10-01 | -10.14 | 8.16 | 6.41 |
| 2025-11-01 | -14.27 | 8.66 | 6.90 |
| 2025-12-01 | -18.40 | 8.66 | 6.93 |
# Identificar el primer mes de 2025 en el que cada señal cementera pasa a crecimiento positivo
primer_positivo <- function(fechas, valores) {
idx <- which(format(fechas, "%Y") == "2025" & valores > 0)
if (length(idx) == 0) return(NA)
format(fechas[min(idx)], "%Y-%m")
}
puntos_giro <- data.frame(
Variable = c("DECEM", "PNCEM"),
Primer_mes_tendencia_YoY_positiva_2025 = c(
primer_positivo(decem_a$fechas_yoy, as.numeric(decem_a$yoy_tendencia)),
primer_positivo(pncem_a$fechas_yoy, as.numeric(pncem_a$yoy_tendencia))
)
)
kable(puntos_giro, caption = "Puntos de giro hacia crecimiento positivo en 2025")
| Variable | Primer_mes_tendencia_YoY_positiva_2025 |
|---|---|
| DECEM | 2025-03 |
| PNCEM | 2025-04 |
Este análisis permite detectar aceleraciones y desaceleraciones, no únicamente si una variable es positiva o negativa al cierre del período.
Para integrar las variables se compara el comportamiento reciente de las tasas interanuales de sus tendencias y se calcula una correlación descriptiva. La correlación se utiliza únicamente como medida de asociación; no implica causalidad.
# Alinear las tres tasas YoY de tendencia
integrado <- data.frame(
Fecha = licc_a$fechas_yoy,
LICC = as.numeric(licc_a$yoy_tendencia),
DECEM = as.numeric(decem_a$yoy_tendencia),
PNCEM = as.numeric(pncem_a$yoy_tendencia)
)
# Correlaciones descriptivas
cor(integrado[, c("LICC", "DECEM", "PNCEM")], use = "complete.obs")
## LICC DECEM PNCEM
## LICC 1.0000000 0.6841062 0.6918289
## DECEM 0.6841062 1.0000000 0.9857883
## PNCEM 0.6918289 0.9857883 1.0000000
# Últimos 12 meses
kable(tail(integrado, 12), digits = 2,
caption = "Crecimiento interanual de la tendencia: últimos 12 meses")
| Fecha | LICC | DECEM | PNCEM | |
|---|---|---|---|---|
| 145 | 2025-01-01 | -7.06 | -1.62 | -2.16 |
| 146 | 2025-02-01 | -4.16 | -0.42 | -1.13 |
| 147 | 2025-03-01 | -1.11 | 0.79 | -0.09 |
| 148 | 2025-04-01 | 0.39 | 2.10 | 1.00 |
| 149 | 2025-05-01 | 1.98 | 3.43 | 2.11 |
| 150 | 2025-06-01 | 1.59 | 4.62 | 3.13 |
| 151 | 2025-07-01 | 1.18 | 5.82 | 4.15 |
| 152 | 2025-08-01 | -2.38 | 6.75 | 5.03 |
| 153 | 2025-09-01 | -5.87 | 7.67 | 5.92 |
| 154 | 2025-10-01 | -10.14 | 8.16 | 6.41 |
| 155 | 2025-11-01 | -14.27 | 8.66 | 6.90 |
| 156 | 2025-12-01 | -18.40 | 8.66 | 6.93 |
Para evitar una lectura basada únicamente en diciembre, se resume el crecimiento interanual promedio de la tendencia en 2023, 2024 y 2025.
promedio_anual_yoy <- integrado %>%
mutate(Anio = as.integer(format(Fecha, "%Y"))) %>%
filter(Anio %in% c(2023, 2024, 2025)) %>%
group_by(Anio) %>%
summarise(
LICC = mean(LICC, na.rm = TRUE),
DECEM = mean(DECEM, na.rm = TRUE),
PNCEM = mean(PNCEM, na.rm = TRUE)
)
kable(promedio_anual_yoy, digits = 2,
caption = "Crecimiento YoY promedio de la tendencia: 2023-2025")
| Anio | LICC | DECEM | PNCEM |
|---|---|---|---|
| 2023 | -19.77 | -4.21 | -2.43 |
| 2024 | -15.24 | -4.09 | -3.81 |
| 2025 | -4.85 | 4.55 | 3.18 |
La pregunta no es solamente si el sector creció en 2025, sino
qué parte del sistema se recuperó y cuál no. Si
DECEM y PNCEM regresan a crecimiento positivo
mientras LICC continúa negativa, la lectura correcta sería
una recuperación asimétrica, no una expansión homogénea
de toda la cadena.
Como referencia descriptiva se compara el promedio mensual observado en 2025 con el promedio de 2019. Esta comparación no pretende afirmar causalidad ni establecer que 2019 sea un año “normal”; solamente ayuda a dimensionar si las tres variables han recuperado de la misma manera sus niveles previos.
comparar_promedios <- function(serie, nombre) {
valores <- as.numeric(serie)
anios <- as.integer(format(fechas, "%Y"))
prom_2019 <- mean(valores[anios == 2019], na.rm = TRUE)
prom_2025 <- mean(valores[anios == 2025], na.rm = TRUE)
data.frame(
Variable = nombre,
Promedio_2019 = prom_2019,
Promedio_2025 = prom_2025,
Cambio_2025_vs_2019_pct = (prom_2025 / prom_2019 - 1) * 100
)
}
comparacion_2019_2025 <- bind_rows(
comparar_promedios(licc_ts, "LICC"),
comparar_promedios(decem_ts, "DECEM"),
comparar_promedios(pncem_ts, "PNCEM")
)
kable(comparacion_2019_2025, digits = 2,
caption = "Promedio mensual 2025 frente a 2019")
| Variable | Promedio_2019 | Promedio_2025 | Cambio_2025_vs_2019_pct |
|---|---|---|---|
| LICC | 2213007 | 1747974 | -21.01 |
| DECEM | 1042943 | 1075934 | 3.16 |
| PNCEM | 1084399 | 1172795 | 8.15 |
Una recuperación de los indicadores cementeros acompañada de un nivel de licencias todavía inferior al de 2019 sería otro indicio de que las tres señales no deben interpretarse como si representaran exactamente el mismo fenómeno.
Una lectura basada únicamente en el promedio anual puede conducir a una conclusión distinta de la que muestra la tendencia al final del año. Por eso se compara el cambio del promedio mensual 2025 frente a 2024 con el crecimiento interanual de la tendencia en diciembre de 2025.
comparar_anual_vs_cierre <- function(serie, analisis, nombre) {
valores <- as.numeric(serie)
anios <- as.integer(format(fechas, "%Y"))
prom_2024 <- mean(valores[anios == 2024], na.rm = TRUE)
prom_2025 <- mean(valores[anios == 2025], na.rm = TRUE)
data.frame(
Variable = nombre,
Cambio_promedio_2025_vs_2024_pct = (prom_2025 / prom_2024 - 1) * 100,
YoY_tendencia_dic_2025_pct = tail(as.numeric(analisis$yoy_tendencia), 1)
)
}
paradoja_anual_cierre <- bind_rows(
comparar_anual_vs_cierre(licc_ts, licc_a, "LICC"),
comparar_anual_vs_cierre(decem_ts, decem_a, "DECEM"),
comparar_anual_vs_cierre(pncem_ts, pncem_a, "PNCEM")
)
kable(paradoja_anual_cierre, digits = 2,
caption = "Promedio anual 2025 vs 2024 y señal de tendencia al cierre de 2025")
| Variable | Cambio_promedio_2025_vs_2024_pct | YoY_tendencia_dic_2025_pct |
|---|---|---|
| LICC | 6.58 | -18.40 |
| DECEM | 5.42 | 8.66 |
| PNCEM | 3.77 | 6.93 |
Este contraste busca un hallazgo que un análisis superficial podría perder: una variable puede cerrar 2025 con un promedio anual superior al de 2024 y, simultáneamente, mostrar una tendencia interanual negativa y deteriorándose en los últimos meses. En ese caso, el dato anual describe lo ocurrido durante el año, mientras que la tendencia de cierre aporta información más relevante para una decisión que se tomará hacia 2026.
La extracción de señales es especialmente útil cuando una serie es volátil. Para observar este punto se comparan, mes a mes durante 2025, la variación interanual de la serie original y la variación interanual de su tendencia.
comparar_observado_tendencia_2025 <- function(serie, analisis, nombre) {
yoy_obs <- (serie[13:length(serie)] / serie[1:(length(serie)-12)] - 1) * 100
df <- data.frame(
Fecha = analisis$fechas_yoy,
Variable = nombre,
YoY_observado = as.numeric(yoy_obs),
YoY_tendencia = as.numeric(analisis$yoy_tendencia)
)
df %>% filter(format(Fecha, "%Y") == "2025")
}
obs_vs_senal_2025 <- bind_rows(
comparar_observado_tendencia_2025(licc_ts, licc_a, "LICC"),
comparar_observado_tendencia_2025(decem_ts, decem_a, "DECEM"),
comparar_observado_tendencia_2025(pncem_ts, pncem_a, "PNCEM")
)
kable(obs_vs_senal_2025, digits = 2,
caption = "2025: variación YoY observada frente a variación YoY de la tendencia")
| Fecha | Variable | YoY_observado | YoY_tendencia |
|---|---|---|---|
| 2025-01-01 | LICC | 8.76 | -7.06 |
| 2025-02-01 | LICC | 61.54 | -4.16 |
| 2025-03-01 | LICC | 40.27 | -1.11 |
| 2025-04-01 | LICC | 2.53 | 0.39 |
| 2025-05-01 | LICC | -8.46 | 1.98 |
| 2025-06-01 | LICC | 46.58 | 1.59 |
| 2025-07-01 | LICC | 46.01 | 1.18 |
| 2025-08-01 | LICC | -15.00 | -2.38 |
| 2025-09-01 | LICC | -5.51 | -5.87 |
| 2025-10-01 | LICC | 20.87 | -10.14 |
| 2025-11-01 | LICC | -28.90 | -14.27 |
| 2025-12-01 | LICC | -24.70 | -18.40 |
| 2025-01-01 | DECEM | -2.55 | -1.62 |
| 2025-02-01 | DECEM | -5.94 | -0.42 |
| 2025-03-01 | DECEM | 14.08 | 0.79 |
| 2025-04-01 | DECEM | -7.13 | 2.10 |
| 2025-05-01 | DECEM | 8.67 | 3.43 |
| 2025-06-01 | DECEM | 2.47 | 4.62 |
| 2025-07-01 | DECEM | 13.47 | 5.82 |
| 2025-08-01 | DECEM | 1.35 | 6.75 |
| 2025-09-01 | DECEM | 16.57 | 7.67 |
| 2025-10-01 | DECEM | 10.63 | 8.16 |
| 2025-11-01 | DECEM | 8.24 | 8.66 |
| 2025-12-01 | DECEM | 5.57 | 8.66 |
| 2025-01-01 | PNCEM | -5.98 | -2.16 |
| 2025-02-01 | PNCEM | -3.42 | -1.13 |
| 2025-03-01 | PNCEM | 6.50 | -0.09 |
| 2025-04-01 | PNCEM | -6.84 | 1.00 |
| 2025-05-01 | PNCEM | 9.62 | 2.11 |
| 2025-06-01 | PNCEM | 1.93 | 3.13 |
| 2025-07-01 | PNCEM | 8.30 | 4.15 |
| 2025-08-01 | PNCEM | 4.88 | 5.03 |
| 2025-09-01 | PNCEM | 10.46 | 5.92 |
| 2025-10-01 | PNCEM | 6.55 | 6.41 |
| 2025-11-01 | PNCEM | 8.29 | 6.90 |
| 2025-12-01 | PNCEM | 3.80 | 6.93 |
licc_obs_senal <- obs_vs_senal_2025 %>% filter(Variable == "LICC")
p_licc_senal <- ggplot(licc_obs_senal, aes(x = Fecha)) +
geom_hline(yintercept = 0, linetype = "dotted") +
geom_line(aes(y = YoY_observado, color = "YoY observado"), linewidth = 0.75) +
geom_line(aes(y = YoY_tendencia, color = "YoY tendencia"), linewidth = 1) +
labs(title = "LICC: dato observado vs señal subyacente durante 2025",
x = "Mes", y = "% interanual", color = "") +
theme_minimal()
ggplotly(p_licc_senal)
El objetivo de esta comparación no es elegir una medida y descartar la otra. La serie observada muestra lo que efectivamente ocurrió en cada mes, mientras que la tendencia ayuda a identificar el movimiento persistente después de separar estacionalidad y ruido. Una diferencia grande entre ambas es, por sí misma, un hallazgo sobre la volatilidad de la señal.
correlaciones_tendencia <- cor(
integrado[, c("LICC", "DECEM", "PNCEM")],
use = "complete.obs"
)
kable(round(correlaciones_tendencia, 3),
caption = "Correlación entre tasas YoY de las tendencias")
| LICC | DECEM | PNCEM | |
|---|---|---|---|
| LICC | 1.000 | 0.684 | 0.692 |
| DECEM | 0.684 | 1.000 | 0.986 |
| PNCEM | 0.692 | 0.986 | 1.000 |
La correlación se interpreta como asociación descriptiva, no
causalidad. Un valor muy alto entre DECEM y
PNCEM indicaría que producción y despachos se
ajustan de forma estrechamente sincronizada, mientras que una
asociación menor con LICC sugeriría que las licencias
aportan una señal distinta y más volátil. Esto es más útil que
simplemente afirmar que “las tres variables se relacionan”.
Además de la correlación, se revisa la diferencia entre producción y despachos. Esta diferencia no se interpreta automáticamente como inventarios, porque puede incorporar exportaciones y otros movimientos. Su utilidad es observar si la brecha entre producción total y despachos nacionales se amplía o se reduce con el tiempo.
df_brecha_pd <- data.frame(
Fecha = fechas,
PNCEM = as.numeric(pncem_ts),
DECEM = as.numeric(decem_ts)
) %>%
mutate(
Brecha_toneladas = PNCEM - DECEM,
Ratio_produccion_despachos = PNCEM / DECEM,
Anio = as.integer(format(Fecha, "%Y"))
)
resumen_brecha_pd <- df_brecha_pd %>%
filter(Anio %in% c(2019, 2023, 2024, 2025)) %>%
group_by(Anio) %>%
summarise(
Brecha_promedio_ton = mean(Brecha_toneladas, na.rm = TRUE),
Ratio_promedio = mean(Ratio_produccion_despachos, na.rm = TRUE),
Correlacion_mensual = cor(PNCEM, DECEM, use = "complete.obs")
)
kable(resumen_brecha_pd, digits = 3,
caption = "Relación entre producción y despachos en años seleccionados")
| Anio | Brecha_promedio_ton | Ratio_promedio | Correlacion_mensual |
|---|---|---|---|
| 2019 | 41456.26 | 1.039 | 0.878 |
| 2023 | 109765.44 | 1.103 | 0.763 |
| 2024 | 109627.90 | 1.109 | 0.851 |
| 2025 | 96861.11 | 1.091 | 0.943 |
Un estrechamiento de la brecha junto con una mayor sincronización puede indicar que producción y despachos se están moviendo de forma más alineada; no permite, por sí solo, identificar la causa de esa alineación.
integrado <- integrado %>%
mutate(
Senal_cementera = (DECEM + PNCEM) / 2,
Brecha_cemento_licencias = Senal_cementera - LICC
)
df_divergencia <- integrado %>% filter(Fecha >= as.Date("2023-01-01"))
p_div <- ggplot(df_divergencia, aes(x = Fecha)) +
geom_hline(yintercept = 0, linetype = "dotted") +
geom_line(aes(y = LICC, color = "Licencias (LICC)"), linewidth = 0.85) +
geom_line(aes(y = Senal_cementera, color = "Promedio DECEM-PNCEM"), linewidth = 0.95) +
labs(title = "Divergencia reciente: licencias vs señal cementera",
subtitle = "Crecimiento interanual (%) de las tendencias",
x = "Tiempo", y = "% YoY", color = "") +
theme_minimal()
ggplotly(p_div)
kable(tail(integrado[, c("Fecha", "LICC", "Senal_cementera", "Brecha_cemento_licencias")], 12),
digits = 2,
caption = "Brecha entre la señal cementera y las licencias durante los últimos 12 meses")
| Fecha | LICC | Senal_cementera | Brecha_cemento_licencias | |
|---|---|---|---|---|
| 145 | 2025-01-01 | -7.06 | -1.89 | 5.18 |
| 146 | 2025-02-01 | -4.16 | -0.77 | 3.38 |
| 147 | 2025-03-01 | -1.11 | 0.35 | 1.46 |
| 148 | 2025-04-01 | 0.39 | 1.55 | 1.17 |
| 149 | 2025-05-01 | 1.98 | 2.77 | 0.79 |
| 150 | 2025-06-01 | 1.59 | 3.87 | 2.29 |
| 151 | 2025-07-01 | 1.18 | 4.99 | 3.81 |
| 152 | 2025-08-01 | -2.38 | 5.89 | 8.27 |
| 153 | 2025-09-01 | -5.87 | 6.80 | 12.67 |
| 154 | 2025-10-01 | -10.14 | 7.29 | 17.42 |
| 155 | 2025-11-01 | -14.27 | 7.78 | 22.05 |
| 156 | 2025-12-01 | -18.40 | 7.79 | 26.19 |
La brecha no constituye por sí sola una prueba de desequilibrio ni de inventarios. Su utilidad es mostrar cuándo el comportamiento del cemento deja de acompañar la señal de licencias y exige una explicación más profunda.
Esta parte no proviene de la base del curso; se utiliza únicamente para contextualizar un patrón detectado en nuestros datos.
El boletín de Estadísticas de Cemento Gris (DANE, diciembre de 2025) reporta que los despachos nacionales acumulados de 2025 crecieron 4,9% frente a 2024. Sin embargo, la composición del crecimiento es todavía más interesante: el canal de comercialización aumentó 11,4% y aportó 6,4 puntos porcentuales a la variación total, mientras que el canal de constructores y contratistas disminuyó 5,9%. Esto ofrece una explicación económicamente plausible para que los despachos totales mejoren sin que la señal de licencias muestre la misma fortaleza.
Además, el mismo boletín aclara que la diferencia entre la producción
total y los despachos nacionales corresponde a exportaciones y,
en menor magnitud, existencias. Por ello, en este trabajo no se
interpretará automáticamente la diferencia PNCEM - DECEM
como inventario.
Fuente oficial: https://www.dane.gov.co/files/operaciones/ECG/bol-ECG-dic2025.pdf
El boletín de Licencias de Construcción (DANE, diciembre de 2025) muestra además una diferencia importante entre el acumulado anual y el cierre del año: el área licenciada de todo 2025 aumentó 6,1% frente a 2024, pero solamente diciembre de 2025 cayó 28,1% frente a diciembre de 2024. Esta aparente contradicción refuerza la utilidad de analizar la tendencia y no solamente el total del año o una variación aislada.
Fuente oficial: https://www.dane.gov.co/files/operaciones/ELIC/bol-ELIC-dic2025.pdf
Nota de comparabilidad: el DANE actualiza cifras provisionales cuando recibe correcciones o información extemporánea. Por ello, las cifras de los boletines más recientes pueden diferir ligeramente de la base entregada en clase. Para los cálculos del trabajo se mantendrá siempre la base oficial suministrada por la profesora; las publicaciones del DANE se usarán únicamente como contexto explicativo.
Los resultados muestran una recuperación asimétrica
al cierre de 2025. La tasa interanual de la tendencia de
DECEM pasa de -1.6% en enero a 8.7% en diciembre, mientras
PNCEM pasa de -2.2% a 6.9%. En contraste, LICC
cierra con una tasa interanual de tendencia de -18.4%.
El contraste anual añade un hallazgo importante: el promedio
observado de LICC en 2025 fue 6.6% superior al de 2024,
pero su tendencia cerró el año en -18.4%. Esto demuestra que un
balance anual favorable puede coexistir con una señal de cierre
deteriorándose, precisamente el tipo de información que se
perdería si se observaran únicamente promedios o variaciones mensuales
aisladas.
La comparación con 2019 refuerza esa lectura: en promedio,
DECEM se ubica 3.2% por encima de 2019 y PNCEM
8.2% por encima, mientras LICC permanece -21% respecto de
ese nivel de referencia. No se interpreta 2019 como un año causalmente
“normal”; se utiliza únicamente como punto descriptivo de
comparación.
En conjunto, las señales son mixtas: existe fortaleza reciente en despachos y producción, pero no una confirmación equivalente desde las licencias. Para una empresa como Cementos Argos, la implicación no es frenar automáticamente la operación, sino aprovechar la demanda corriente con disciplina de capacidad e inventarios y monitorear si el deterioro de LICC persiste antes de extrapolar la expansión cementera hacia horizontes más largos.
Se selecciona DECEM para el pronóstico porque los despachos representan una medida cercana a la demanda efectiva de cemento y, por tanto, tienen una conexión directa con decisiones de producción, inventarios y logística.
Se reserva el año 2025 como periodo de prueba para evaluar el comportamiento predictivo en datos no utilizados en la estimación inicial.
train_decem <- window(decem_ts, end = c(2024, 12))
test_decem <- window(decem_ts, start = c(2025, 1))
length(train_decem)
## [1] 156
length(test_decem)
## [1] 12
adf_nivel <- adf.test(train_decem)
adf_nivel
##
## Augmented Dickey-Fuller Test
##
## data: train_decem
## Dickey-Fuller = -4.5337, Lag order = 5, p-value = 0.01
## alternative hypothesis: stationary
p_adf <- adf_nivel$p.value
# Verificación complementaria del número de diferencias sugeridas por
# dos criterios disponibles en el paquete forecast.
d_adf <- ndiffs(train_decem, test = "adf")
d_kpss <- ndiffs(train_decem, test = "kpss")
data.frame(
Criterio = c("ADF", "KPSS"),
Diferencias_sugeridas = c(d_adf, d_kpss)
) %>%
kable(caption = "Número de diferencias sugeridas por pruebas alternativas")
| Criterio | Diferencias_sugeridas |
|---|---|
| ADF | 0 |
| KPSS | 1 |
El test ADF arroja un valor p de 0.01. Por tanto, se rechaza H0 y, bajo la especificación de esta prueba, no se encuentra evidencia de raíz unitaria.
Hipótesis utilizadas en clase:
Además, se reporta ndiffs() con ADF y KPSS porque
distintos contrastes pueden sugerir un número diferente de
diferenciaciones. Esto permite explicar de forma transparente
por qué la selección automática puede incorporar d = 1
aunque el ADF individual rechace la raíz unitaria. El orden
final no se justifica por una sola prueba: se contrasta con la selección
automática, la parsimonia, el diagnóstico de residuos y el desempeño
predictivo fuera de muestra.
ggAcf(train_decem, lag.max = 36) + ggtitle("DECEM - Función de autocorrelación (ACF)")
ggPacf(train_decem, lag.max = 36) + ggtitle("DECEM - Función de autocorrelación parcial (PACF)")
# Procedimiento automático permitido por la guía del trabajo final.
# Se mantiene seasonal = TRUE porque la serie es mensual y la extracción de señales
# permite comprobar la presencia o ausencia de patrón estacional.
modelo_auto <- auto.arima(
train_decem,
seasonal = TRUE,
stepwise = FALSE,
approximation = FALSE
)
modelo_auto
## Series: train_decem
## ARIMA(1,1,1)(1,0,0)[12]
##
## Coefficients:
## ar1 ma1 sar1
## 0.4859 -0.9439 0.2263
## s.e. 0.0903 0.0435 0.0796
##
## sigma^2 = 8.324e+09: log likelihood = -1989.53
## AIC=3987.07 AICc=3987.34 BIC=3999.24
summary(modelo_auto)
## Series: train_decem
## ARIMA(1,1,1)(1,0,0)[12]
##
## Coefficients:
## ar1 ma1 sar1
## 0.4859 -0.9439 0.2263
## s.e. 0.0903 0.0435 0.0796
##
## sigma^2 = 8.324e+09: log likelihood = -1989.53
## AIC=3987.07 AICc=3987.34 BIC=3999.24
##
## Training set error measures:
## ME RMSE MAE MPE MAPE MASE ACF1
## Training set 8375.07 90060.37 61598.39 -0.8299535 7.44389 0.776646 -0.01721159
orden_auto <- arimaorder(modelo_auto)
orden_auto
## p d q P D Q Frequency
## 1 1 1 1 0 0 12
coeftest(modelo_auto)
##
## z test of coefficients:
##
## Estimate Std. Error z value Pr(>|z|)
## ar1 0.485874 0.090294 5.3810 7.406e-08 ***
## ma1 -0.943912 0.043495 -21.7016 < 2.2e-16 ***
## sar1 0.226296 0.079571 2.8439 0.004456 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
texto_modelo <- if (orden_auto["P"] == 0 && orden_auto["D"] == 0 && orden_auto["Q"] == 0) {
paste0("ARIMA(", orden_auto["p"], ",", orden_auto["d"], ",", orden_auto["q"], ")")
} else {
paste0("SARIMA(", orden_auto["p"], ",", orden_auto["d"], ",", orden_auto["q"], ")(",
orden_auto["P"], ",", orden_auto["D"], ",", orden_auto["Q"], ")[", orden_auto["Frequency"], "]")
}
El procedimiento automático selecciona SARIMA(1,1,1)(1,0,0)[12], con AIC = 3987.07 y BIC = 3999.24. La selección no se aceptará únicamente por el criterio de información: se contrasta con residuos y desempeño predictivo fuera de muestra.
checkresiduals(modelo_auto)
##
## Ljung-Box test
##
## data: Residuals from ARIMA(1,1,1)(1,0,0)[12]
## Q* = 10.755, df = 21, p-value = 0.9673
##
## Model df: 3. Total lags used: 24
# Prueba de Ljung-Box reproducible para reportar el valor p en el texto
lag_lb <- min(24, floor(length(residuals(modelo_auto)) / 5))
fitdf_lb <- sum(orden_auto[c("p", "q", "P", "Q")])
ljung_box <- Box.test(
residuals(modelo_auto),
lag = lag_lb,
type = "Ljung-Box",
fitdf = fitdf_lb
)
La prueba de Ljung-Box obtiene un valor p de 0.9673. No se rechaza la hipótesis de ausencia de autocorrelación residual, por lo que los residuos son razonablemente compatibles con ruido blanco.
pronostico_test <- forecast(modelo_auto, h = length(test_decem))
comparacion_test <- data.frame(
Mes = seq.Date(as.Date("2025-01-01"), by = "month", length.out = length(test_decem)),
Observado = as.numeric(test_decem),
Pronosticado = as.numeric(pronostico_test$mean)
)
kable(comparacion_test, digits = 2, caption = "DECEM: observado vs pronosticado en 2025")
| Mes | Observado | Pronosticado |
|---|---|---|
| 2025-01-01 | 889613.5 | 994458 |
| 2025-02-01 | 981553.2 | 1033714 |
| 2025-03-01 | 1084450.0 | 1017384 |
| 2025-04-01 | 1012146.3 | 1051170 |
| 2025-05-01 | 1099239.1 | 1034558 |
| 2025-06-01 | 974792.5 | 1021468 |
| 2025-07-01 | 1203723.8 | 1046520 |
| 2025-08-01 | 1098571.4 | 1051877 |
| 2025-09-01 | 1181978.7 | 1036110 |
| 2025-10-01 | 1201023.9 | 1052347 |
| 2025-11-01 | 1112120.4 | 1039209 |
| 2025-12-01 | 1071996.2 | 1036482 |
MAE_test <- mean(abs(comparacion_test$Observado - comparacion_test$Pronosticado))
RMSE_test <- sqrt(mean((comparacion_test$Observado - comparacion_test$Pronosticado)^2))
MAPE_test <- mean(abs((comparacion_test$Observado - comparacion_test$Pronosticado) /
comparacion_test$Observado)) * 100
metricas_test <- data.frame(
Metrica = c("MAE", "RMSE", "MAPE (%)"),
Valor = c(MAE_test, RMSE_test, MAPE_test)
)
kable(metricas_test, digits = 2, caption = "Métricas de error - validación 2025")
| Metrica | Valor |
|---|---|
| MAE | 81776.83 |
| RMSE | 92623.51 |
| MAPE (%) | 7.48 |
En el conjunto de prueba de 2025, el modelo obtiene un MAE de 81.777 toneladas, un RMSE de 92.624 toneladas y un MAPE de 7.48%. Estas métricas se interpretan conjuntamente: MAE expresa el error medio en toneladas, RMSE penaliza con mayor fuerza los errores grandes y MAPE permite dimensionar el error en términos relativos.
p_validacion <- ggplot(comparacion_test, aes(x = Mes)) +
geom_line(aes(y = Observado, color = "Observado"), linewidth = 0.8) +
geom_line(aes(y = Pronosticado, color = "Pronosticado"), linewidth = 0.8) +
labs(title = "Despachos de cemento: observado vs pronosticado - 2025",
x = "Mes", y = "Toneladas", color = "") +
theme_minimal()
ggplotly(p_validacion)
Nota metodológica: los ejemplos previos de clase
muestran principalmente separación entrenamiento/prueba. La guía final
exige expresamente validación cruzada. Por ello, se añade una
validación temporal de origen móvil con tsCV() del mismo
paquete forecast que se utiliza en los ejercicios del
curso. Esta parte se incorpora para cumplir la guía final y se
interpreta como complemento del conjunto de prueba 2025.
En cada origen temporal se conserva la estructura ARIMA/SARIMA seleccionada, pero sus parámetros se vuelven a estimar utilizando solamente los datos disponibles hasta ese momento. Así se evita evaluar el modelo con información futura.
usa_drift <- "drift" %in% names(coef(modelo_auto))
usa_media <- "intercept" %in% names(coef(modelo_auto))
funcion_cv <- function(y, h) {
ajuste <- Arima(
y,
order = c(orden_auto["p"], orden_auto["d"], orden_auto["q"]),
seasonal = list(
order = c(orden_auto["P"], orden_auto["D"], orden_auto["Q"]),
period = orden_auto["Frequency"]
),
include.drift = usa_drift,
include.mean = usa_media,
method = "ML"
)
forecast(ajuste, h = h)
}
errores_cv <- tsCV(
decem_ts,
forecastfunction = funcion_cv,
h = 1,
initial = 60
)
# Para h = 1, el error asociado a cada origen se compara con la observación siguiente.
error_cv <- as.numeric(errores_cv)
actual_siguiente <- c(as.numeric(decem_ts)[-1], NA)
validos <- !is.na(error_cv) & !is.na(actual_siguiente) & actual_siguiente != 0
MAE_cv <- mean(abs(error_cv[validos]))
RMSE_cv <- sqrt(mean(error_cv[validos]^2))
MAPE_cv <- mean(abs(error_cv[validos] / actual_siguiente[validos])) * 100
metricas_cv <- data.frame(
Metrica = c("MAE CV", "RMSE CV", "MAPE CV (%)"),
Valor = c(MAE_cv, RMSE_cv, MAPE_cv)
)
kable(metricas_cv, digits = 2, caption = "Métricas de validación cruzada temporal")
| Metrica | Valor |
|---|---|
| MAE CV | 71223.00 |
| RMSE CV | 114680.01 |
| MAPE CV (%) | 9.26 |
La validación cruzada temporal obtiene un MAE de 71.223 toneladas, un RMSE de 114.680 toneladas y un MAPE de 9.26%. Esta evaluación es especialmente útil porque una sola partición entrenamiento/prueba puede favorecer o perjudicar al modelo dependiendo de qué tan atípico sea el periodo reservado. La comparación entre estas métricas y las del holdout 2025 permite evaluar si el desempeño es estable en diferentes orígenes temporales.
Un MAPE aceptable puede ocultar meses en los que el error es mucho mayor. Por eso se estudia cuándo falla el modelo y si existe un sesgo sistemático de sobreestimación o subestimación.
comparacion_test <- comparacion_test %>%
mutate(
Error = Observado - Pronosticado,
Error_abs = abs(Error),
Error_pct_abs = abs(Error / Observado) * 100,
Sesgo = ifelse(Error > 0, "Subestimó", "Sobreestimó")
)
# Error promedio con signo: positivo significa que, en promedio, el modelo quedó por debajo del observado.
sesgo_medio_2025 <- mean(comparacion_test$Error, na.rm = TRUE)
kable(
comparacion_test %>% arrange(desc(Error_pct_abs)),
digits = 2,
caption = "Meses de 2025 ordenados desde el mayor error porcentual"
)
| Mes | Observado | Pronosticado | Error | Error_abs | Error_pct_abs | Sesgo |
|---|---|---|---|---|---|---|
| 2025-07-01 | 1203723.8 | 1046520 | 157203.59 | 157203.59 | 13.06 | Subestimó |
| 2025-10-01 | 1201023.9 | 1052347 | 148676.77 | 148676.77 | 12.38 | Subestimó |
| 2025-09-01 | 1181978.7 | 1036110 | 145868.98 | 145868.98 | 12.34 | Subestimó |
| 2025-01-01 | 889613.5 | 994458 | -104844.50 | 104844.50 | 11.79 | Sobreestimó |
| 2025-11-01 | 1112120.4 | 1039209 | 72911.63 | 72911.63 | 6.56 | Subestimó |
| 2025-03-01 | 1084450.0 | 1017384 | 67066.18 | 67066.18 | 6.18 | Subestimó |
| 2025-05-01 | 1099239.1 | 1034558 | 64680.77 | 64680.77 | 5.88 | Subestimó |
| 2025-02-01 | 981553.2 | 1033714 | -52160.82 | 52160.82 | 5.31 | Sobreestimó |
| 2025-06-01 | 974792.5 | 1021468 | -46675.82 | 46675.82 | 4.79 | Sobreestimó |
| 2025-08-01 | 1098571.4 | 1051877 | 46694.49 | 46694.49 | 4.25 | Subestimó |
| 2025-04-01 | 1012146.3 | 1051170 | -39024.08 | 39024.08 | 3.86 | Sobreestimó |
| 2025-12-01 | 1071996.2 | 1036482 | 35514.38 | 35514.38 | 3.31 | Subestimó |
data.frame(Sesgo_medio_toneladas = sesgo_medio_2025) %>%
kable(digits = 0, caption = "Sesgo promedio del pronóstico durante 2025")
| Sesgo_medio_toneladas |
|---|
| 41326 |
peor_mes <- comparacion_test %>% arrange(desc(Error_pct_abs)) %>% slice(1)
El mayor error porcentual del holdout ocurre en 2025-07, con 13.06%. El sesgo medio es de 41.326 toneladas; con la convención utilizada, un valor positivo significa que el modelo tendió a quedar por debajo del observado y uno negativo que tendió a sobreestimarlo. Esto permite discutir no solo cuánto se equivoca el modelo, sino cómo se distribuyen sus errores.
p_error <- ggplot(comparacion_test, aes(x = Mes, y = Error)) +
geom_hline(yintercept = 0, linetype = "dotted") +
geom_col() +
labs(title = "Error mensual del modelo durante 2025",
subtitle = "Error = observado - pronosticado",
x = "Mes", y = "Toneladas") +
theme_minimal()
ggplotly(p_error)
Esta sección permite formular una recomendación más útil que “el modelo tiene un MAPE de X%”. Si los mayores errores se concentran en meses con movimientos extraordinarios, la empresa sabrá que el modelo sirve como línea base, pero debe complementarse con información operativa reciente antes de comprometer capacidad o inventarios. Si, en cambio, el error presenta siempre el mismo signo, habría evidencia de sesgo que debería revisarse.
La defensa del modelo no depende únicamente de que
auto.arima() lo seleccione. Se basa en cuatro condiciones:
(1) estructura parsimoniosa, (2) residuos razonablemente compatibles con
ruido blanco, (3) métricas de error fuera de muestra y (4)
comportamiento estable en validación cruzada temporal.
Un punto metodológico importante es que el modelo con menor AIC/AICc no necesariamente tiene que ser el que produzca el menor error fuera de muestra. Los criterios de información evalúan ajuste y complejidad dentro de la muestra; la validación mide desempeño predictivo. Cuando ambos criterios apuntan en la misma dirección, la evidencia es más consistente; cuando difieren, se prioriza la discusión transparente entre ajuste en muestra y capacidad predictiva.
Después de validar la especificación, se conserva la estructura del modelo seleccionada con el conjunto de entrenamiento, pero se reestiman sus coeficientes utilizando toda la información disponible hasta diciembre de 2025, tal como exige la guía. De este modo, el pronóstico final de 2026 aprovecha también las observaciones de 2025 sin alterar retroactivamente la validación fuera de muestra.
# IMPORTANTE:
# Se conserva la estructura ARIMA/SARIMA elegida y validada con el conjunto
# de entrenamiento, pero los parámetros se REESTIMAN con toda la información
# disponible hasta diciembre de 2025.
#
# No se usa Arima(decem_ts, model = modelo_auto), porque al pasar `model=`
# el paquete forecast aplica el mismo modelo sin reestimar sus parámetros.
modelo_final <- Arima(
decem_ts,
order = c(orden_auto["p"], orden_auto["d"], orden_auto["q"]),
seasonal = list(
order = c(orden_auto["P"], orden_auto["D"], orden_auto["Q"]),
period = orden_auto["Frequency"]
),
include.drift = usa_drift,
include.mean = usa_media,
method = "ML"
)
summary(modelo_final)
## Series: decem_ts
## ARIMA(1,1,1)(1,0,0)[12]
##
## Coefficients:
## ar1 ma1 sar1
## 0.4784 -0.9467 0.2355
## s.e. 0.0856 0.0399 0.0772
##
## sigma^2 = 8.279e+09: log likelihood = -2143.21
## AIC=4294.42 AICc=4294.66 BIC=4306.89
##
## Training set error measures:
## ME RMSE MAE MPE MAPE MASE
## Training set 9609.169 89899.11 62305.64 -0.6453789 7.381897 0.7831375
## ACF1
## Training set -0.0369308
checkresiduals(modelo_final)
##
## Ljung-Box test
##
## data: Residuals from ARIMA(1,1,1)(1,0,0)[12]
## Q* = 12.375, df = 21, p-value = 0.9289
##
## Model df: 3. Total lags used: 24
pronostico_2026 <- forecast(modelo_final, h = 3, level = c(80, 95))
pronostico_2026
## Point Forecast Lo 80 Hi 80 Lo 95 Hi 95
## Jan 2026 1036864 920257.3 1153471 858529.3 1215199
## Feb 2026 1055882 923815.8 1187949 853904.0 1257861
## Mar 2026 1078854 942000.6 1215708 869554.6 1288154
tabla_2026 <- data.frame(
Mes = as.Date(c("2026-01-01", "2026-02-01", "2026-03-01")),
Pronostico = as.numeric(pronostico_2026$mean),
LI_80 = as.numeric(pronostico_2026$lower[,1]),
LS_80 = as.numeric(pronostico_2026$upper[,1]),
LI_95 = as.numeric(pronostico_2026$lower[,2]),
LS_95 = as.numeric(pronostico_2026$upper[,2])
)
kable(tabla_2026, digits = 0, caption = "Pronóstico de despachos de cemento - enero a marzo de 2026")
| Mes | Pronostico | LI_80 | LS_80 | LI_95 | LS_95 |
|---|---|---|---|---|---|
| 2026-01-01 | 1036864 | 920257 | 1153471 | 858529 | 1215199 |
| 2026-02-01 | 1055882 | 923816 | 1187949 | 853904 | 1257861 |
| 2026-03-01 | 1078854 | 942001 | 1215708 | 869555 | 1288154 |
El modelo estima despachos de 1.036.864 toneladas en enero, 1.055.882 en febrero y 1.078.854 en marzo de 2026. Para enero, el intervalo de predicción del 95% va aproximadamente de 858.529 a 1.215.199 toneladas, por lo que el valor puntual debe utilizarse como referencia central y no como una cifra cierta.
autoplot(pronostico_2026) +
autolayer(decem_ts, series = "Observado") +
labs(title = "Pronóstico de despachos de cemento - enero a marzo de 2026",
x = "Tiempo", y = "Toneladas") +
theme_minimal()
El pronóstico puntual debe comunicarse junto con sus intervalos. El modelo no produce certezas: los intervalos muestran que existe un rango de resultados compatibles con la incertidumbre del proceso.
La evidencia conjunta sugiere un escenario mixto. La demanda cementera observada y la producción terminan 2025 con una señal de tendencia favorable, pero las licencias muestran un deterioro persistente al cierre. El valor empresarial del análisis está precisamente en no confundir la fortaleza corriente de los despachos con una confirmación homogénea de toda la actividad futura de construcción.
DECEM cierra 2025 con una tendencia YoY de 8.7%, pero
LICC lo hace en -18.4%. Utilizar solo la primera señal
podría inducir una lectura demasiado optimista del horizonte
posterior.DECEM y PNCEM respalda una
planeación operativa que atienda la demanda actual sin asumir
automáticamente que la misma tasa de crecimiento continuará.DECEM como línea base de corto plazo y
actualizarlo mensualmente con los nuevos datos. La decisión no debe
apoyarse únicamente en el pronóstico puntual, sino también en los
intervalos y en el error histórico del modelo.LICC justifica revisar periódicamente la
decisión antes de aumentar compromisos de largo plazo.DECEM como señal de absorción del mercado y contrastarlo
con información interna de pedidos, clientes y canales. El DANE muestra
que en 2025 el crecimiento de los despachos estuvo impulsado
principalmente por comercialización, mientras constructores y
contratistas se contrajeron; la composición del crecimiento importa
tanto como el total.LICC
como indicador complementario de proyectos formalmente autorizados. No
se interpreta como predictor mecánico de los despachos, pero una
debilidad persistente merece seguimiento porque representa una dimensión
distinta de la actividad edificadora.PNCEM y DECEM mensualmente para detectar
desacoples operativos. La diferencia no debe llamarse “inventario” sin
información adicional, porque también puede reflejar exportaciones y
existencias.Quién utilizaría esta información: un equipo de planeación comercial y de operaciones, junto con responsables de producción y logística, podría utilizar el pronóstico de despachos para dimensionar necesidades de corto plazo; la dirección comercial y estratégica utilizaría la lectura conjunta de licencias, despachos y producción para decidir qué tan agresivamente ampliar compromisos de capacidad, inventarios y cobertura comercial.
El análisis no muestra una historia simple de expansión o
contracción. El hallazgo central es una recuperación cementera
acompañada de una señal más débil en licencias. Durante 2025,
la tendencia interanual de DECEM pasa de -1.6% en enero a
8.7% en diciembre y PNCEM de -2.2% a 6.9%.
LICC, en cambio, termina el año en -18.4%. Por ello, la
evidencia respalda hablar de señales mixtas y recuperación
asimétrica, no de una expansión homogénea de todo el
sector.
Un segundo hallazgo es que el promedio anual y la señal de
cierre pueden contar historias distintas. LICC
presenta un promedio mensual 2025 6.6% superior a 2024, mientras su
tendencia interanual de diciembre es -18.4%. Esto demuestra por qué la
extracción de señales agrega información para una decisión hacia 2026:
evita confundir un buen balance acumulado con una trayectoria reciente
favorable.
El tercer hallazgo es que la recuperación tampoco es igual frente a
2019. El promedio de 2025 de DECEM presenta una variación
de 3.2% frente a 2019, PNCEM de 8.2% y LICC de
-21%. Esta diferencia refuerza que las tres variables describen
dimensiones relacionadas, pero no idénticas, de la actividad
sectorial.
En materia predictiva, el modelo seleccionado SARIMA(1,1,1)(1,0,0)[12] obtiene un MAPE de 7.48% en el holdout de 2025 y de 9.26% en la validación cruzada temporal. El diagnóstico de Ljung-Box arroja un valor p de 0.9673. Estas medidas permiten evaluar el modelo con mayor rigor que el AIC por sí solo. El pronóstico final para enero-marzo de 2026 debe entenderse como una estimación probabilística y actualizarse a medida que se conozcan nuevos datos.
En síntesis, la recomendación para una empresa como Cementos Argos es
responder a la fortaleza actual de los despachos sin convertirla
automáticamente en una expectativa de crecimiento sostenido de mediano
plazo. La mejor decisión combina el pronóstico operativo de
DECEM con el seguimiento de PNCEM,
LICC, los canales de comercialización y la información
interna de pedidos y clientes. Esa combinación transforma el modelo de
una simple predicción estadística en una herramienta de decisión.
LICC y DECEM no se
interpreta como un adelanto temporal demostrado. Probar formalmente esa
hipótesis requeriría un análisis adicional que no forma parte de la
metodología principal exigida en la guía.Autor: Juan Esteban Cabrera
Pontificia Universidad Javeriana Cali