1. Contexto empresarial y pregunta de análisis

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?

1.1 Selección de variables

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.

2. Preparación de la base de datos

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

3. Extracción de señales

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

3.1 Serie original y tendencia

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)

3.2 Tasa de crecimiento interanual (YoY) de la tendencia

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)

3.3 Estacionalidad

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

3.4 Componente irregular

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

3.5 Lecturas no obvias: nivel, variación mensual y señal subyacente

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

3.6 Puntos de giro de la tendencia durante 2025

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

4. Integración de las tres señales

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

4.1 Hallazgo de segundo nivel: ¿la recuperación de 2025 fue sincronizada?

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

4.2 Hallazgo de segundo nivel: recuperación frente al nivel prepandemia

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

4.3 Hallazgo de segundo nivel: el promedio anual puede ocultar el deterioro del cierre

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

4.4 Hallazgo de segundo nivel: cuando el dato observado y la señal subyacente cuentan historias distintas

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

4.5 Hallazgo de segundo nivel: sincronización entre despachos y producción

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

4.6 Divergencia reciente entre la señal cementera y las licencias

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

4.7 Contraste con evidencia externa oficial: por qué puede crecer DECEM aunque LICC se debilite

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.

4.8 Lectura ejecutiva integrada de las señales

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.

5. Pronóstico de corto plazo: despachos de cemento (DECEM)

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.

5.1 Separación entrenamiento/prueba

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

5.2 Estacionariedad

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

  • H0: la serie tiene raíz unitaria (no estacionaria).
  • Ha: la serie no tiene raíz unitaria (estacionaria).

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

5.3 Selección automática ARIMA/SARIMA

# 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.

5.4 Diagnóstico de residuos

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.

5.5 Evaluación sobre el conjunto de prueba 2025

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

5.6 Validación cruzada temporal

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

5.7 Hallazgo de segundo nivel: no basta con conocer el error promedio

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

5.8 Criterio para escoger y defender el modelo

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.

6. Pronóstico final enero-marzo de 2026

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

7. De los resultados a la decisión

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.

7.1 Riesgos

  • Extrapolación excesiva del crecimiento reciente: 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.
  • Confundir ruido o estacionalidad con tendencia: en variables volátiles como LICC, variaciones mensuales elevadas pueden coexistir con una tendencia subyacente negativa.
  • Riesgo de error de pronóstico: incluso un modelo con buen error promedio puede fallar con mayor intensidad en meses atípicos; por eso el pronóstico debe actualizarse y acompañarse de intervalos de predicción.

7.2 Oportunidades

  • Aprovechar la fortaleza de corto plazo: la recuperación de DECEM y PNCEM respalda una planeación operativa que atienda la demanda actual sin asumir automáticamente que la misma tasa de crecimiento continuará.
  • Mejorar la anticipación: combinar el seguimiento de despachos, producción y licencias permite detectar divergencias que un único indicador no mostraría.
  • Planificar por estacionalidad: el componente estacional permite distinguir meses recurrentemente altos y bajos para organizar producción, transporte y disponibilidad comercial con anticipación.

7.3 Decisiones recomendadas para Cementos Argos

  1. Planeación de producción y despacho: utilizar el pronóstico de 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.
  2. Disciplina de capacidad e inventarios: atender la recuperación presente sin comprometer expansiones permanentes de capacidad únicamente a partir de los buenos despachos de 2025. La señal negativa de LICC justifica revisar periódicamente la decisión antes de aumentar compromisos de largo plazo.
  3. Monitoreo comercial: seguir mensualmente 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.
  4. Monitoreo sectorial: mantener 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.
  5. Control producción-despachos: comparar 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.

8. Conclusiones

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.

9. Limitaciones del análisis

  • Las variables corresponden a indicadores sectoriales nacionales, no a información interna de Cementos Argos. Por ello, las recomendaciones son de referencia para un tomador de decisiones del sector y deben complementarse con datos internos de la empresa.
  • STL y ARIMA/SARIMA son herramientas univariantes. Identifican patrones temporales y generan pronósticos, pero no demuestran causalidad entre licencias, producción y despachos.
  • La relación entre 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.
  • Las cifras del DANE pueden ser revisadas posteriormente. Para todos los cálculos se conserva la base suministrada por la asignatura y las publicaciones oficiales externas se utilizan únicamente como contexto.
  • Choques extraordinarios pueden reducir la precisión del pronóstico porque un modelo de series de tiempo supone que los patrones históricos conservan parte de su utilidad hacia el futuro.

10. Referencias


Autor: Juan Esteban Cabrera
Pontificia Universidad Javeriana Cali