Universidad Nacional de San Antonio Abad del Cusco
La corteza terrestre no es una estructura rígida e inamovible. En realidad, se comporta como un medio elástico que se deforma y “rebota” continuamente debido a dos factores principales: el empuje de las placas tectónicas y las variaciones de peso en la superficie (redistribución de masas). Las estaciones GNSS nos permiten registrar esta deformación tridimensional con precisión milimétrica.
Carga Hidrológica Regional: En la región andina, este fenómeno es especialmente crítico. La acumulación masiva de agua en el sistema de la cuenca amazónica genera una fuerza de carga que deforma físicamente la placa sudamericana, induciendo una oscilación vertical (hundimiento estacional) en la corteza (Bevis et al. 2005).
La estación CS01 en Cusco captura fielmente esta cinemática, pero extraer la señal pura es un reto estadístico complejo:
El Ruido de la Altura (H): La componente vertical es intrínsecamente inestable. No presenta un simple error aleatorio, sino un ruido Flicker que complica la estimación de incertidumbres (Williams et al. 2004).
Discontinuidades: Los fallos de equipo o cortes de energía generan vacíos (gaps) que introducen sesgos en los algoritmos clásicos de análisis de series temporales (Sun et al. 2023).
Propuesta del Trabajo
Ir más allá del monitoreo tradicional: aplicar modelos predictivos avanzados sobre la componente de altura (H) en Wanchaq y comprender su compleja dinámica estacional no lineal.
Los receptores GNSS calculan su posición tridimensional rastreando el tiempo de viaje de las señales satelitales. Estos datos brutos se obtienen en el sistema global XYZ (referenciado al centro de masa de la Tierra).
Para su interpretación geofísica, se proyectan a un plano local NEU (North, East, Up), permitiendo medir desplazamientos en una escala milimétrica:
La estación CS01 de Cusco forma parte de la Red de Operación Continua SIRGAS-CON, la cual establece el marco de referencia geodésico para las Américas.
Se descartaron métodos clásicos (como la interpolación lineal o medias móviles) debido a que tienden a “aplanar” la varianza y destruir los ciclos naturales de la serie, lo cual es perjudicial para la componente vertical (Altura) que es fuertemente influenciada por la carga hidrológica estacional y térmica.
Se reemplazaron los métodos de interpolación clásicos por el modelo SAITS (Self-Attention-based Imputation for Time Series), modelo respaldado por evaluaciones recientes en escenarios de pérdida severa de datos reales (Poette et al. 2026):
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from pypots.imputation import SAITS
df = pd.read_csv('Libro1 - copia.csv', sep=';')
# Extraer fechas y convertirlas a datetime
fechas = pd.to_datetime(df['fecha'], format='%d/%m/%Y')
datos_numericos = df.drop(columns=['fecha'])
scaler = StandardScaler()
datos_escalados = scaler.fit_transform(datos_numericos)
n_pasos = datos_escalados.shape[0]
n_variables = datos_escalados.shape[1]
datos_3d = datos_escalados.reshape(1, n_pasos, n_variables)
saits_modelo = SAITS(
n_steps=n_pasos,
n_features=n_variables,
n_layers=1,
d_model=64,
d_ffn=64,
n_heads=4,
d_k=64,
d_v=64,
dropout=0.2,
epochs=150
)
dataset_para_saits = {"X": datos_3d}
saits_modelo.fit(dataset_para_saits)
resultado_imputacion = saits_modelo.impute(dataset_para_saits)
datos_imputados_escalados = resultado_imputacion.reshape(n_pasos, n_variables)
datos_finales = scaler.inverse_transform(datos_imputados_escalados)
df_imputado = pd.DataFrame(datos_finales, columns=datos_numericos.columns)
df_imputado.insert(0, 'fecha', fechas)
fig, axes = plt.subplots(3, 1, figsize=(12, 10), sharex=True)
axes[0].plot(df_imputado['fecha'], df_imputado['north'], color='royalblue', linewidth=1.5, marker='o', markersize=3)
axes[0].set_title('Series de Tiempo GPS (Imputación Deep Learning: SAITS)', fontsize=14, fontweight='bold')
axes[0].set_ylabel('North (m)', fontsize=12)
axes[0].grid(True, linestyle='--', alpha=0.6)
axes[1].plot(df_imputado['fecha'], df_imputado['east'], color='crimson', linewidth=1.5, marker='o', markersize=3)
axes[1].set_ylabel('East (m)', fontsize=12)
axes[1].grid(True, linestyle='--', alpha=0.6)
axes[2].plot(df_imputado['fecha'], df_imputado['height'], color='forestgreen', linewidth=1.5, marker='o', markersize=3)
axes[2].set_ylabel('Height (m)', fontsize=12)
axes[2].set_xlabel('Fecha', fontsize=12)
axes[2].grid(True, linestyle='--', alpha=0.6)
plt.tight_layout()
plt.show()La naturaleza cronológica de los datos GNSS exige una división que preserve el orden temporal. Dado que las observaciones presentan una periodicidad semanal, se ha definido un esquema de partición que permita capturar la tendencia y estacionalidad a largo plazo en el entrenamiento, reservando el periodo más reciente para una evaluación ciega de la capacidad predictiva.
Se ha optado por una división técnica de 85/15:
library(tidyverse)
library(urca)
gnss <- read.csv("GNSS_NEU.csv", sep=";") # Datos
gnss$fecha <- as.Date(gnss$fecha)
ts_datos <- gnss %>% dplyr::select(north, east, height) %>% as.data.frame()
n_total <- nrow(ts_datos)
n_train <- floor(0.85 * n_total)
train_data <- ts_datos[1:n_train, ]
test_data <- ts_datos[(n_train + 1):n_total, ]
fecha_train <- gnss$fecha[1:n_train]
fecha_test <- gnss$fecha[(n_train + 1):n_total]
cat(paste0(
"Resumen de división:\n",
"- Entrenamiento (Train): ", nrow(train_data), " observaciones\n",
"- Prueba (Test): ", nrow(test_data), " observaciones"
))Resumen de división:
- Entrenamiento (Train): 326 observaciones
- Prueba (Test): 58 observaciones
par(mfrow = c(3, 2), mar = c(3, 5, 3, 1))
variables <- c("north", "east", "height")
for (i in 1:3) {
var <- variables[i]
acf(train_data[[var]], lag.max = 50, main = "", col = "darkblue")
if (i == 1) title(main = "ACF", cex.main = 1.5)
mtext(toupper(var), side = 2, line = 3, font = 2, cex = 1.1)
pacf(train_data[[var]], lag.max = 50, main = "", col = "darkred")
if (i == 1) title(main = "PACF", cex.main = 1.5)
}\[H_0: \text{No es estacionario}\]
A un 5%:
Resultados de Estacionariedad (Prueba ADF)
El modelo VECM exige que todas las variables sean estrictamente \(I(1)\). Debido al orden de integración mixto de las coordenadas GNSS, el enfoque ARDL es la metodología idónea, ya que permite modelar relaciones a largo plazo combinando series \(I(0)\) e \(I(1)\) sin perder información espacial.
El ARDL (Autoregresivo de Rezagos Distribuidos) es un modelo econométrico avanzado que permite estimar la dinámica temporal entre variables (Natsiopoulos y Tzeremes 2022). A diferencia de los modelos clásicos, el ARDL se destaca por dos ventajas fundamentales:
Es la única metodología que no exige que todas las variables tengan el mismo orden de integración, permitiendo combinar perfectamente variables \(I(0)\) (Este) e \(I(1)\) (Norte y Altura).
Estima simultáneamente la dinámica de corrección a corto plazo y la relación de equilibrio (cointegración) a largo plazo en una sola ecuación.
La componente vertical (Altura) en GNSS exhibe un ciclo periódico de 52 semanas impulsado por la variación estacional de las precipitaciones en los Andes. Para incorporar esta dinámica en el modelo ARDL, se optó por un enfoque armónico:
sin_anual y cos_anual) con \(K=1\) (Dong et al. 2002).library(ARDL)
library(forecast)
# 1. Aislar la estacionalidad (Ciclo anual GNSS)
ts_referencia_train <- ts(train_data$height, frequency = 52)
terminos_fourier_train <- fourier(ts_referencia_train, K = 1)
train_data$sin_anual <- terminos_fourier_train[, 1]
train_data$cos_anual <- terminos_fourier_train[, 2]La extracción del armónico fundamental (\(K=1\)) valida nuestra hipótesis física:
# 1. Crear los términos de Fourier
ts_referencia_train <- ts(train_data$height, frequency = 52)
terminos_fourier_train <- fourier(ts_referencia_train, K = 1)
# 2. Ajustar el modelo estacional para capturar la "Onda" a escala
modelo_estacional <- lm(train_data$height ~ terminos_fourier_train[, 1] + terminos_fourier_train[, 2])
train_data$onda_fourier <- fitted(modelo_estacional)
if (!"fecha" %in% colnames(train_data)) {
train_data$fecha <- seq(as.Date("2018-07-25"), by = "7 days", length.out = nrow(train_data))
}
ggplot(train_data, aes(x = fecha)) +
# Datos originales de fondo (tenues)
geom_line(aes(y = height, color = "Deformación Bruta"), linewidth = 0.6, alpha = 0.4) +
# La onda perfecta al frente (gruesa y llamativa)
geom_line(aes(y = onda_fourier, color = "Ciclo Anual (Fourier K=1)"), linewidth = 1.2) +
scale_color_manual(values = c("Deformación Bruta" = "#2c3e50", "Ciclo Anual (Fourier K=1)" = "#e74c3c")) +
theme_bw() +
labs(
title = "Aislamiento de la Estacionalidad",
subtitle = "Extracción de la carga hidrológica anual mediante Serie de Fourier",
y = "Coordenada Vertical (m)",
x = "Fecha de Entrenamiento",
color = ""
) +
theme(
legend.position = "bottom",
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(face = "italic", color = "gray30"),
legend.text = element_text(face = "bold", size = 11)
)La búsqueda algorítmica priorizó la parsimonia para evitar el sobreajuste del ruido GNSS, seleccionando un modelo ARDL(2, 1, 0).
Hallazgos Estadísticos Clave:
L(height, 1) y L(height, 2) altamente significativas, \(p < 0.001\)).East muestra un impacto significativo inmediato y retardado. Curiosamente, la componente North no es estadísticamente significativa para predecir la altura (\(p = 0.208\)).sin_anual y cos_anual son significativos, validando la absorción de la carga hidrológica estacional.
Time series regression with "ts" data:
Start = 3, End = 326
Call:
dynlm::dynlm(formula = full_formula, data = data, start = start,
end = end)
Residuals:
Min 1Q Median 3Q Max
-0.008511 -0.001981 -0.000116 0.001847 0.008385
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -0.0002729 0.0003797 -0.719 0.4729
L(height, 1) 0.6305792 0.0525071 12.009 < 2e-16 ***
L(height, 2) 0.2362290 0.0529271 4.463 1.12e-05 ***
east 0.3086212 0.0957503 3.223 0.0014 **
L(east, 1) -0.4805682 0.0953716 -5.039 7.89e-07 ***
north 0.0292012 0.0231634 1.261 0.2084
sin_anual 0.0006782 0.0003234 2.097 0.0368 *
cos_anual 0.0009689 0.0002411 4.019 7.32e-05 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 0.002934 on 316 degrees of freedom
Multiple R-squared: 0.8847, Adjusted R-squared: 0.8821
F-statistic: 346.3 on 7 and 316 DF, p-value: < 2.2e-16
Para determinar si las coordenadas GNSS comparten una relación de equilibrio fundamental a pesar de sus dinámicas individuales, se aplicó la prueba de Límites de Pesaran (Bounds F-test), ideal para variables con órdenes de integración mixtos.
Interpretación de Resultados:
La existencia de cointegración justifica matemáticamente la transición hacia un Modelo de Corrección de Errores (ECM) para evaluar a qué velocidad la estación corrige sus desviaciones a corto plazo.
Bounds F-test (Wald) for no cointegration
data: d(height) ~ L(height, 1) + L(east, 1) + north + d(L(height, 1)) + d(east) + sin_anual + cos_anual
F = 9.7808, p-value = 0.0004708
alternative hypothesis: Possible cointegration
null values:
k T
2 1000
1. Estimación Robusta (HAC): Se aplicó la matriz de varianzas y covarianzas de Newey-West. Esto “blinda” los errores estándar contra la heterocedasticidad y la autocorrelación intrínseca de los datos GNSS, garantizando que las inferencias estadísticas sean rigurosas.
2. Acoplamiento Este-Vertical: Se comprobó que el desplazamiento longitudinal (Este) es el principal motor de la deformación vertical a largo plazo (\(p < 0.05\)).
3. Desacoplamiento del Norte: De manera consistente con la dinámica a corto plazo, el desplazamiento latitudinal (Norte) no ejerce una influencia estadísticamente significativa en la vertical de largo plazo (\(p = 0.286\)).
# 2. Modelo de Corrección de Errores (ECM) con Errores Robustos (HAC)
modelo_ecm <- uecm(mejor_ardl)
ecm_robusto <- coeftest(modelo_ecm, vcov. = NeweyWest(modelo_ecm))
# 3. Multiplicadores de Largo Plazo (Impacto real de la placa tectónica)
multiplicadores_robustos <- multipliers(mejor_ardl, vcov_matrix = NeweyWest(mejor_ardl))
print(multiplicadores_robustos) Term Estimate Std. Error t value Pr(>|t|)
1 (Intercept) -0.002048677 0.002114642 -0.9688056 0.33338309
2 east -1.290972628 0.568321631 -2.2715529 0.02378563
3 north 0.219241738 0.205279630 1.0680151 0.28632893
El modelo ARDL demuestra una sólida capacidad para replicar la dinámica de deformación vertical histórica:
# 1. Extraer métricas de precisión
valores_ajustados <- as.numeric(fitted(mejor_ardl))
filas_restantes <- nrow(train_data) - length(valores_ajustados) + 1
valores_reales_train <- train_data$height[filas_restantes:nrow(train_data)]
r_cuadrado <- summary(mejor_ardl)$adj.r.squared
# 2. Gráfica de Ajuste
datos_train_grafica <- train_data[filas_restantes:nrow(train_data), ]
datos_train_grafica$fecha <- fecha_train[filas_restantes:length(fecha_train)]
datos_train_grafica$Pronostico <- valores_ajustados
ggplot(datos_train_grafica, aes(x = fecha)) +
geom_line(aes(y = height, color = "Altura Real"), linewidth = 0.8, alpha = 0.6) +
geom_line(aes(y = Pronostico, color = "Ajuste ARDL"), linewidth = 0.8, linetype = "dashed") +
scale_color_manual(values = c("Altura Real" = "gray60", "Ajuste ARDL" = "darkblue")) +
theme_minimal() +
labs(title = "Memoria del Modelo: Ajuste en Datos de Entrenamiento (85%)",
subtitle = sprintf("Varianza explicada (R-cuadrado Ajustado): %.2f%%", r_cuadrado * 100),
y = "Desplazamiento Vertical (m)", x = "Fecha", color = "") +
theme(legend.position = "bottom", plot.title = element_text(face="bold"))Para garantizar que el modelo ha extraído toda la información sistemática de la serie y que los estimadores son eficientes, se analizó la estructura de los residuos.
Los residuos del modelo se comportan estrictamente como ruido blanco. Esto significa que las variaciones no explicadas por el modelo son perturbaciones puramente aleatorias, garantizando la robustez y validez de las inferencias realizadas.
Box-Ljung test
data: residuos_ardl
X-squared = 12.582, df = 10, p-value = 0.248
Breusch-Godfrey test for serial correlation of order up to 1
data: mejor_ardl
LM test = 0.82109, df = 1, p-value = 0.3649
Una vez validada la estructura del modelo, la cointegración y la limpieza de los residuos, el paso definitivo es medir su capacidad de generalización. Para ello, se proyectó el modelo sobre el 15% de datos recientes (conjunto de prueba), un segmento de información que el algoritmo “nunca ha visto”, lo que garantiza una evaluación sin sesgos por sobreajuste.
library(dplyr)
library(ggplot2)
library(forecast)
# 1. Proyectar la estacionalidad al futuro
terminos_fourier_test <- fourier(ts_referencia_train, K = 1, h = nrow(test_data))
test_data$sin_anual <- as.numeric(terminos_fourier_test[, 1])
test_data$cos_anual <- as.numeric(terminos_fourier_test[, 2])
# 2. EL ARREGLO: Usar bind_rows en lugar de rbind
data_predict <- bind_rows(train_data, test_data) %>%
mutate(
height_L1 = dplyr::lag(height, 1),
height_L2 = dplyr::lag(height, 2),
east_L1 = dplyr::lag(east, 1)
)
train_predict <- data_predict[1:nrow(train_data), ]
test_predict <- data_predict[(nrow(train_data) + 1):nrow(data_predict), ]
# Re-entrenamos la versión LM explícita con los coeficientes correctos
modelo_lm_predict <- lm(height ~ height_L1 + height_L2 + east + east_L1 + north + sin_anual + cos_anual,
data = train_predict)
# 3. Pronóstico y Cálculo de Error
predicciones_test <- predict(modelo_lm_predict, newdata = test_predict)
rmse_test <- sqrt(mean((test_data$height - predicciones_test)^2, na.rm = TRUE))
mae_test <- mean(abs(test_data$height - predicciones_test), na.rm = TRUE)
# 4. Gráfica Final
# --- SOLUCIÓN: Inyectar fechas al test_data ---
if (!"fecha" %in% colnames(test_data)) {
# Tomamos la última fecha de tu entrenamiento y le sumamos 7 días
ultima_fecha_train <- max(train_data$fecha)
test_data$fecha <- seq(ultima_fecha_train + 7, by = "7 days", length.out = nrow(test_data))
}
df_test_grafica <- data.frame(
fecha = test_data$fecha,
Real = test_data$height,
Pronostico = as.numeric(predicciones_test)
)
ggplot(df_test_grafica, aes(x = fecha)) +
geom_line(aes(y = Real, color = "Altura Real"), linewidth = 1) +
geom_line(aes(y = Pronostico, color = "Pronóstico ARDL"), linewidth = 1, linetype = "dashed") +
scale_color_manual(values = c("Altura Real" = "gray40", "Pronóstico ARDL" = "darkred")) +
theme_minimal() +
labs(title = "Predicción en Datos Nuevos (15%)",
subtitle = sprintf("Precisión de pronóstico: RMSE = %.5f m | MAE = %.5f m", rmse_test, mae_test),
y = "Desplazamiento Vertical (m)", x = "Fecha", color = "") +
theme(legend.position = "bottom", plot.title = element_text(face="bold"))Para entender la resiliencia geológica de la estación, simulamos un “shock” súbito (impulso) de una unidad hacia el Este y rastreamos la reacción vertical en el tiempo:
# RESPUESTA AL IMPULSO (MULTIPLICADORES DINÁMICOS) PARA ARDL
coefs <- coef(modelo_lm_predict)
horizonte <- 20
respuestas <- numeric(horizonte)
# Simulamos un "Shock" unitario en el componente ESTE
# Recordando la estructura: Height ~ Height_L1 + Height_L2 + East + East_L1 ...
impulse_value <- 1
# Calculamos el efecto inmediato y propagado (Recursión)
for (t in 1:horizonte) {
if (t == 1) {
# Efecto inmediato: Coeficiente de 'east'
respuestas[t] <- coefs["east"] * impulse_value
} else if (t == 2) {
# Primer rezago: Coeficiente de 'east_L1' + persistencia de la altura
respuestas[t] <- (coefs["height_L1"] * respuestas[t-1]) + (coefs["east_L1"] * impulse_value)
} else {
# Efecto a largo plazo: Solo queda la persistencia (memoria) de la altura
respuestas[t] <- (coefs["height_L1"] * respuestas[t-1]) + (coefs["height_L2"] * respuestas[t-2])
}
}
# 4. Graficar el Multiplicador Dinámico (Convirtiendo metros a milímetros)
df_irf_ardl <- data.frame(Semana = 1:horizonte, Impacto = respuestas * 1000)
ggplot(df_irf_ardl, aes(x = Semana, y = Impacto)) +
geom_line(color = "darkblue", linewidth = 1) +
geom_point(color = "darkblue", size = 2) +
geom_hline(yintercept = 0, linetype = "dashed", color = "red", linewidth = 0.8) +
theme_minimal() +
labs(title = "Respuesta Dinámica de la Altura ante un Shock Súbito en el Este",
subtitle = "Función de Respuesta al Impulso (IRF)",
x = "Semanas después del evento",
y = "Respuesta Transitoria en Altura (mm)") +
theme(plot.title = element_text(face="bold"))Para validar la robustez del ARDL, estructuramos un modelo ARIMAX como línea base (benchmark). Este modelo evalúa la deformación vertical asumiendo que los desplazamientos horizontales son choques externos directos.
xreg) fusiona la cinemática horizontal (Este, Norte) con la carga hidrológica.seasonal = FALSE): Como la serie de Fourier ya extrajo matemáticamente la onda anual hidrológica perfecta, desactivamos la búsqueda estacional nativa del algoritmo para evitar redundancia algorítmica y sobreparametrización.library(forecast)
# 1. Preparar las variables externas (La matriz "X" de regresores)
# Unimos el desplazamiento horizontal y las ondas estacionales de Fourier
xreg_train_sarimax <- cbind(
east = train_data$east,
north = train_data$north,
sin_anual = terminos_fourier_train[, 1],
cos_anual = terminos_fourier_train[, 2]
)En lugar de imponer los parámetros autorregresivos y de media móvil manualmente, empleamos el algoritmo iterativo de Hyndman-Khandakar (auto.arima).
El algoritmo optimizado por el criterio BIC seleccionó una estructura parsimoniosa que revela detalles clave sobre la dinámica de la estación:
d=1, q=1): El modelo determinó que la serie requiere una diferenciación (d=1) para estabilizarse, confirmando que la altura “acumula” deformación en el tiempo. El término de media móvil (ma1 = -0.33) indica que el sistema corrige aproximadamente un tercio de los choques aleatorios de la semana anterior.east (\(0.2388\)) es estadísticamente significativo, ya que su valor es más del doble de su error estándar (\(0.1000\)).
north no resulta significativo (su error estándar de \(0.1762\) es casi el doble que su coeficiente).# 2. Búsqueda algorítmica del mejor modelo SARIMAX
# auto.arima evaluará decenas de modelos y elegirá el de menor criterio AICc
modelo_sarimax <- auto.arima(train_data$height,
xreg = xreg_train_sarimax,
seasonal = FALSE,
ic="bic"
) # Fourier ya controla la estacionalidad
# 3. Mostrar el resumen del mejor modelo encontrado
summary(modelo_sarimax)Series: train_data$height
Regression with ARIMA(0,1,1) errors
Coefficients:
ma1 east north sin_anual cos_anual
-0.3367 0.2388 -0.0939 0.0075 0.0007
s.e. 0.0626 0.1000 0.1762 0.0014 0.0013
sigma^2 = 9.416e-06: log likelihood = 1421.94
AIC=-2831.88 AICc=-2831.61 BIC=-2809.17
Training set error measures:
ME RMSE MAE MPE MAPE MASE
Training set -6.571751e-05 0.003040132 0.002338452 -Inf Inf 0.9303668
ACF1
Training set 0.02083822
Antes de proyectar hacia el futuro, verificamos que el modelo ARIMA(0,1,1) haya extraído toda la información útil de la serie.
Ljung-Box test
data: Residuals from Regression with ARIMA(0,1,1) errors
Q* = 14.005, df = 9, p-value = 0.1221
Model df: 1. Total lags used: 10
# IMPORTANTE: Usamos los datos de prueba que el modelo NUNCA ha visto
xreg_test_arimax <- cbind(
east = test_data$east,
north = test_data$north,
sin_anual = test_data$sin_anual,
cos_anual = test_data$cos_anual
)
pronostico_arimax <- forecast(modelo_sarimax, xreg = xreg_test_arimax)
rmse_arimax <- sqrt(mean((test_data$height - pronostico_arimax$mean)^2, na.rm = TRUE))
mae_arimax <- mean(abs(test_data$height - pronostico_arimax$mean), na.rm = TRUE)
df_grafica_arimax <- data.frame(
fecha = test_data$fecha,
Real = test_data$height,
Pronostico = as.numeric(pronostico_arimax$mean)
)Evaluamos el desempeño del modelo frente a datos desconocidos (periodo 2025), lo que permite medir su utilidad real como herramienta de alerta temprana.
ggplot(df_grafica_arimax, aes(x = fecha)) +
geom_line(aes(y = Real, color = "Altura Real"), linewidth = 1) +
geom_line(aes(y = Pronostico, color = "Pronóstico ARIMAX"), linewidth = 1, linetype = "dashed") +
scale_color_manual(values = c("Altura Real" = "gray40", "Pronóstico ARIMAX" = "blue")) +
theme_minimal() +
labs(title = "Validación Externa: Pronóstico ARIMAX (Out-of-Sample)",
subtitle = sprintf("Precisión: RMSE = %.5f m | MAE = %.5f m", rmse_arimax, mae_arimax),
y = "Desplazamiento Vertical (m)", x = "Fecha", color = "") +
theme(legend.position = "bottom", plot.title = element_text(face="bold"))En la geodinámica real, la estación Wanchaq no es un receptor pasivo; el desplazamiento de la corteza es un fenómeno tridimensional interconectado.
library(vars)
library(urca)
matriz_gnss <- cbind(
height = train_data$height,
east = train_data$east,
north = train_data$north
)
seleccion_lags <- VARselect(matriz_gnss, lag.max = 10, type = "const")
#cat("\n--- REZAGOS ÓPTIMOS SUGERIDOS ---\n")
#print(seleccion_lags$selection)
K_optimo <- max(seleccion_lags$selection["SC(n)"], 2)
cat("\nRezago (K) elegido para Johansen:", K_optimo, "\n")
Rezago (K) elegido para Johansen: 2
La prueba de Johansen determina cuántas “vías” de comunicación estables existen entre la altura y los desplazamientos horizontales.
Resultados del Test de Traza:
Para \(r=0\), el estadístico (\(67.15\)) supera ampliamente el valor crítico al 1% (\(41.07\)).
Para \(r \leq 1\) (\(32.87 > 24.60\)) y \(r \leq 2\) (\(13.53 > 12.97\)), también se rechaza la hipótesis nula con total significancia.
Conclusión : Existen 3 relaciones de cointegración. El sistema tiene rango completo.
Las tres coordenadas están integradas en un equilibrio dinámico. Cualquier movimiento en el plano horizontal (Este/Norte) genera un ajuste predecible y mandatorio en la vertical (Altura). La estación Wanchaq muestra una respuesta estructuralmente coherente.
Al despejar la altura para entender la cinemática geodésica: \[Height = -13.449 (East) + 4.771 (North) - 0.046\]
Este vector confirma la endogeneidad del sistema: los desplazamientos hacia el Este están asociados a un reajuste negativo en la vertical (hundimiento/subsidencia en este vector específico), mientras que los movimientos hacia el Norte se asocian positivamente.
# type = "trace": Usamos el estadístico de Traza (el más robusto)
# ecdet = "const": Asumimos intercepto pero sin tendencia en la ecuación (equivalente a tu Case 3)
prueba_johansen <- ca.jo(matriz_gnss, type = "trace", ecdet = "const", K = K_optimo)
cat("\n--- RESULTADOS DE LA PRUEBA DE JOHANSEN ---\n")
--- RESULTADOS DE LA PRUEBA DE JOHANSEN ---
######################
# Johansen-Procedure #
######################
Test type: trace statistic , without linear trend and constant in cointegration
Eigenvalues (lambda):
[1] 1.004103e-01 5.793508e-02 4.089887e-02 1.526557e-16
Values of teststatistic and critical values of test:
test 10pct 5pct 1pct
r <= 2 | 13.53 7.52 9.24 12.97
r <= 1 | 32.87 17.85 19.96 24.60
r = 0 | 67.15 32.00 34.91 41.07
Eigenvectors, normalised to first column:
(These are the cointegration relations)
height.l2 east.l2 north.l2 constant
height.l2 1.00000000 1.00000000 1.000000000 1.0000000
east.l2 13.44910793 2.06055219 -0.328190117 1.4013379
north.l2 -4.77158567 -0.21035438 0.277011525 4.9898976
constant 0.04643199 -0.01991509 0.003772574 -0.1468218
Weights W:
(This is the loading matrix)
height.l2 east.l2 north.l2 constant
height.d -0.013649914 -0.027329368 -0.044232290 9.113070e-18
east.d -0.007948208 -0.009530759 0.032242687 3.767177e-18
north.d 0.004251315 -0.011723117 0.004880814 1.320329e-18
Aunque la prueba de Traza sugiere estadísticamente un rango completo (\(r=3\)), este resultado es un artefacto matemático esperado al introducir series con órdenes de integración mixtos en la metodología clásica de Johansen (la cual asume homogeneidad I(1)). Dado que el enfoque ARDL ya demostró la existencia de una relación de equilibrio válida, se optó por imponer teóricamente un rango de cointegración de r = 1 en el modelo VECM.
# 1. Convertimos el test de Johansen en un modelo calculable
# Usamos r = 1 (asumiendo 1 relación principal de equilibrio a pesar del over-rejection)
modelo_vecm <- vec2var(prueba_johansen, r = 1)
# 2. Funciones de Impulso-Respuesta (IRF)
# Simulamos un "shock" en el Este y vemos cómo reacciona la Altura 20 periodos adelante
irf_este_altura <- irf(modelo_vecm, impulse = "east", response = "height",
n.ahead = 20, boot = TRUE)
plot(irf_este_altura, main = "Impacto en la Altura tras un 'Shock' hacia el Este")El modelo VECM (\(r=1\)) proyecta la evolución de la altura basándose en la inercia del sistema de corrección de errores.
El VECM genera una trayectoria de “retorno al equilibrio”. Se observa que la predicción (línea roja) actúa como un promedio de largo plazo, ignorando el ruido estacional de alta frecuencia.
# 3. Le pedimos al modelo que pronostique tantos pasos adelante como tenga tu test_data
prediccion_vecm <- predict(modelo_vecm, n.ahead = nrow(test_data))
# Extraemos exclusivamente los valores pronosticados para la Altura
pronostico_altura_vecm <- prediccion_vecm$fcst$height[, "fcst"]
# Calculamos el error final (RMSE y MAE)
rmse_vecm <- sqrt(mean((test_data$height - pronostico_altura_vecm)^2, na.rm = TRUE))
mae_vecm <- mean(abs(test_data$height - pronostico_altura_vecm), na.rm = TRUE)
# Construimos el dataframe para graficar
df_test_vecm <- data.frame(
fecha = fecha_test, # Usamos el mismo vector de fechas del test que en ARIMAX
Real = test_data$height,
Pronostico = pronostico_altura_vecm
)
# Generamos la gráfica con el mismo estilo para que coincida en tu póster
ggplot(df_test_vecm, aes(x = fecha)) +
geom_line(aes(y = Real, color = "Altura Real"), linewidth = 1) +
geom_line(aes(y = Pronostico, color = "Pronóstico VECM"), linewidth = 1, linetype = "dashed") +
# Usamos un color distinto (ej. darkred) para diferenciarlo del ARIMAX en el póster
scale_color_manual(values = c("Altura Real" = "gray40", "Pronóstico VECM" = "darkred")) +
theme_minimal() +
labs(title = "VECM: Predicción Tridimensional en Datos Nuevos (15%)",
subtitle = sprintf("Precisión de pronóstico: RMSE = %.5f m | MAE = %.5f m", rmse_vecm, mae_vecm),
y = "Desplazamiento Vertical (m)", x = "Fecha", color = "") +
theme(legend.position = "bottom", plot.title = element_text(face="bold"))Tras ajustar el modelo VECM (\(r=1\)), sometemos los residuos a un test de Portmanteau para verificar la presencia de información no capturada. El rechazo de la hipótesis nula en la prueba de Portmanteau (\(p < 0.05\)) y la presencia de autocorrelación en el correlograma no solo obedecen a las restricciones paramétricas del VECM, sino a factores geofísicos ampliamente documentados. Según Williams et al. (2004), las amplitudes del ruido blanco y del ruido de parpadeo (flicker noise) en series GNSS presentan una fuerte dependencia latitudinal, alcanzando máximos en la región ecuatorial y mostrando un sesgo de mayor ruido sistemático en el hemisferio sur (aproximadamente 2–4 mm adicionales de ruido de parpadeo).
Dado que la estación Wanchaq se encuentra en el hemisferio sur (latitud ~13.5° S), la autocorrelación observada en los residuales es la manifestación empírica y esperada de este ruido de color regional. Por lo tanto, el modelo VECM estructural es válido, y el ruido residual refleja fielmente las complejas condiciones atmosféricas y geodésicas de los Andes peruanos, más que un error de especificación del modelo.
Portmanteau Test (asymptotic)
data: Residuals of VAR object vecm_to_var
Chi-squared = 107.93, df = 75, p-value = 0.007641
$serial
Portmanteau Test (asymptotic)
data: Residuals of VAR object vecm_to_var
Chi-squared = 107.93, df = 75, p-value = 0.007641
Interpretación de la Competencia de Modelos
El gráfico de validación externa revela el comportamiento de cada arquitectura frente a la dinámica compleja de la estación Wanchaq:
ARDL: Es el único que logra capturar tanto la tendencia de caída como la variabilidad de corto plazo. Al tener una estructura de rezagos distribuida, reacciona más rápido a los cambios en el componente “Este”, manteniéndose casi siempre dentro del área sombreada de incertidumbre.
ARIMAX-Fourier: Capta perfectamente la estacionalidad, pero se “pierde” en la tendencia. Es un modelo excelente para promediar ciclos anuales, pero insuficiente para capturar aceleraciones tectónicas imprevistas.
VECM: Se comporta como un filtro de baja frecuencia. Al buscar el equilibrio de largo plazo, ignora el ruido y la estacionalidad, entregando una línea de tendencia pura. Es útil para estudios de deformación secular, pero pobre para alertas tempranas semanales.
v_real <- as.numeric(test_data$height)
v_ardl <- as.numeric(predicciones_test)
v_arimax <- as.numeric(pronostico_arimax$mean)
v_vecm <- as.numeric(pronostico_altura_vecm)
df_integrado <- data.frame(
fecha = test_data$fecha,
Real = v_real,
ARDL = v_ardl,
ARIMAX = v_arimax,
VECM = v_vecm,
ARDL_lwr = v_ardl - (1.96 * rmse_test),
ARDL_upr = v_ardl + (1.96 * rmse_test)
)
ggplot(df_integrado, aes(x = fecha)) +
geom_ribbon(aes(ymin = ARDL_lwr, ymax = ARDL_upr, fill = "Incertidumbre ARDL (95%)"), alpha = 0.15) +
geom_line(aes(y = Real, color = "Observado (Real)"), linewidth = 1.1) +
geom_line(aes(y = ARDL, color = "ARDL (Dinámico)"), linewidth = 0.9) +
geom_line(aes(y = ARIMAX, color = "ARIMAX-Fourier"), linewidth = 0.8, linetype = "dashed") +
geom_line(aes(y = VECM, color = "VECM (Sistémico)"), linewidth = 0.8, linetype = "dotdash") +
scale_color_manual(values = c(
"Observado (Real)" = "black",
"ARDL (Dinámico)" = "blue",
"ARIMAX-Fourier" = "darkgreen",
"VECM (Sistémico)" = "darkred"
)) +
scale_fill_manual(values = c("Incertidumbre ARDL (95%)" = "dodgerblue")) +
theme_minimal() +
labs(title = "Comparación de Modelos: Predicción de Altura Wanchaq",
subtitle = "Evaluación sobre el 15% de datos finales (no vistos)",
y = "Desplazamiento Vertical (m)", x = "Año",
color = "Modelos", fill = "") +
theme(legend.position = "bottom", plot.title = element_text(face="bold", size = 14))El NSE estandariza la evaluación midiendo si el modelo supera la varianza natural de los datos: * \(NSE = 1\): Predicción perfecta. * \(NSE > 0\): Predictor estadísticamente válido. * \(NSE \le 0\): Peor que usar el promedio histórico.
# Función para el Nash-Sutcliffe Efficiency
f_nse <- function(real, pred) {
1 - (sum((real - pred)^2, na.rm=T) / sum((real - mean(real, na.rm=T))^2, na.rm=T))
}
# Consolidación de la tabla
tabla_final <- data.frame(
Modelo = c("ARDL", "ARIMAX-Fourier", "VECM"),
RMSE_m = c(rmse_test, rmse_arimax, rmse_vecm),
MAE_m = c(mae_test, mae_arimax, mae_vecm),
NSE = c(f_nse(test_data$height, predicciones_test),
f_nse(test_data$height, as.numeric(pronostico_arimax$mean)),
f_nse(test_data$height, as.numeric(pronostico_altura_vecm)))
)
# Redondeo para estética académica
tabla_final[, 2:3] <- round(tabla_final[, 2:3], 5)
tabla_final[, 4] <- round(tabla_final[, 4], 4)
kableExtra::kable(tabla_final)| Modelo | RMSE_m | MAE_m | NSE |
|---|---|---|---|
| ARDL | 0.00570 | 0.00460 | 0.5437 |
| ARIMAX-Fourier | 0.01310 | 0.01055 | -1.4073 |
| VECM | 0.01726 | 0.01551 | -3.1762 |