Modelamiento y Pronóstico de Desplazamientos GNSS mediante Enfoques Univariantes y Multivariantes: Un Análisis Comparativo

ARDL, VECM y SARIMAX

Chullo Puclla Oscar | Colque Caillahua Percy Elbis

Universidad Nacional de San Antonio Abad del Cusco

Introducción

Estación CS01-WANCHAQ: movimiento del suelo en Cusco

¿Qué se mide?

Los receptores GNSS registran cada semana cómo se desplaza la corteza terrestre en tres ejes:

  • Norte (N): tendencia lineal creciente ≈ 10 mm/año por compresión tectónica
  • Este (E): movimiento irregular con ruido instrument al; comportamiento ambiguo en tests
  • Altura (H): oscilación anual causada por variaciones de carga hidriológica (precipitaciones Andes)

Preparación de datos

Imputación de datos

Se aplico el modelo SAITS para la impujtacion de datos de series temporales debiso que mostró un mejor ajuste en cuanto a los factores multivariados

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

Resultados

División de datos

Dado que los datos presentan una periodicidad semanal, elegir el porcentaje de división es importante para evaluar la capacidad predictiva de los modelos, es por ello que se optó por dividir la data de la siguiente manera:

  • 85%: Entrenamiento (Train)
  • 15%: Prueba (Test)
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("Tamaño del set de Entrenamiento (Train):", nrow(train_data), "observaciones\n")
Tamaño del set de Entrenamiento (Train): 326 observaciones
cat("Tamaño del set de Prueba (Test):", nrow(test_data), "observaciones\n")
Tamaño del set de Prueba (Test): 58 observaciones

¿Las series son estacionarias?

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

Test de raiz unitaria

# North
Value of test-statistic is: -2.507 8.9308 4.0031 

Critical values for test statistics: 
      1pct  5pct 10pct
tau3 -3.98 -3.42 -3.13
phi2  6.15  4.71  4.05
phi3  8.34  6.30  5.36
# East
Value of test-statistic is: -4.4704 6.8694 9.9959 

Critical values for test statistics: 
      1pct  5pct 10pct
tau3 -3.98 -3.42 -3.13
phi2  6.15  4.71  4.05
phi3  8.34  6.30  5.36
# Height
Value of test-statistic is: -3.0104 3.0819 4.5844 

Critical values for test statistics: 
      1pct  5pct 10pct
tau3 -3.98 -3.42 -3.13
phi2  6.15  4.71  4.05
phi3  8.34  6.30  5.36

\[H_0: \text{No es estacionario}\]

A un 5%:

  • North: \(-2.507>-3.42\). No se rechaza la \(H_0\).
  • East: \(-4.4704<-3.42\). Se rechaza la \(H_0\). Estacionaria en tendencia.
  • Height: \(-3.0104>-3.42\). No se rechaza la \(H_0\).

Primeras diferencias I(1)

ur.df(diff(train_data$north), type="drift") |> summary()
# North
Value of test-statistic is: -15.7488 124.0157 

Critical values for test statistics: 
      1pct  5pct 10pct
tau2 -3.44 -2.87 -2.57
phi1  6.47  4.61  3.79
ur.df(diff(train_data$height), type="drift") |> summary()
# Height
Value of test-statistic is: -14.4498 104.4034 

Critical values for test statistics: 
      1pct  5pct 10pct
tau2 -3.44 -2.87 -2.57
phi1  6.47  4.61  3.79

Resultados de Estacionariedad (Prueba ADF)

  1. Este: Estacionario en niveles \(\rightarrow I(0)\)
  2. Norte: Estacionario en primera diferencia \(\rightarrow I(1)\)
  3. Altura: Estacionario en primera diferencia \(\rightarrow I(1)\)

La prueba de cointegración de Johansen (base de los modelos VECM) exige de manera estricta que todas las variables compartan el mismo orden de integración, específicamente \(I(1)\). Dado que las coordenadas GNSS presentan órdenes mixtos, el enfoque ARDL (Autoregresivo de Rezagos Distribuidos) se establece como la metodología teóricamente correcta, ya que permite modelar relaciones a largo plazo combinando variables \(I(0)\) e \(I(1)\) sin perder información espacial.

Modelos de equilibrio

ARDL

El ARDL (Autoregresivo de Rezagos Distribuidos) es un modelo econométrico avanzado que permite estimar la dinámica temporal entre variables. A diferencia de los modelos clásicos, el ARDL se destaca por dos ventajas fundamentales:

  1. Flexibilidad Matemática: 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).
  2. Análisis Dual: 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.

Estacionalidad y seleccion del modelo ARDL

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]
# 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 (Filtro Físico)",
    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)
  )

Las variables de Fourier sin_anual y cos_anual están después de la barra | porque son variables “fijas” o deterministas, el modelo no les busca rezagos del pasado.

Best ARDL

# Búsqueda algorítmica del mejor ARDL (Criterio BIC)
seleccion_ardl <- auto_ardl(height ~ east + north | sin_anual + cos_anual, 
                            data = train_data, 
                            max_order = c(4, 4, 4), 
                            selection = "BIC")
mejor_ardl <- seleccion_ardl$best_model
summary(mejor_ardl)

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

Conclusiones del Modelo ARDL Óptimo

  • Estructura Seleccionada [ARDL (2, 1, 0)]: El criterio BIC determinó que la “memoria” geofísica del sistema es corta. La deformación vertical actual depende fuertemente de lo ocurrido hace solo 2 semanas, sin necesidad de mayor rezago temporal.
  • Alto Poder Explicativo: El modelo logra capturar y explicar el 88.21% (R-cuadrado Ajustado) de la variabilidad del desplazamiento vertical en la estación Wanchaq.
  • Validación de la Estacionalidad: Los términos trigonométricos de Fourier (Seno y Coseno) resultaron estadísticamente significativos (p < 0.05), comprobando matemáticamente la presencia de la carga hidrológica anual.
  • Dinámica Espacial: El hundimiento vertical reacciona drásticamente al movimiento tectónico hacia el Este (p < 0.01), pero es estadísticamente independiente de los desplazamientos hacia el Norte a corto plazo.

Cointegración

library(sandwich)
library(lmtest)
# 1. Prueba de Cointegración (Bounds Test de Pesaran)
prueba_cointegracion <- bounds_f_test(mejor_ardl, case = 3)
print(prueba_cointegracion)

    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 

La prueba de límites (Bounds Test) de Pesaran arrojó un estadístico \(F = 9.78\), siendo altísimamente significativo (\(p < 0.001\)). Esto rechaza de manera contundente la hipótesis de no-cointegración.

Esto confirma que la Altura y el desplazamiento horizontal (Este) no se mueven de manera aislada o aleatoria. Existe una fuerza que los “amarra” en el tiempo manteniendo un equilibrio a largo plazo (lo cual valida perfectamente la física del bloque tectónico sobre el que está Wanchaq).

Al existir cointegración comprobada, el modelo ARDL queda blindado. Se garantiza al jurado que la relación encontrada representa un fenómeno geofísico real y no una simple coincidencia estadística temporal.

Errores Robustos (HAC): Para garantizar la máxima rigurosidad, los multiplicadores a largo plazo y el modelo de corrección de errores (ECM) fueron estimados utilizando la matriz de varianzas-covarianzas de Newey-West, blindando los resultados contra cualquier autocorrelación o heterocedasticidad residual.

HAC

# 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

Los multiplicadores fueron estimados aplicando la matriz de varianzas-covarianzas de Newey-West (HAC). Esto asegura que los errores estándar están blindados contra la heterocedasticidad y la autocorrelación, haciendo que los valores p sean irrefutables.

El Impacto Tectónico del Este: Se comprobó que el desplazamiento hacia el Este es el principal motor de la deformación vertical a largo plazo (\(p < 0.05\)). El coeficiente estimado es de -1.29.

Tasa de hundimiento: Por cada unidad (ej. milímetro) que la corteza se desplaza permanentemente hacia el Este, la estación Wanchaq experimenta un hundimiento geológico a largo plazo de 1.29 unidades.

Independencia de la Coordenada Norte: De manera consistente con la dinámica a corto plazo, el desplazamiento hacia el Norte no ejerce una influencia estadísticamente significativa (\(p = 0.286\)) sobre la coordenada vertical en el equilibrio de largo plazo.

Aprendizaje ARDL

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

Alta Fidelidad de Aprendizaje: El modelo ARDL logra un seguimiento casi perfecto (línea azul punteada) de la trayectoria real de deformación (línea gris), validando visualmente el 88.21% de varianza explicada.

Captura Multiescalar: El ajuste demuestra que el algoritmo no solo aprendió la macro-tendencia tectónica general, sino que también es capaz de replicar con precisión los micro-picos y valles (la volatilidad a corto plazo y la estacionalidad).

Ausencia de Desfase Severo: A pesar de tener rezagos temporales (memoria de 2 semanas), el modelo no muestra un retraso visual significativo (lagging) respecto a la serie original, indicando una respuesta rápida a los cambios geofísicos.

Diagnostico de residuos

residuos_ardl <- residuals(mejor_ardl)
Box.test(residuos_ardl, type = "Ljung-Box", lag = 10)

    Box-Ljung test

data:  residuos_ardl
X-squared = 12.582, df = 10, p-value = 0.248
lmtest::bgtest(mejor_ardl)

    Breusch-Godfrey test for serial correlation of order up to 1

data:  mejor_ardl
LM test = 0.82109, df = 1, p-value = 0.3649

Predicción ARDL de la altura

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

Efectos de shocks

# 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
df_irf_ardl <- data.frame(Semana = 1:horizonte, Impacto = respuestas)

ggplot(df_irf_ardl, aes(x = Semana, y = Impacto)) +
  geom_line(color = "blue", linewidth = 1) +
  geom_point(color = "blue") +
  geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
  theme_minimal() +
  labs(title = "Respuesta Dinámica de la Altura ante un Shock en el Este (ARDL)",
       subtitle = "Muestra cómo el hundimiento se estabiliza tras el movimiento horizontal",
       x = "Semanas después del evento", y = "Impacto acumulado en Altura (m)")

ARIMAX

Modelo ARIMAX con Estacionalidad Modelada por Series de Fourier

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

# 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) # Fourier ya controla la estacionalidad

# 3. Mostrar el resumen del mejor modelo encontrado
summary(modelo_sarimax)
Series: train_data$height 
Regression with ARIMA(2,1,1) errors 

Coefficients:
         ar1     ar2      ma1    east    north  sin_anual  cos_anual
      0.5826  0.1577  -0.9252  0.2685  -0.1724     0.0074      6e-04
s.e.  0.0695  0.0622   0.0375  0.0997   0.1548     0.0010      9e-04

sigma^2 = 9.184e-06:  log likelihood = 1426.87
AIC=-2837.74   AICc=-2837.28   BIC=-2807.46

Training set error measures:
                        ME        RMSE         MAE  MPE MAPE      MASE
Training set -8.312616e-05 0.002993094 0.002332568 -Inf  Inf 0.9280259
                    ACF1
Training set 0.004296426

Prueba de residuos

# 1. Preparar la matriz XREG para el futuro (Test Data)
# 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)
)
checkresiduals(modelo_sarimax)


    Ljung-Box test

data:  Residuals from Regression with ARIMA(2,1,1) errors
Q* = 8.4763, df = 7, p-value = 0.2925

Model df: 3.   Total lags used: 10

Gráfica de pronosticos

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

VECM

Interdependencia Tectónica

En la realidad, la estación Wanchaq no solo “recibe” efectos. El movimiento de la placa sudamericana es un vector tridimensional. Un empuje tectónico no ocurre solo en el plano horizontal; por la elasticidad de la corteza, un desplazamiento al Este a menudo “obliga” a un reajuste vertical (subsidencia o levantamiento). El VECM permite que las tres variables sean “endógenas”, es decir, que todas dependan de todas, capturando la cinemática real del bloque.

Dado que en el ARDL confirmamos mediante el test de Pesaran que existe cointegración, el Teorema de Granger dicta que obligatoriamente existe una representación de corrección de errores (VECM). Mientras que el ARDL es una ecuación simplificada, el VECM es la forma estructural completa.

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 

Prueba de cointegración de Johansen

# 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 ---
summary(prueba_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

Construcción del modelo

# ==============================================================================
#  CONSTRUCCIÓN DEL MODELO Y SIMULACIÓN DE CHOQUES
# ==============================================================================
# 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)

# Al correr esto, R te abrirá una ventana con una gráfica física espectacular
plot(irf_este_altura, main = "Impacto en la Altura tras un 'Shock' hacia el Este")

# ==============================================================================
# FASE 5 (VECM): PREDICCIÓN DEL FUTURO Y CÁLCULO DEL RMSE
# ==============================================================================
# 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)
cat("RMSE VECM:", round(rmse_vecm, 5), "metros\n")
RMSE VECM: 0.01726 metros
cat("MAE VECM:", round(mae_vecm, 5), "metros\n")
MAE VECM: 0.01551 metros

Residuales


    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

Gráfica final

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

Cuadro comparativo de modelos

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

print(tabla_final)
          Modelo  RMSE_m   MAE_m     NSE
1           ARDL 0.00570 0.00460  0.5437
2 ARIMAX-Fourier 0.01500 0.01264 -2.1537
3           VECM 0.01726 0.01551 -3.1762