PARCIAL 2: Diagnóstico espacio–temporal 2024 de variables meteorológicas de la REDMET (México)

Author

Jairo Andrés Ángel, Sara Nathalia Reina

Contexto y fuente de datos

Este trabajo utiliza datos horarios del año 2024 provenientes de la BASE DE DATOS DE LA RED DE METEOROLOGÍA Y RADIACIÓN SOLAR (REDMET) de la Ciudad de México.
La consulta y descarga de información se realiza desde el portal oficial de SEDEMA CDMX:
https://www.aire.cdmx.gob.mx/default.php?opc=‘aKBi’

La REDMET integra estaciones automáticas con registros puntuales en el espacio y horarios en el tiempo, adecuados para análisis geoestadísticos y diagnósticos operativos.

Objetivo general

Entregar un diagnóstico espacio–temporal con resolución horaria para 2024 de las variables meteorológicas clave medidas por la REDMET, con el fin de soportar decisiones operativas (planeación de actividades en campo, evaluación de riesgos, control de calidad de datos y preparación de modelos predictivos).

Variables y unidades

  • Temperatura del aire (TMP) — grados Celsius (°C)
  • Humedad relativa (RH) — porcentaje (%)

Resolución temporal: series horarias con sellos de tiempo tipo dd/mm/aaaa hh:mm (p. ej., 01/01/2024 01:00, 01/01/2024 02:00, …).
Cobertura espacial: estaciones puntuales georreferenciadas en el territorio de la CDMX y áreas aledañas.

Planteamiento del problema

Para una operación eficiente, la empresa necesita cuantificar cómo varían en el espacio y a lo largo del tiempo las condiciones atmosféricas que inciden directamente en la seguridad, productividad y desempeño de sus procesos. En particular, se requiere:

  1. Integrar y depurar las series horarias 2024 (detección de faltantes/atípicos y estandarización de unidades).
  2. Localizar y mapear las estaciones, superponiéndolas al área de interés para identificar gradientes y zonas con condiciones atípicas.
  3. Caracterizar patrones temporales (ciclo diurno/semanal) y patrones espaciales básicos (mapas de valores y semivariograma exploratorio).
  4. Evaluar estacionariedad en media para habilitar modelos geoestadísticos sobre un proceso residual de media cero en etapas posteriores.

El enfoque parte de cortes horarios representativos para validar la metodología y luego escala a todo 2024 con rutinas reproducibles (agregaciones diarias/mensuales y tableros/figuras de soporte).

Mapa de estaciones

knitr::include_graphics("Img/Rplot01.png")
Figure 1: Mapa de estaciones

# --- Lectura ---
dat_raw <- read_delim(path_datos, delim = ";", locale = locale(encoding = "ISO-8859-1"),show_col_types = FALSE) |> clean_names()

est_raw <- read_csv(path_estaciones, locale = locale(encoding = "ISO-8859-1"), show_col_types = FALSE) |> clean_names()

# --- Tipos + fecha/hora robusta ---
dat <- dat_raw |>
  mutate(
    id_station   = as.character(id_station),
    id_parameter = as.character(id_parameter),

    # --- Normalizar formato "DD/MM/YYYY H:MM" → "DD/MM/YYYY HH:MM"
    date_clean = date |>
      stringr::str_replace("^([0-9]{2}/[0-9]{2}/[0-9]{4}) ([0-9]):",
                           "\\1 0\\2:"),

    # --- Ahora sí se puede parsear correctamente
    datetime = lubridate::dmy_hm(date_clean, tz = TZ),

    date = as.Date(datetime)
  )
Warning: There was 1 warning in `mutate()`.
ℹ In argument: `datetime = lubridate::dmy_hm(date_clean, tz = TZ)`.
Caused by warning:
!  40880 failed to parse.
est <- est_raw |>
  mutate(
    cve_estac = as.character(cve_estac),
    epsg      = as.integer(epsg)
  )

# --- Merge ---
datos_merged <- inner_join(dat, est, by = c("id_station" = "cve_estac"))

epsg_crs <- unique(na.omit(datos_merged$epsg))[1]

datos_sf <- st_as_sf(datos_merged,
                     coords=c("easting","northing"),
                     crs=epsg_crs, remove=FALSE)

instante_obj <- ymd_hm(paste(dia_obj,hora_texto), tz=TZ)

datos_instante <- datos_sf |>
  filter(date == dia_obj,
         floor_date(datetime, "hour") == instante_obj)

datos_instante |> 
  dplyr::select(datetime, id_station, id_parameter, value, unit, nom_estac, easting, northing)
Simple feature collection with 112 features and 8 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 460020.4 ymin: 2117907 xmax: 511970 ymax: 2180751
Projected CRS: WGS 84 / UTM zone 14N
# A tibble: 112 × 9
   datetime            id_station id_parameter value  unit nom_estac    easting
   <dttm>              <chr>      <chr>        <dbl> <dbl> <chr>          <dbl>
 1 2024-11-25 12:00:00 ACO        RH            21       6 Acolman      509226.
 2 2024-11-25 12:00:00 ACO        TMP           18.3     5 Acolman      509226.
 3 2024-11-25 12:00:00 ACO        WDR          272       4 Acolman      509226.
 4 2024-11-25 12:00:00 ACO        WSP            3.1     3 Acolman      509226.
 5 2024-11-25 12:00:00 AJM        RH            14       6 Ajusco Medio 478171.
 6 2024-11-25 12:00:00 AJM        TMP           19.3     5 Ajusco Medio 478171.
 7 2024-11-25 12:00:00 AJM        WDR           NA       4 Ajusco Medio 478171.
 8 2024-11-25 12:00:00 AJM        WSP           NA       3 Ajusco Medio 478171.
 9 2024-11-25 12:00:00 AJU        RH            17       6 Ajusco       482901.
10 2024-11-25 12:00:00 AJU        TMP           15.9     5 Ajusco       482901.
# ℹ 102 more rows
# ℹ 2 more variables: northing <dbl>, geometry <POINT [m]>
# --- Vista rápida (top 10) ---
knitr::kable(
  datos_instante |>
    st_drop_geometry() |>
    dplyr::select(datetime, id_station, id_parameter, value, unit, nom_estac, easting, northing) |>
    slice_head(n = 10),
  caption = paste("REDMET —", format(instante_obj, "%Y-%m-%d %H:%M"), TZ)
)
REDMET — 2024-11-25 12:00 America/Mexico_City
datetime id_station id_parameter value unit nom_estac easting northing
2024-11-25 12:00:00 ACO RH 21.0 6 Acolman 509226.0 2171149
2024-11-25 12:00:00 ACO TMP 18.3 5 Acolman 509226.0 2171149
2024-11-25 12:00:00 ACO WDR 272.0 4 Acolman 509226.0 2171149
2024-11-25 12:00:00 ACO WSP 3.1 3 Acolman 509226.0 2171149
2024-11-25 12:00:00 AJM RH 14.0 6 Ajusco Medio 478170.7 2130955
2024-11-25 12:00:00 AJM TMP 19.3 5 Ajusco Medio 478170.7 2130955
2024-11-25 12:00:00 AJM WDR NA 4 Ajusco Medio 478170.7 2130955
2024-11-25 12:00:00 AJM WSP NA 3 Ajusco Medio 478170.7 2130955
2024-11-25 12:00:00 AJU RH 17.0 6 Ajusco 482901.0 2117907
2024-11-25 12:00:00 AJU TMP 15.9 5 Ajusco 482901.0 2117907

1. Análisis geoestadístico univariado y bivariado de al menos dos variables espaciales:

1.1 Análisis y modelamiento de cada una de las medias.

Gráficos Exploratorios:

# --- Pares (x, y, variable) para un parámetro ---
pairs_param <- function(df, param, etiqueta) {
  d <- df |>
    sf::st_drop_geometry() |>
    dplyr::filter(id_parameter == param, !is.na(value)) |>
    dplyr::select(easting, northing, value)

  if (nrow(d) == 0) {
    message("Sin datos para ", param); return(invisible(NULL))
  }

  # Etiquetas en los paneles (x, y, nombre de la variable)
  pairs(~ easting + northing + value, data = d,
        labels = c("x", "y", etiqueta),
        pch = 1)
}

# --- Llamadas (una figura por variable) ---
pairs_param(datos_instante, "TMP", "Temperatura (°C)")

pairs_param(datos_instante, "RH",  "Humedad relativa (%)")

Temperatura (TMP): En la primera figura no se aprecia una tendencia espacial marcada para la temperatura. No obstante, se observan valores claramente periféricos que podrían influir de forma desproporcionada en los ajustes. Por prudencia, se repite la exploración excluyéndolas y se comparan los resultados.

Humedad relativa (RH): En el caso de la humedad relativa se sugiere la presencia de una tendencia positiva: los valores tienden a aumentar a medida que cambia la posición espacial. Esto amerita modelar y/o remover la tendencia antes de ajustar el variograma.

Detección y remoción de outliers

calc_outliers <- function(df_var) {
  vals <- df_var$value
  Q1 <- quantile(vals, 0.25, na.rm = TRUE)
  Q3 <- quantile(vals, 0.75, na.rm = TRUE)
  IQR <- Q3 - Q1
  lower <- Q1 - 1.5 * IQR
  upper <- Q3 + 1.5 * IQR
  df_var |> filter(value < lower | value > upper) |> pull(id_station)
}

# --- TMP ---
df_tmp <- datos_instante |>
  filter(id_parameter == "TMP") |>
  st_drop_geometry() |>
  mutate(value = as.numeric(value))

ids_tmp_out <- calc_outliers(df_tmp)
cat("Outliers TMP:", ifelse(length(ids_tmp_out)==0, "ninguno", paste(ids_tmp_out, collapse=", ")), "\n")
Outliers TMP: AJU, FAC, INN 
datos_sin_tmp <- datos_instante |> filter(!(id_station %in% ids_tmp_out))

# --- RH ---
df_rh <- datos_sin_tmp |>
  filter(id_parameter == "RH") |>
  st_drop_geometry() |>
  mutate(value = as.numeric(value))

ids_rh_out <- calc_outliers(df_rh)
cat("Outliers RH:", ifelse(length(ids_rh_out)==0, "ninguno", paste(ids_rh_out, collapse=", ")), "\n")
Outliers RH: ninguno 
datos_limpios <- datos_sin_tmp |> filter(!(id_station %in% ids_rh_out))

# --- Gráficos finales ---
pairs_param(datos_limpios, "TMP", "Temperatura (°C)")

pairs_param(datos_limpios, "RH",  "Humedad relativa (%)")

A partir de la evaluación gráfica inicial y del criterio intercuartílico (IQR), se quitaron las estaciones AJU, FAC e INN las cuales presentaban mediciones anomalas para la temperatura. En el caso de laTemperatura (TMP) los puntos parecen bastante dispersos y sin un patrón evidente, lo cual sugiere que quitar las estaciones extremas eliminó los posibles outliers que distorsionaban la distribución. Esto indica que el supuesto de estacionariedad (media constante en el espacio) puede ser razonable para continuar con el análisis geoestadístico.

En la Humedad relativa (RH) aunque sigue habiendo cierta dispersión, no se observa ya una tendencia fuerte. La dispersión es mayor que en TMP, pero no concentrada en un solo sector del área, lo cual también sugiere ausencia de tendencia clara. Esto permite asumir que la variabilidad de la humedad es más local que global, por lo que el variograma debería capturar estructuras de dependencia a pequeña escala.

Modelamiento de la media

## --- función para preparar un df por variable ---
prep_df_utm <- function(datos,param){
  datos |> filter(id_parameter==param) |> st_drop_geometry() |>
    transmute(
      x = easting,
      y = northing,
      z = as.numeric(value)
    ) |> filter(!is.na(z))
}

df_tmp <- prep_df_utm(datos_limpios,"TMP")
df_rh  <- prep_df_utm(datos_limpios,"RH")

1. Temperatura (TMP)

# Modelos para la media
m0_tmp <- lm(z ~ 1, data = df_tmp)
m1_tmp <- lm(z ~ x + y, data = df_tmp)
m2_tmp <- lm(z ~ x + y + I(x^2) + I(y^2) + I(x*y), data = df_tmp)

summary(m0_tmp)

Call:
lm(formula = z ~ 1, data = df_tmp)

Residuals:
     Min       1Q   Median       3Q      Max 
-0.95333 -0.15333 -0.05333  0.34667  0.84667 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)   19.253      0.139   138.5   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.5383 on 14 degrees of freedom
summary(m1_tmp)

Call:
lm(formula = z ~ x + y, data = df_tmp)

Residuals:
    Min      1Q  Median      3Q     Max 
-0.5471 -0.3917 -0.1247  0.3596  0.7757 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)   
(Intercept)  6.543e+01  1.685e+01   3.883  0.00218 **
x           -6.306e-06  1.186e-05  -0.532  0.60449   
y           -2.004e-05  7.907e-06  -2.534  0.02623 * 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.4558 on 12 degrees of freedom
Multiple R-squared:  0.3854,    Adjusted R-squared:  0.283 
F-statistic: 3.763 on 2 and 12 DF,  p-value: 0.05388
summary(m2_tmp)

Call:
lm(formula = z ~ x + y + I(x^2) + I(y^2) + I(x * y), data = df_tmp)

Residuals:
    Min      1Q  Median      3Q     Max 
-0.5760 -0.2657 -0.1323  0.4042  0.5621 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)
(Intercept) -4.346e+03  2.985e+03  -1.456    0.179
x            4.870e-04  1.734e-03   0.281    0.785
y            3.965e-03  2.633e-03   1.506    0.166
I(x^2)      -8.188e-11  1.302e-09  -0.063    0.951
I(y^2)      -9.028e-10  5.651e-10  -1.598    0.145
I(x * y)    -1.944e-10  8.953e-10  -0.217    0.833

Residual standard error: 0.4563 on 9 degrees of freedom
Multiple R-squared:  0.5381,    Adjusted R-squared:  0.2815 
F-statistic: 2.097 on 5 and 9 DF,  p-value: 0.1578
anova(m0_tmp, m1_tmp, m2_tmp)
Analysis of Variance Table

Model 1: z ~ 1
Model 2: z ~ x + y
Model 3: z ~ x + y + I(x^2) + I(y^2) + I(x * y)
  Res.Df    RSS Df Sum of Sq      F Pr(>F)  
1     14 4.0573                             
2     12 2.4935  2   1.56386 3.7549 0.0652 .
3      9 1.8742  3   0.61929 0.9913 0.4398  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Se evaluaron tres modelos para la media espacial:

  • m0: modelo sin tendencia
  • m1: tendencia lineal en ((x, y))
  • m2: tendencia cuadrática en ((x, y))

La comparación mediante ANOVA mostró que el modelo lineal no mejora significativamente al modelo sin tendencia ((p = 0.065)).
Asimismo, los términos cuadráticos incluidos en (m2) no aportaron una mejora estadística respecto a (m1) ((p = 0.44)).

Aunque dentro del modelo lineal el coeficiente asociado a la coordenada (y) aparece marginalmente significativo, el efecto global del modelo completo no supera al de (m0). Esto indica que, tras la eliminación de las estaciones con valores atípicos (AJU, FAC, INN), no se observa un gradiente espacial relevante en la temperatura para el instante analizado.

En consecuencia, se selecciona el modelo sin tendencia (m0) y se asume estacionariedad en la media para TMP.

RESIDUOS PARA EL VARIOGRAMA

df_tmp$resid <- residuals(m0_tmp)
df_tmp$resid <- df_tmp$resid - mean(df_tmp$resid, na.rm = TRUE)  # centrar exacto en 0

2. Humedad relativa (RH)

m0_rh <- lm(z ~ 1, data = df_rh)
m1_rh <- lm(z ~ x + y, data = df_rh)
m2_rh <- lm(z ~ x + y + I(x^2) + I(y^2) + I(x*y), data = df_rh)

summary(m0_rh)

Call:
lm(formula = z ~ 1, data = df_rh)

Residuals:
    Min      1Q  Median      3Q     Max 
-6.8889 -2.6389 -0.8889  3.6111  7.1111 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  20.8889     0.9249   22.58 4.07e-14 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 3.924 on 17 degrees of freedom
summary(m1_rh)

Call:
lm(formula = z ~ x + y, data = df_rh)

Residuals:
    Min      1Q  Median      3Q     Max 
-4.7402 -2.5088  0.1192  2.2854  7.0244 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)  
(Intercept) -2.376e+02  1.260e+02  -1.886   0.0788 .
x            1.788e-04  7.552e-05   2.367   0.0318 *
y            7.937e-05  5.662e-05   1.402   0.1813  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 3.388 on 15 degrees of freedom
Multiple R-squared:  0.3424,    Adjusted R-squared:  0.2547 
F-statistic: 3.905 on 2 and 15 DF,  p-value: 0.04314
summary(m2_rh)

Call:
lm(formula = z ~ x + y + I(x^2) + I(y^2) + I(x * y), data = df_rh)

Residuals:
    Min      1Q  Median      3Q     Max 
-4.8526 -1.3401  0.2186  1.5106  5.2337 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)  
(Intercept) -2.335e+04  2.163e+04  -1.079    0.302  
x            2.225e-02  1.128e-02   1.973    0.072 .
y            1.651e-02  1.976e-02   0.835    0.420  
I(x^2)      -1.050e-08  7.785e-09  -1.349    0.202  
I(y^2)      -3.194e-09  4.356e-09  -0.733    0.477  
I(x * y)    -5.447e-09  4.575e-09  -1.191    0.257  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 3.13 on 12 degrees of freedom
Multiple R-squared:  0.5509,    Adjusted R-squared:  0.3637 
F-statistic: 2.944 on 5 and 12 DF,  p-value: 0.05822
anova(m0_rh, m1_rh, m2_rh)
Analysis of Variance Table

Model 1: z ~ 1
Model 2: z ~ x + y
Model 3: z ~ x + y + I(x^2) + I(y^2) + I(x * y)
  Res.Df    RSS Df Sum of Sq      F  Pr(>F)  
1     17 261.78                              
2     15 172.15  2    89.625 4.5738 0.03338 *
3     12 117.57  3    54.582 1.8570 0.19069  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

La comparación mediante ANOVA mostró que el modelo lineal mejora significativamente al modelo sin tendencia ((p = 0.033)), indicando la presencia de un gradiente espacial suave en la humedad relativa.

Por otro lado, la inclusión de términos cuadráticos en (m2) no aportó una mejora estadística respecto al modelo lineal ((p = 0.19)), y ninguno de los términos no lineales resultó significativo.

En consecuencia, se selecciona el modelo lineal (m1) para describir la media espacial de RH.

RESIDUOS PARA EL VARIOGRAMA

df_rh$resid <- residuals(m1_rh)
df_rh$resid <- df_rh$resid - mean(df_rh$resid, na.rm = TRUE)

1.2 Selección de los semivariogramas empíricos individuales, más adecuados para la estimación del modelo teórico

Variograma cloud

# variograma cloud de residuales
vg_TMP_cloud <- variogram(resid ~ 1, ~ x + y, data = df_tmp, cloud = TRUE)
vg_RH_cloud  <- variogram(resid ~ 1, ~ x + y, data = df_rh,  cloud = TRUE)

par(mfrow = c(1,2))
plot(vg_TMP_cloud, main = "TMP — Cloud (residuales)")

plot(vg_RH_cloud,  main = "RH — Cloud (residuales)")

par(mfrow = c(1,1))

El variograma cloud de TMP muestra una nube compacta, sin valores atípicos ni semivarianzas extremas. La variación entre pares es baja y estable, indicando que el cálculo del variograma clásico es adecuado.

En contraste, el variograma cloud de RH presenta una dispersión mucho mayor, con semivarianzas que alcanzan valores altos incluso a distancias intermedias. Esto refleja variabilidad local alta y posibles pares influyentes. Por ello, resulta pertinente complementar el análisis con un estimador robusto.

Semivariograma Clásico (binned)

variogram_binning <- function(df, prop_cutoff = 0.6, nbins = 15) {
  Dmax   <- max(dist(df[, c("x", "y")]))
  cutoff <- prop_cutoff * Dmax
  width  <- cutoff / nbins
  list(cutoff = cutoff, width = width)
}

p_tmp <- variogram_binning(df_tmp)
p_rh  <- variogram_binning(df_rh)

# Binned variogram (clásico)
vg_TMP <- variogram(resid ~ 1, ~ x + y, data = df_tmp,
                    cutoff = p_tmp$cutoff, width = p_tmp$width)

vg_RH  <- variogram(resid ~ 1, ~ x + y, data = df_rh,
                    cutoff = p_rh$cutoff,  width = p_rh$width)

par(mfrow = c(1,2))
plot(vg_TMP, main = "TMP — Semivariograma clásico")

plot(vg_RH,  main = "RH — Semivariograma clásico")

par(mfrow = c(1,1))

El semivariograma clásico de TMP muestra una estructura espacial suave, con semivarianzas muy bajas. Esto confirma que la temperatura es un proceso relativamente estable, sin valores extremos.

En contraste, el semivariograma clásico de RH (calculado sobre los residuales del modelo lineal) revela una dependencia espacial más marcada y una variabilidad considerablemente mayor. La forma general sigue siendo creciente, aunque con mayor dispersión, lo que refleja la naturaleza más heterogénea de la humedad relativa.

Semivariograma resistente a datos atípicos

vg_TMP_rob <- variogram(resid ~ 1, ~ x + y, data = df_tmp,
                        cutoff = p_tmp$cutoff, width = p_tmp$width,
                        cressie = TRUE)

vg_RH_rob  <- variogram(resid ~ 1, ~ x + y, data = df_rh,
                        cutoff = p_rh$cutoff,  width = p_rh$width,
                        cressie = TRUE)

par(mfrow = c(1,2))
plot(vg_TMP_rob, main = "TMP — Semivariograma robusto")

plot(vg_RH_rob,  main = "RH — Semivariograma robusto")

par(mfrow = c(1,1))

En el caso de TMP, el semivariograma robusto (Cressie–Hawkins) presenta una forma casi idéntica al clásico. Esto confirma la ausencia de valores atípicos influyentes y la estabilidad de la estructura espacial de segundo orden. Por tanto, el estimador clásico es suficiente para ajustar un modelo teórico.

Para RH, el variograma robusto suaviza la variabilidad inicial y atenúa la influencia de pares de observaciones con diferencias extremas. Esto produce una estructura espacial más clara, con un aumento más regular de la semivarianza con la distancia. Debido a la mayor heterogeneidad local de la humedad relativa, el estimador robusto resulta más adecuado para la selección del modelo teórico.

1.3 Estimación de los semivariogramas individuales teóricos a sentimiento

El ajuste visual del semivariograma para la temperatura (TMP) se realizó mediante la herramienta eyefit() del paquete geoR.

El modelo que mejor representó la forma del variograma empírico fue esférico.

Parámetros seleccionados (aprox.):

  • Nugget ((^2)) ≈ 0.10
  • Sill parcial ((^2)) ≈ 0.215
  • Rango (()) ≈ 18 km
knitr::include_graphics("Img/Semivariograma_tem.png")

El modelo esférico captura razonablemente el crecimiento inicial del variograma y alcanza un nivel cercano a la meseta alrededor de 18 km, consistente con la estructura espacial suave observada para TMP. La presencia de un nugget pequeño concuerda con una medición relativamente estable y sin discontinuidades fuertes entre estaciones cercanas.

Para la humedad relativa (RH), el semivariograma presenta una mayor dispersión y valores altos de semivarianza. El modelo que mejor representó la forma creciente y cóncava del variograma empírico fue el exponencial.

Parámetros seleccionados (aprox.):

  • Nugget ((^2)) ≈ 1.85
  • Sill parcial ((^2)) ≈ 14.7
  • Rango (()) ≈ 11.5 km
knitr::include_graphics("img/Semivariograma_RH.png")

El modelo exponencial refleja de manera adecuada la rápida subida inicial del variograma y la aproximación asintótica hacia la meseta. El nugget relativamente grande es coherente con la alta variabilidad local detectada en RH, incluso entre estaciones cercanas.

##1.4. Estimación de los semivariogramas individuales y del modelo teórico usando MCO, MCP, MV, CL.

Vamos a usar geor:

# --- Geodata usando los residuales ---
geo_TMP <- as.geodata(df_tmp,
                      coords.col = c("x", "y"),
                      data.col   = "resid")

geo_RH <- as.geodata(df_rh,
                     coords.col = c("x", "y"),
                     data.col   = "resid"
                     )

var_TMP <- variog(geo_TMP, max.dist = p_tmp$cutoff)
variog: computing omnidirectional variogram
var_RH  <- variog(geo_RH,  max.dist = p_rh$cutoff)
variog: computing omnidirectional variogram
plot(var_TMP, main = "TMP — Variograma empírico (geoR)")

plot(var_RH,  main = "RH — Variograma empírico (geoR)")

Parámetros iniciales según lo eyefit:

# ---- TMP (modelo esférico) ----
ini_TMP <- c(sigmasq = 0.215, phi = 18000)
nug_TMP <- 0.10

# ---- RH (modelo exponencial) ----
ini_RH  <- c(sigmasq = 14.7, phi = 11500)
nug_RH  <- 1.85

Temperatura:

# ---- TMP: MCO ----
fit_TMP_MCO <- variofit(
  vario = var_TMP,
  ini.cov.pars = ini_TMP,
  cov.model = "spherical",
  fix.nugget = TRUE,
  nugget = nug_TMP,
  weights = "equal"
)
variofit: covariance model used is spherical 
variofit: weights used: equal 
variofit: minimisation function used: optim 
# ---- TMP: MCP (npairs) ----
fit_TMP_npairs <- variofit(
  vario = var_TMP,
  ini.cov.pars = ini_TMP,
  cov.model = "spherical",
  fix.nugget = TRUE,
  nugget = nug_TMP,
  weights = "npairs"
)
variofit: covariance model used is spherical 
variofit: weights used: npairs 
variofit: minimisation function used: optim 
# ---- TMP: MCP (Cressie) ----
fit_TMP_cressie <- variofit(
  vario = var_TMP,
  ini.cov.pars = ini_TMP,
  cov.model = "spherical",
  fix.nugget = TRUE,
  nugget = nug_TMP,
  weights = "cressie"
)
variofit: covariance model used is spherical 
variofit: weights used: cressie 
variofit: minimisation function used: optim 
# ---- TMP: Máxima verosimilitud (ML) ----
fit_TMP_ML <- likfit(
  geodata = geo_TMP,
  coords = geo_TMP$coords,
  data = geo_TMP$data,
  ini.cov.pars = ini_TMP,
  fix.nugget = TRUE,
  nugget = nug_TMP,
  cov.model = "spherical",
  lik.method = "ML"
)
kappa not used for the spherical correlation function
---------------------------------------------------------------
likfit: likelihood maximisation using the function optim.
likfit: Use control() to pass additional
         arguments for the maximisation function.
        For further details see documentation for optim.
likfit: It is highly advisable to run this function several
        times with different initial values for the parameters.
likfit: WARNING: This step can be time demanding!
---------------------------------------------------------------
likfit: end of numerical maximisation.
# ---- TMP: REML ----
fit_TMP_REML <- likfit(
  geodata = geo_TMP,
  coords = geo_TMP$coords,
  data = geo_TMP$data,
  ini.cov.pars = ini_TMP,
  fix.nugget = TRUE,
  nugget = nug_TMP,
  cov.model = "spherical",
  lik.method = "REML"
)
kappa not used for the spherical correlation function
---------------------------------------------------------------
likfit: likelihood maximisation using the function optim.
likfit: Use control() to pass additional
         arguments for the maximisation function.
        For further details see documentation for optim.
likfit: It is highly advisable to run this function several
        times with different initial values for the parameters.
likfit: WARNING: This step can be time demanding!
---------------------------------------------------------------
likfit: end of numerical maximisation.
plot(var_TMP$u, var_TMP$v,
     xlab = "Distancia (h)",
     ylab = "Semivarianza",
     main = "TMP — Ajustes del semivariograma")

lines(fit_TMP_MCO,      col = 1, lwd = 2)
lines(fit_TMP_npairs,   col = 2, lwd = 2)
lines(fit_TMP_cressie,  col = 3, lwd = 2)
lines(fit_TMP_ML,       col = 4, lwd = 2)
lines(fit_TMP_REML,     col = 5, lwd = 2)

legend(
  "bottomright",
  legend = c("MCO", "MCP npairs", "MCP Cressie", "ML", "REML"),
  col = 1:5,
  lwd = 2
)

# ---- RH: MCO ----
fit_RH_MCO <- variofit(
  vario = var_RH,
  ini.cov.pars = ini_RH,
  cov.model = "exponential",
  fix.nugget = TRUE,
  nugget = nug_RH,
  weights = "equal"
)
variofit: covariance model used is exponential 
variofit: weights used: equal 
variofit: minimisation function used: optim 
# ---- RH: MCP (npairs) ----
fit_RH_npairs <- variofit(
  vario = var_RH,
  ini.cov.pars = ini_RH,
  cov.model = "exponential",
  fix.nugget = TRUE,
  nugget = nug_RH,
  weights = "npairs"
)
variofit: covariance model used is exponential 
variofit: weights used: npairs 
variofit: minimisation function used: optim 
# ---- RH: MCP (Cressie) ----
fit_RH_cressie <- variofit(
  vario = var_RH,
  ini.cov.pars = ini_RH,
  cov.model = "exponential",
  fix.nugget = TRUE,
  nugget = nug_RH,
  weights = "cressie"
)
variofit: covariance model used is exponential 
variofit: weights used: cressie 
variofit: minimisation function used: optim 
# ---- RH: Máxima verosimilitud (ML) ----
fit_RH_ML <- likfit(
  geodata = geo_RH,
  coords = geo_RH$coords,
  data = geo_RH$data,
  ini.cov.pars = ini_RH,
  fix.nugget = TRUE,
  nugget = nug_RH,
  cov.model = "exponential",
  lik.method = "ML"
)
kappa not used for the exponential correlation function
---------------------------------------------------------------
likfit: likelihood maximisation using the function optim.
likfit: Use control() to pass additional
         arguments for the maximisation function.
        For further details see documentation for optim.
likfit: It is highly advisable to run this function several
        times with different initial values for the parameters.
likfit: WARNING: This step can be time demanding!
---------------------------------------------------------------
likfit: end of numerical maximisation.
# ---- RH: REML ----
fit_RH_REML <- likfit(
  geodata = geo_RH,
  coords = geo_RH$coords,
  data = geo_RH$data,
  ini.cov.pars = ini_RH,
  fix.nugget = TRUE,
  nugget = nug_RH,
  cov.model = "exponential",
  lik.method = "REML"
)
kappa not used for the exponential correlation function
---------------------------------------------------------------
likfit: likelihood maximisation using the function optim.
likfit: Use control() to pass additional
         arguments for the maximisation function.
        For further details see documentation for optim.
likfit: It is highly advisable to run this function several
        times with different initial values for the parameters.
likfit: WARNING: This step can be time demanding!
---------------------------------------------------------------
likfit: end of numerical maximisation.
plot(var_RH$u, var_RH$v,
     xlab = "Distancia (h)",
     ylab = "Semivarianza",
     main = "RH — Ajustes del semivariograma")

lines(fit_RH_MCO,      col = 1, lwd = 2)
lines(fit_RH_npairs,   col = 2, lwd = 2)
lines(fit_RH_cressie,  col = 3, lwd = 2)
lines(fit_RH_ML,       col = 4, lwd = 2)
lines(fit_RH_REML,     col = 5, lwd = 2)

legend(
  "bottomright",
  legend = c("MCO", "MCP npairs", "MCP Cressie", "ML", "REML"),
  col = 1:5,
  lwd = 2
)

Los semivariogramas empíricos presentan alta dispersión, sin evidenciar una silla clara ni una tendencia monótonamente creciente. Esto sugiere que la estructura espacial a la hora analizada es débil o no estacionaria. Por esta razón, los modelos paramétricos (esférico y exponencial) solo logran ajustes aproximados y no necesariamente representan fielmente la variabilidad real.

Semivariogramas recortados:

# diámetro espacial total
Dmax_tmp <- max(dist(df_tmp[, c("x", "y")]))
Dmax_rh  <- max(dist(df_rh[,  c("x", "y")]))

Dmax_tmp; Dmax_rh
[1] 56218.04
[1] 60107.79
cut_tmp <- 0.40 * Dmax_tmp
cut_rh  <- 0.40 * Dmax_rh

# Recalcular variogramas recortados en geoR
var_TMP <- variog(
  geo_TMP,
  max.dist = cut_tmp
)
variog: computing omnidirectional variogram
var_RH  <- variog(
  geo_RH,
  max.dist = cut_rh
)
variog: computing omnidirectional variogram
par(mfrow = c(1,2))
plot(var_TMP, main = "TMP — Variograma recortado")
plot(var_RH,  main = "RH — Variograma recortado")

par(mfrow = c(1,1))
# --- Parámetros iniciales TMP (tomados del eyefit) ---
ini_TMP <- c(sigmasq = 0.215, phi = 18000)
nug_TMP <- 0.10   # nugget fijo

# -------------------------
# 1. MCO (OLS)
# -------------------------
fit_TMP_MCO_REC <- variofit(
  vario = var_TMP,
  ini.cov.pars = ini_TMP,
  cov.model = "spherical",
  weights = "equal",
  fix.nugget = TRUE,
  nugget = nug_TMP
)
variofit: covariance model used is spherical 
variofit: weights used: equal 
variofit: minimisation function used: optim 
# -------------------------
# 2. MCP (WLS - npairs)
# -------------------------
fit_TMP_npairs_REC <- variofit(
  vario = var_TMP,
  ini.cov.pars = ini_TMP,
  cov.model = "spherical",
  weights = "npairs",
  fix.nugget = TRUE,
  nugget = nug_TMP
)
variofit: covariance model used is spherical 
variofit: weights used: npairs 
variofit: minimisation function used: optim 
# -------------------------
# 3. MCP (WLS - Cressie)
# -------------------------
fit_TMP_cressie_REC <- variofit(
  vario = var_TMP,
  ini.cov.pars = ini_TMP,
  cov.model = "spherical",
  weights = "cressie",
  fix.nugget = TRUE,
  nugget = nug_TMP
)
variofit: covariance model used is spherical 
variofit: weights used: cressie 
variofit: minimisation function used: optim 
# -------------------------
# 4. ML (Maximum Likelihood)
# -------------------------
fit_TMP_ML_REC <- likfit(
  geodata = geo_TMP,
  coords = geo_TMP$coords,
  data = geo_TMP$data,
  ini.cov.pars = ini_TMP,
  fix.nugget = TRUE,
  nugget = nug_TMP,
  cov.model = "spherical",
  lik.method = "ML"
)
kappa not used for the spherical correlation function
---------------------------------------------------------------
likfit: likelihood maximisation using the function optim.
likfit: Use control() to pass additional
         arguments for the maximisation function.
        For further details see documentation for optim.
likfit: It is highly advisable to run this function several
        times with different initial values for the parameters.
likfit: WARNING: This step can be time demanding!
---------------------------------------------------------------
likfit: end of numerical maximisation.
# -------------------------
# 5. REML
# -------------------------
fit_TMP_REML_REC <- likfit(
  geodata = geo_TMP,
  coords = geo_TMP$coords,
  data = geo_TMP$data,
  ini.cov.pars = ini_TMP,
  fix.nugget = TRUE,
  nugget = nug_TMP,
  cov.model = "spherical",
  lik.method = "REML"
)
kappa not used for the spherical correlation function
---------------------------------------------------------------
likfit: likelihood maximisation using the function optim.
likfit: Use control() to pass additional
         arguments for the maximisation function.
        For further details see documentation for optim.
likfit: It is highly advisable to run this function several
        times with different initial values for the parameters.
likfit: WARNING: This step can be time demanding!
---------------------------------------------------------------
likfit: end of numerical maximisation.
# --- Parámetros iniciales RH (del eyefit) ---
ini_RH <- c(sigmasq = 14.7, phi = 11500)
nug_RH <- 1.85

# -------------------------
# 1. MCO (OLS)
# -------------------------
fit_RH_MCO_REC <- variofit(
  vario = var_RH,
  ini.cov.pars = ini_RH,
  cov.model = "exponential",
  weights = "equal",
  fix.nugget = TRUE,
  nugget = nug_RH
)
variofit: covariance model used is exponential 
variofit: weights used: equal 
variofit: minimisation function used: optim 
# -------------------------
# 2. MCP (npairs)
# -------------------------
fit_RH_npairs_REC <- variofit(
  vario = var_RH,
  ini.cov.pars = ini_RH,
  cov.model = "exponential",
  weights = "npairs",
  fix.nugget = TRUE,
  nugget = nug_RH
)
variofit: covariance model used is exponential 
variofit: weights used: npairs 
variofit: minimisation function used: optim 
# -------------------------
# 3. MCP Cressie
# -------------------------
fit_RH_cressie_REC <- variofit(
  vario = var_RH,
  ini.cov.pars = ini_RH,
  cov.model = "exponential",
  weights = "cressie",
  fix.nugget = TRUE,
  nugget = nug_RH
)
variofit: covariance model used is exponential 
variofit: weights used: cressie 
variofit: minimisation function used: optim 
# -------------------------
# 4. ML
# -------------------------
fit_RH_ML_REC <- likfit(
  geodata = geo_RH,
  coords = geo_RH$coords,
  data = geo_RH$data,
  ini.cov.pars = ini_RH,
  fix.nugget = TRUE,
  nugget = nug_RH,
  cov.model = "exponential",
  lik.method = "ML"
)
kappa not used for the exponential correlation function
---------------------------------------------------------------
likfit: likelihood maximisation using the function optim.
likfit: Use control() to pass additional
         arguments for the maximisation function.
        For further details see documentation for optim.
likfit: It is highly advisable to run this function several
        times with different initial values for the parameters.
likfit: WARNING: This step can be time demanding!
---------------------------------------------------------------
likfit: end of numerical maximisation.
# -------------------------
# 5. REML
# -------------------------
fit_RH_REML_REC  <- likfit(
  geodata = geo_RH,
  coords = geo_RH$coords,
  data = geo_RH$data,
  ini.cov.pars = ini_RH,
  fix.nugget = TRUE,
  nugget = nug_RH,
  cov.model = "exponential",
  lik.method = "REML"
)
kappa not used for the exponential correlation function
---------------------------------------------------------------
likfit: likelihood maximisation using the function optim.
likfit: Use control() to pass additional
         arguments for the maximisation function.
        For further details see documentation for optim.
likfit: It is highly advisable to run this function several
        times with different initial values for the parameters.
likfit: WARNING: This step can be time demanding!
---------------------------------------------------------------
likfit: end of numerical maximisation.
plot(var_TMP$u, var_TMP$v,
     pch = 16,
     cex = 1,
     xlab = "Distancia (h)",
     ylab = "Semivarianza",
     main = "TMP — Ajustes del semivariograma (recortado)")

lines(fit_TMP_MCO_REC,     col = 1, lwd = 2)
lines(fit_TMP_npairs_REC,  col = 2, lwd = 2)
lines(fit_TMP_cressie_REC, col = 3, lwd = 2)
lines(fit_TMP_ML_REC,      col = 4, lwd = 2)
lines(fit_TMP_REML_REC,    col = 5, lwd = 2)

legend(
  "bottomright",
  legend = c("MCO", "MCP npairs", "MCP Cressie", "ML", "REML"),
  col = 1:5,
  lwd = 2
)

plot(var_RH$u, var_RH$v,
     pch = 16,
     cex = 1,
     xlab = "Distancia (h)",
     ylab = "Semivarianza",
     main = "RH — Ajustes del semivariograma (recortado)")

lines(fit_RH_MCO_REC,      col = 1, lwd = 2)
lines(fit_RH_npairs_REC,   col = 2, lwd = 2)
lines(fit_RH_cressie_REC,  col = 3, lwd = 2)
lines(fit_RH_ML_REC,       col = 4, lwd = 2)
lines(fit_RH_REML_REC,     col = 5, lwd = 2)

legend(
  "bottomright",
  legend = c("MCO", "MCP npairs", "MCP Cressie", "ML", "REML"),
  col = 1:5,
  lwd = 2
)

La estructura espacial de las variables Temperatura (TMP) y Humedad Relativa (RH) fue analizada mediante la estimación de semivariogramas empíricos y sus correspondientes modelos teóricos. En ambos casos, los resultados muestran que la variabilidad espacial presente en los datos no se ajusta de manera clara a un modelo paramétrico estándar, aun después de aplicar procedimientos robustos, centrar los residuales y recortar las distancias máximas consideradas.

Para cada variable se ajustaron modelos de semivariograma mediante MCO, MCP ponderado por número de pares, MCP con pesos de Cressie, máxima verosimilitud (ML) y máxima verosimilitud restringida (REML). En el caso de la temperatura (TMP), los modelos ML y REML producen curvas muy similares, que reproducen adecuadamente el nivel de semivarianza a distancias intermedias y un rango coherente con la separación máxima entre estaciones. Además, REML presenta el mejor compromiso entre verosimilitud y número de parámetros (AIC/BIC ligeramente menores) y es menos sesgado en la estimación de la varianza en muestras pequeñas. Por estas razones, el modelo esférico estimado por REML se adopta como modelo teórico final para TMP y se utiliza en la etapa de kriging. Para la humedad relativa (RH), el variograma empírico es más ruidoso; sin embargo, los modelos exponenciales ajustados por MCP con pesos de Cressie y por REML son los que mejor capturan la tendencia creciente inicial. Dado que REML proporciona también una mejor estimación de la varianza del proceso, se selecciona el modelo exponencial estimado por REML como base para las predicciones de kriging.

1.5 Mapas de predicción de kriging acompañados de cada uno de los mapas respectivos de varianza del error de predicción.

# ===============================================================
# Convertir modelos ML a gstat::vgm()
# ===============================================================

# --- TMP ---
vg_TMP_model <- vgm(
  psill  = fit_TMP_ML$cov.pars[1],
  model  = "Sph",
  nugget = fit_TMP_ML$nugget,
  range  = fit_TMP_ML$cov.pars[2]
)

# --- RH ---
# Extraemos parámetros
sig_RH <- fit_RH_ML$cov.pars[1]
phi_RH <- fit_RH_ML$cov.pars[2]
nug_RH <- fit_RH_ML$nugget

# Si ML entrega phi_RH = 0 (caso frecuente)
if (phi_RH <= 0) {
  message("ML dio rango=0, se fuerza a un mínimo razonable")
  phi_RH <- max(2000, 0.15 * Dmax_rh)   # regla razonable
}

vg_RH_model <- vgm(
  psill  = sig_RH,
  model  = "Exp",
  nugget = nug_RH,
  range  = phi_RH
)
df_tmp_sp <- df_tmp
coordinates(df_tmp_sp) <- ~ x + y
proj4string(df_tmp_sp) <- CRS(paste0("EPSG:",epsg_crs))

df_rh_sp <- df_rh
coordinates(df_rh_sp) <- ~ x + y
proj4string(df_rh_sp) <- CRS(paste0("EPSG:",epsg_crs))

buffer <- 2000

grd <- expand.grid(
  x = seq(min(df_tmp$x)-buffer, max(df_tmp$x)+buffer, length.out=120),
  y = seq(min(df_tmp$y)-buffer, max(df_tmp$y)+buffer, length.out=120)
)

coordinates(grd) <- ~ x + y
gridded(grd) <- TRUE
proj4string(grd) <- CRS(paste0("EPSG:",epsg_crs))

# ===============================================================
# Kriging ordinario
# ===============================================================

g_TMP <- gstat(id="resid", formula = resid ~ 1,
               data = df_tmp_sp, model = vg_TMP_model)

krig_TMP <- predict(g_TMP, grd)
[using ordinary kriging]
g_RH <- gstat(id="resid", formula = resid ~ 1,
              data = df_rh_sp, model = vg_RH_model)

krig_RH <- predict(g_RH, grd)
[using ordinary kriging]
# TMP no tiene tendencia → predicción = residual krigeado
TMP_pred_final <- krig_TMP$resid.pred
TMP_var_final  <- krig_TMP$resid.var

# RH sí tiene tendencia → reconstrucción media + residuales
beta_rh <- coef(m1_rh)
RH_mean_grid <- beta_rh[1] + beta_rh[2]*grd$x + beta_rh[3]*grd$y

RH_pred_final <- RH_mean_grid + krig_RH$resid.pred
RH_var_final  <- krig_RH$resid.var

# Añadir al grid
grd$TMP_pred <- TMP_pred_final
grd$RH_pred  <- RH_pred_final
grd$TMP_var <- TMP_var_final
grd$RH_var  <- RH_var_final

library(raster)

Adjuntando el paquete: 'raster'
The following object is masked from 'package:dplyr':

    select
# ===============================================================
# Raster en UTM (predicción y varianza)
# ===============================================================

# Raster de predicción en UTM
ras_TMP_utm    <- raster(grd["TMP_pred"])
ras_RH_utm     <- raster(grd["RH_pred"])

# Raster de varianza en UTM
ras_TMPvar_utm <- raster(grd["TMP_var"])
ras_RHvar_utm  <- raster(grd["RH_var"])

# --- Proyección a lat/long ---
crs_ll <- CRS("+proj=longlat +datum=WGS84 +no_defs")

ras_TMP_ll    <- projectRaster(ras_TMP_utm,    crs = crs_ll)
ras_RH_ll     <- projectRaster(ras_RH_utm,     crs = crs_ll)
ras_TMPvar_ll <- projectRaster(ras_TMPvar_utm, crs = crs_ll)
ras_RHvar_ll  <- projectRaster(ras_RHvar_utm,  crs = crs_ll)


# ===============================================================
# Preparar estaciones
# ===============================================================

pts_sf <- st_as_sf(df_tmp, coords = c("x","y"), crs = epsg_crs)
pts_ll <- st_transform(pts_sf, 4326)


# ===============================================================
# Paletas de color para cada capa
# ===============================================================

# Valores de TMP
vals_TMP    <- values(ras_TMP_ll)
vals_TMPvar <- values(ras_TMPvar_ll)

# Valores de RH
vals_RH     <- values(ras_RH_ll)
vals_RHvar  <- values(ras_RHvar_ll)

pal_TMP    <- colorNumeric(viridis(200), range(vals_TMP, na.rm = TRUE))
pal_TMPvar <- colorNumeric(viridis(200), range(vals_TMPvar, na.rm = TRUE))

pal_RH     <- colorNumeric(viridis(200), range(vals_RH, na.rm = TRUE))
pal_RHvar  <- colorNumeric(viridis(200), range(vals_RHvar, na.rm = TRUE))


# Extensión para enfocar el mapa
ext <- extent(ras_TMP_ll)


# ===============================================================
# Leaflet con 4 capas (predicción + varianza)
# ===============================================================

leaflet() %>%
  addProviderTiles("CartoDB.Positron") %>%

  # --- TMP Predicción ---
  addRasterImage(
    ras_TMP_ll,
    colors = pal_TMP,
    opacity = 0.7,
    group = "TMP — Predicción"
  ) %>%

  # --- TMP Varianza ---
  addRasterImage(
    ras_TMPvar_ll,
    colors = pal_TMPvar,
    opacity = 0.7,
    group = "TMP — Varianza"
  ) %>%

  # --- RH Predicción ---
  addRasterImage(
    ras_RH_ll,
    colors = pal_RH,
    opacity = 0.7,
    group = "RH — Predicción"
  ) %>%

  # --- RH Varianza ---
  addRasterImage(
    ras_RHvar_ll,
    colors = pal_RHvar,
    opacity = 0.7,
    group = "RH — Varianza"
  ) %>%

  # --- Estaciones ---
  addCircleMarkers(
    data = pts_ll,
    lng = ~st_coordinates(geometry)[,1],
    lat = ~st_coordinates(geometry)[,2],
    radius = 5,
    color = "red",
    fillOpacity = 0.7
  ) %>%

  # --- Control de capas ---
  addLayersControl(
    overlayGroups = c(
      "TMP — Predicción", "TMP — Varianza",
      "RH — Predicción",  "RH — Varianza"
    ),
    options = layersControlOptions(collapsed = FALSE)
  ) %>%

  # --- Ajustar vista ---
  fitBounds(
    lng1 = xmin(ext), lat1 = ymin(ext),
    lng2 = xmax(ext), lat2 = ymax(ext)
  )

2a. Modelos de semivariograma alternativos mediante regresión lineal y no lineal

En esta sección mostramos ejemplos de ajuste de modelos de semivariograma que no están predefinidos en los paquetes geoR y gstat, usando herramientas generales de regresión (lm, nls) y esquemas de mínimos cuadrados ponderados iterativos (IRLS). Trabajamos sobre datos simulados.

library(dplyr)
library(ggplot2)
library(tidyr)
library(sp)
library(knitr)
set.seed(123)

2.1a. Simulación de datos espaciales

Creamos un campo espacial sintético con dependencia espacial suave.

n <- 80
coords <- cbind(runif(n, 0, 10), runif(n, 0, 10))
colnames(coords) <- c("x", "y")

# Proceso con estructura espacial (tendencia + ruido correlacionado)
z_true <- 3 + 2*sin(coords[,1]/2) + 1.5*cos(coords[,2]/3)
z <- z_true + rnorm(n, 0, 0.4)
data <- data.frame(coords, z)

2.2a. Cálculo empírico del semivariograma

# Matriz de distancias y semivarianzas
dist_mat <- as.matrix(dist(coords))
gamma_mat <- 0.5 * (outer(z, z, "-"))^2

# Agrupamos distancias en bins
breaks <- seq(0, max(dist_mat), length.out = 15)
hcut <- cut(dist_mat, breaks)
emp_variog <- data.frame(
  h = breaks[-length(breaks)],
  gamma = tapply(gamma_mat, hcut, mean, na.rm = TRUE),
  np = tapply(gamma_mat, hcut, length)
)

emp_variog <- emp_variog %>% filter(!is.na(gamma))

ggplot(emp_variog, aes(h, gamma)) +
  geom_point() +
  theme_minimal() +
  labs(title = "Semivariograma empírico",
       x = "Distancia h", y = "Semivarianza γ(h)")

2.3a. Ajuste de modelos personalizados (fuera de geoR/gstat)

Los paquetes geoR y gstat ofrecen modelos clásicos (esférico, exponencial, gaussiano, Matérn, etc.). Aquí usamos modelos no estándar:

  • Modelo polinomial cuadrático decreciente
  • Modelo logístico modificado

Ambos estimados por MCO y Mínimos Cuadrados No Lineales (NLS).

# Modelo polinomial cuadrático (MCO)
poly_model <- lm(gamma ~ poly(h, 2, raw = TRUE), data = emp_variog)
summary(poly_model)

Call:
lm(formula = gamma ~ poly(h, 2, raw = TRUE), data = emp_variog)

Residuals:
     Min       1Q   Median       3Q      Max 
-2.24362 -0.14059  0.03095  0.25494  1.50051 

Coefficients:
                        Estimate Std. Error t value Pr(>|t|)  
(Intercept)             -0.01771    0.59689  -0.030   0.9769  
poly(h, 2, raw = TRUE)1  0.51382    0.23253   2.210   0.0492 *
poly(h, 2, raw = TRUE)2  0.05062    0.01881   2.691   0.0210 *
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.8533 on 11 degrees of freedom
Multiple R-squared:  0.9682,    Adjusted R-squared:  0.9624 
F-statistic: 167.6 on 2 and 11 DF,  p-value: 5.78e-09
# Modelo logístico modificado para el semivariograma
logistic_fun <- function(h, nug, sill, range){
  nug + (sill - nug)/(1 + exp(-0.5 * (h - range)))
}

nls_model <- nls(
  gamma ~ logistic_fun(h, nug, sill, range),
  data = emp_variog,
  start = list(nug = 0.1, sill = 1, range = 5)
)
summary(nls_model)

Formula: gamma ~ logistic_fun(h, nug, sill, range)

Parameters:
      Estimate Std. Error t value Pr(>|t|)    
nug     0.4017     0.6340   0.634    0.539    
sill   13.6624     1.1503  11.877 1.29e-07 ***
range   7.5432     0.6718  11.228 2.30e-07 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 1.108 on 11 degrees of freedom

Number of iterations to convergence: 13 
Achieved convergence tolerance: 3.423e-06

2.4a. Ponderaciones fijas y ponderaciones iterativas (WLS)

# Ponderaciones fijas: inversas al número de pares (np)
w_fixed <- 1 / emp_variog$np

# MCP (WLS con pesos fijos)
wls_model <- lm(
  gamma ~ poly(h, 2, raw = TRUE),
  data = emp_variog,
  weights = w_fixed
)
summary(wls_model)

Call:
lm(formula = gamma ~ poly(h, 2, raw = TRUE), data = emp_variog, 
    weights = w_fixed)

Weighted Residuals:
     Min       1Q   Median       3Q      Max 
-0.41812  0.01302  0.04633  0.05888  0.13184 

Coefficients:
                        Estimate Std. Error t value Pr(>|t|)  
(Intercept)              0.71255    1.42572   0.500   0.6271  
poly(h, 2, raw = TRUE)1 -0.27545    0.53071  -0.519   0.6140  
poly(h, 2, raw = TRUE)2  0.11963    0.03914   3.057   0.0109 *
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.1418 on 11 degrees of freedom
Multiple R-squared:  0.9327,    Adjusted R-squared:  0.9205 
F-statistic: 76.27 on 2 and 11 DF,  p-value: 3.569e-07
# Iterativo: pesos según residuos del modelo anterior
iter_model <- poly_model
for(i in 1:5){
  res <- residuals(iter_model)
  w_iter <- 1 / (abs(res) + 1e-4)  # evitar división por cero
  iter_model <- lm(
    gamma ~ poly(h, 2, raw = TRUE),
    data = emp_variog,
    weights = w_iter
  )
}
summary(iter_model)

Call:
lm(formula = gamma ~ poly(h, 2, raw = TRUE), data = emp_variog, 
    weights = w_iter)

Weighted Residuals:
     Min       1Q   Median       3Q      Max 
-1.60782 -0.31565  0.00508  0.48586  1.02871 

Coefficients:
                         Estimate Std. Error t value Pr(>|t|)    
(Intercept)             -0.046210   0.086546  -0.534    0.604    
poly(h, 2, raw = TRUE)1  0.479943   0.043616  11.004 2.82e-07 ***
poly(h, 2, raw = TRUE)2  0.056549   0.004696  12.042 1.12e-07 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.7239 on 11 degrees of freedom
Multiple R-squared:  0.9998,    Adjusted R-squared:  0.9998 
F-statistic: 3.663e+04 on 2 and 11 DF,  p-value: < 2.2e-16

2.5a. ML y Composite Likelihood (CL) con modelo exponencial casero

Definimos un modelo exponencial de covarianza y el semivariograma correspondiente y estimamos sus parámetros por:

  • ML (verosimilitud gaussiana completa).
  • Composite Likelihood (CL) pareada.
# Centramos los datos para suponer media cero en ML
z_c <- z - mean(z)

# Semivariograma exponencial
gamma_exp <- function(h, sigma2, rho, tau2){
  # nugget (tau2) + parte estructural
  tau2 + sigma2 * (1 - exp(-h / rho))
}

# Matriz de covarianza exponencial a partir de parámetros
make_cov_exp <- function(theta, dist_mat){
  # theta en escala log: (log(sigma2), log(rho), log(tau2))
  sigma2 <- exp(theta[1])
  rho    <- exp(theta[2])
  tau2   <- exp(theta[3])

  C <- sigma2 * exp(-dist_mat / rho)
  diag(C) <- diag(C) + tau2 + 1e-6  # término pequeño para estabilidad
  C
}

# Log-verosimilitud negativa (ML completo)
negloglik_full <- function(theta, z, dist_mat){
  C <- make_cov_exp(theta, dist_mat)
  cholC <- chol(C)
  logdet <- 2 * sum(log(diag(cholC)))
  sol <- backsolve(cholC, z, transpose = TRUE)
  quad <- crossprod(sol)
  0.5 * (length(z) * log(2 * pi) + logdet + quad)
}

# Valores iniciales
start_theta <- log(c(var(z_c) * 0.7, max(dist_mat) / 3, var(z_c) * 0.3))

fit_ml <- optim(
  start_theta, negloglik_full,
  z = z_c, dist_mat = dist_mat,
  method = "BFGS"
)

theta_ml <- exp(fit_ml$par)
names(theta_ml) <- c("sigma2", "rho", "tau2")
theta_ml
     sigma2         rho        tau2 
 6.15342493 29.42527721  0.06726359 

Composite Likelihood (pairwise)

# Índices de pares i<j
pairs_idx <- which(upper.tri(dist_mat), arr.ind = TRUE)
d_vec <- dist_mat[pairs_idx]
i_idx <- pairs_idx[,1]
j_idx <- pairs_idx[,2]

negloglik_cl <- function(theta, z, d_vec, i_idx, j_idx){
  sigma2 <- exp(theta[1])
  rho    <- exp(theta[2])
  tau2   <- exp(theta[3])

  c0  <- sigma2 + tau2 + 1e-6
  cij <- sigma2 * exp(-d_vec / rho)

  ll <- 0
  for(k in seq_along(d_vec)){
    S11 <- c0
    S22 <- c0
    S12 <- cij[k]
    detS <- S11 * S22 - S12^2

    y1 <- z[i_idx[k]]
    y2 <- z[j_idx[k]]

    quad <- (S22 * y1^2 - 2 * S12 * y1 * y2 + S11 * y2^2) / detS

    ll <- ll - 0.5 * (2 * log(2 * pi) + log(detS) + quad)
  }
  -ll  # negativo para minimizar
}

fit_cl <- optim(
  start_theta, negloglik_cl,
  z = z_c, d_vec = d_vec,
  i_idx = i_idx, j_idx = j_idx,
  method = "BFGS",
  control = list(maxit = 60)
)

theta_cl <- exp(fit_cl$par)
names(theta_cl) <- c("sigma2", "rho", "tau2")
theta_cl
      sigma2          rho         tau2 
3.6887698649 2.8761844713 0.0001174517 

Tabla de parámetros ML vs CL

param_df <- rbind(
  ML = theta_ml,
  CL = theta_cl
)

kable(param_df, digits = 3,
      caption = "Parámetros del modelo exponencial estimados por ML y CL")
Parámetros del modelo exponencial estimados por ML y CL
sigma2 rho tau2
ML 6.153 29.425 0.067
CL 3.689 2.876 0.000

2.6a. Visualización comparativa de todos los modelos

Incluimos MCO, MCP, WLS iterativo, NLS logístico, ML exponencial y CL exponencial.

h_seq <- seq(min(emp_variog$h), max(emp_variog$h), length.out = 200)

# Predicciones de los modelos polinomiales y NLS
pred_poly <- predict(poly_model, newdata = data.frame(h = h_seq))
pred_wls  <- predict(wls_model,  newdata = data.frame(h = h_seq))
pred_iter <- predict(iter_model, newdata = data.frame(h = h_seq))
pred_nls  <- predict(nls_model,  newdata = data.frame(h = h_seq))

# Predicciones usando los parámetros ML y CL del modelo exponencial
pred_ml <- gamma_exp(
  h_seq,
  sigma2 = theta_ml["sigma2"],
  rho    = theta_ml["rho"],
  tau2   = theta_ml["tau2"]
)

pred_cl <- gamma_exp(
  h_seq,
  sigma2 = theta_cl["sigma2"],
  rho    = theta_cl["rho"],
  tau2   = theta_cl["tau2"]
)

plot_df <- data.frame(
  h              = h_seq,
  MCO            = pred_poly,
  MCP            = pred_wls,
  Iter_WLS       = pred_iter,
  NLS_logistica  = pred_nls,
  ML_exp         = pred_ml,
  CL_exp         = pred_cl
) |>
  tidyr::pivot_longer(-h, names_to = "metodo", values_to = "gamma")

ggplot() +
  geom_point(data = emp_variog,
             aes(h, gamma),
             alpha = 0.6) +
  geom_line(data = plot_df,
            aes(h, gamma, color = metodo),
            linewidth = 1) +
  theme_minimal() +
  labs(
    title = "Comparación de estimadores del semivariograma",
    x = "Distancia h",
    y = "Semivarianza γ(h)"
  )

2b. Modelos de semivariograma alternativos mediante regresión lineal y no lineal

En esta sección mostramos ejemplos de ajuste de modelos de semivariograma que no están predefinidos en los paquetes geoR y gstat, usando herramientas generales de regresión (lm, nls) y esquemas de mínimos cuadrados ponderados iterativos (IRLS). Trabajamos sobre el semivariograma empírico clásico de la temperatura (TMP), pero el mismo procedimiento se puede replicar para RH.

2.1b. Preparar los datos del semivariograma empírico

# Datos del semivariograma clásico de TMP en formato data.frame
vg_tmp_df <- as.data.frame(vg_TMP) |>
  dplyr::transmute(
    h     = dist,   # distancia media del bin
    gamma = gamma,  # semivarianza empírica
    np    = np      # número de pares
  )

head(vg_tmp_df)
          h     gamma np
1  5632.817 0.1900000  3
2  7559.192 0.2108333  6
3  9642.049 0.3266667  6
4 12306.191 0.1842857  7
5 14861.818 0.1154167 12
6 16571.822 0.1333333  3

2.2b. Modelo polinomial (regresión lineal)

Como ejemplo simple, ajustamos un modelo polinomial cuadrático: \(\gamma(h) \approx \beta_0 + \beta_1 h + \beta_2 h^2\).

# Modelo polinomial: gamma(h) ≈ β0 + β1 h + β2 h^2
vg_poly_lm <- lm(
  gamma ~ h + I(h^2),
  data    = vg_tmp_df,
  weights = np          # pesos fijos: número de pares en cada bin
)

summary(vg_poly_lm)

Call:
lm(formula = gamma ~ h + I(h^2), data = vg_tmp_df, weights = np)

Weighted Residuals:
     Min       1Q   Median       3Q      Max 
-0.25967 -0.09674 -0.02595  0.04185  0.36062 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)  
(Intercept)  2.979e-01  1.376e-01   2.165   0.0556 .
h           -1.310e-05  1.565e-05  -0.837   0.4220  
I(h^2)       3.947e-10  4.003e-10   0.986   0.3474  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.2048 on 10 degrees of freedom
Multiple R-squared:  0.128, Adjusted R-squared:  -0.04634 
F-statistic: 0.7343 on 2 and 10 DF,  p-value: 0.504

Este modelo no garantiza que el semivariograma resultante sea definido positivo, pero ilustra cómo usar regresión lineal estándar para ajustar una forma funcional arbitraria sobre los puntos empíricos.

2.3b. Modelo racional (no lineal) con pesos fijos

Ahora proponemos un modelo racional no estándar:

\(\gamma(h) = \tau^2 + \sigma^2 \frac{h^2}{h^2 + \phi^2}\)

que no está incluido explícitamente en geoR/gstat. Lo ajustamos con nls y pesos fijos.

# Modelo racional de semivariograma
gamma_rat <- function(h, nugget, sill, range) {
  nugget + sill * (h^2 / (h^2 + range^2))
}

# Valores iniciales razonables
start_vals <- list(
  nugget = min(vg_tmp_df$gamma, na.rm = TRUE),
  sill   = max(vg_tmp_df$gamma, na.rm = TRUE) - min(vg_tmp_df$gamma, na.rm = TRUE),
  range  = max(vg_tmp_df$h, na.rm = TRUE) / 3
)

# Ajuste no lineal con pesos fijos (np)
vg_rat_nls <- try(
  nls(
    gamma ~ gamma_rat(h, nugget, sill, range),
    data    = vg_tmp_df,
    start   = start_vals,
    weights = np,   # pesos fijos
    control = nls.control(maxiter = 200, warnOnly = TRUE)
  ),
  silent = TRUE
)
Warning in nls(gamma ~ gamma_rat(h, nugget, sill, range), data = vg_tmp_df, :
gradiente singular
if (inherits(vg_rat_nls, "try-error")) {
  warning("El ajuste nls del modelo racional no convergió; se usan los valores iniciales como aproximación.")
  par_nls <- unlist(start_vals)
} else {
  # Intentar obtener los coeficientes sin llamar a summary() (que puede fallar
  # por problemas numéricos en la matriz de información)
  coef_try <- try(coef(vg_rat_nls), silent = TRUE)

  if (inherits(coef_try, "try-error")) {
    warning("El ajuste nls produjo un objeto singular; se usan los valores iniciales como aproximación.")
    par_nls <- unlist(start_vals)
  } else {
    par_nls <- coef_try
  }
}

2.4b. Mínimos cuadrados ponderados iterativos (IRLS) con pesos que dependen de los parámetros

Para ilustrar el uso de matrices de ponderación que cambian en cada iteración, implementamos un esquema IRLS donde, en la iteración (k), hacemos:

\(w_i^{(k)} = \frac{n_i}{\bigl(\hat\gamma_i^{(k)}\bigr)^2}, \quad \hat\gamma_i^{(k)} = \hat \gamma(h_i; \theta^{(k)})\), y minimizamos \((\sum_i w_i^{(k)} (\gamma_i - \hat\gamma_i)^2)\) respecto a \(\theta\).

irls_vg_rat <- function(vg_df, par_init, max_iter = 15, tol = 1e-4) {
  par_cur  <- par_init
  obj_vals <- numeric(max_iter)

  for (k in seq_len(max_iter)) {
    # semivariograma teórico con parámetros actuales
    g_cur <- gamma_rat(vg_df$h, par_cur[1], par_cur[2], par_cur[3])

    # pesos dependientes del modelo actual (evitar dividir por cero)
    w <- vg_df$np / (g_cur^2 + 1e-6)

    # función objetivo de Mínimos Cuadrados Ponderados
    obj <- function(par) {
      g_mod <- gamma_rat(vg_df$h, par[1], par[2], par[3])
      sum(w * (vg_df$gamma - g_mod)^2)
    }

    opt <- optim(
      par    = par_cur,
      fn     = obj,
      method = "L-BFGS-B",
      lower  = c(0, 0, 1e-6)  # asegurar positividad básica
    )

    obj_vals[k] <- opt$value

    # criterio de parada
    if (max(abs(opt$par - par_cur)) < tol) {
      par_cur  <- opt$par
      obj_vals <- obj_vals[1:k]
      break
    }

    par_cur <- opt$par
  }

  list(par = par_cur, obj = obj_vals, iters = length(obj_vals))
}

# Valores iniciales tomados del ajuste nls
par0_rat <- c(
  nugget = coef(vg_rat_nls)[["nugget"]],
  sill   = coef(vg_rat_nls)[["sill"]],
  range  = coef(vg_rat_nls)[["range"]]
)

vg_rat_irls <- irls_vg_rat(vg_tmp_df, par0_rat)

vg_rat_irls$par      # parámetros finales IRLS
     nugget        sill       range 
0.211105838 0.004014643 0.000001000 
vg_rat_irls$obj      # valores de la función objetivo por iteración
[1] 11.24585 10.39688
vg_rat_irls$iters
[1] 2

2.5b. Comparación visual de modelos alternativos

# Grid de distancias para comparar curvas teóricas
h_grid <- seq(0, max(vg_tmp_df$h, na.rm = TRUE), length.out = 100)

# Curva polinomial (a partir de los coeficientes de lm)
poly_coef  <- coef(vg_poly_lm)
gamma_poly <- poly_coef[1] + poly_coef[2] * h_grid + poly_coef[3] * h_grid^2

# Curva racional (nls)
gamma_nls <- gamma_rat(h_grid, par_nls["nugget"], par_nls["sill"], par_nls["range"])

# Curva racional (IRLS)
par_irls   <- vg_rat_irls$par
gamma_irls <- gamma_rat(h_grid, par_irls[1], par_irls[2], par_irls[3])

plot(vg_tmp_df$h, vg_tmp_df$gamma,
     xlab = "h (distancia)",
     ylab = "semivarianza",
     main = "TMP — Modelos alternativos de semivariograma")
lines(h_grid, gamma_poly, lwd = 2)
lines(h_grid, gamma_nls,  lwd = 2, lty = 2)
lines(h_grid, gamma_irls, lwd = 2, lty = 3)
legend(
  "bottomright",
  legend = c("Empírico", "Polinomial LM", "Racional NLS", "Racional IRLS"),
  lwd    = c(NA, 2, 2, 2),
  pch    = c(1, NA, NA, NA),
  lty    = c(NA, 1, 2, 3)
)

3. Funciones objetivo generales para MCO, MCP, MV y CL y su programación con optim

En esta sección definimos de forma explícita las funciones objetivo asociadas a:

  • Mínimos Cuadrados Ordinarios (MCO)

  • Mínimos Cuadrados Ponderados (MCP) sobre el semivariograma empírico

  • Máxima Verosimilitud (MV) sobre los datos residuales

  • Composite Likelihood (CL) basada en pares

  • Mínimos Cuadrados Ordinarios (MCO)

    Sea
    \(\hat{\boldsymbol\gamma} = (\hat\gamma(h_1),\dots,\hat\gamma(h_K))^\top\)
    el vector de semivariogramas empíricos y
    \(\boldsymbol\gamma(\theta) = (\gamma(h_1;\theta),\dots,\gamma(h_K;\theta))^\top\)
    el semivariograma teórico para el vector de parámetros \(\theta\).

    La función objetivo de MCO es

    \[ Q_{\text{MCO}}(\theta) = \bigl\|\hat{\boldsymbol\gamma} - \boldsymbol\gamma(\theta)\bigr\|^2 = \sum_{k=1}^K \left[\hat\gamma(h_k) - \gamma(h_k;\theta)\right]^2. \]

  • Mínimos Cuadrados Ponderados (MCP) sobre el semivariograma empírico

    Introducimos una matriz de ponderaciones fija \(W\) (por ejemplo diagonal, con pesos \(w_k\)). La función objetivo general es

    \[ Q_{\text{MCP}}(\theta) =\bigl(\hat{\boldsymbol\gamma} - \boldsymbol\gamma(\theta)\bigr)^\top W\, \bigl(\hat{\boldsymbol\gamma} - \boldsymbol\gamma(\theta)\bigr) = \sum_{k=1}^K w_k \left[\hat\gamma(h_k) - \gamma(h_k;\theta)\right]^2. \]

  • Máxima Verosimilitud (MV) sobre los datos (o residuales)

    Sea \(\mathbf{r}=(r_1,\dots,r_n)^\top\) el vector de datos (o residuales) y \(C(\theta)\) la matriz de covarianza gaussiana asociada al modelo espacial. La log-verosimilitud (hasta una constante) viene dada por

    \[ \ell(\theta) = -\tfrac12\left\{ \log\bigl|C(\theta)\bigr| + \mathbf{r}^\top C(\theta)^{-1}\mathbf{r} \right\}. \]

    Equivalentemente, la función objetivo a minimizar en optim puede definirse como

    \[ Q_{\text{MV}}(\theta) = \tfrac12\left\{ \log\bigl|C(\theta)\bigr| + \mathbf{r}^\top C(\theta)^{-1}\mathbf{r} \right\}. \]

  • Composite Likelihood (CL) basada en pares

    Para cada par \((i,j)\) con \(i<j\), sea \(\mathbf{r}_{ij} = (r_i,r_j)^\top\) y \(\Sigma_{ij}(\theta)\) la submatriz \(2\times 2\) de \(C(\theta)\) correspondiente a ese par.
    La log-likelihood pareada gaussiana (hasta constante) es

    \[ \ell_{\text{CL}}(\theta) = -\tfrac12 \sum_{i<j} \left\{ \log\bigl|\Sigma_{ij}(\theta)\bigr| + \mathbf{r}_{ij}^\top \Sigma_{ij}(\theta)^{-1} \mathbf{r}_{ij} \right\}. \]

    Por tanto, la función objetivo de CL a minimizar es

    \[ Q_{\text{CL}}(\theta) = \tfrac12 \sum_{i<j} \left\{ \log\bigl|\Sigma_{ij}(\theta)\bigr| + \mathbf{r}_{ij}^\top \Sigma_{ij}(\theta)^{-1} \mathbf{r}_{ij} \right\}. \]

En todos los casos, el estimador se obtiene como
\(\hat\theta = \arg\min_\theta Q(\theta)\) usando optim().

Usaremos un modelo exponencial estándar como ejemplo, pero las funciones están escritas de forma genérica y se pueden adaptar a otros modelos.

3.1 Modelo exponencial: semivariograma y covarianza

# Semivariograma exponencial
gamma_exp <- function(h, nugget, sill, range) {
  nugget + sill * (1 - exp(-h / range))
}

# Covarianza exponencial (parte estructural)
cov_exp <- function(h, sill, range) {
  sill * exp(-h / range)
}

3.2 Funciones objetivo MCO y MCP sobre el semivariograma empírico

Trabajamos con los puntos del semivariograma empírico de TMP:

h         <- vg_tmp_df$h
gamma_hat <- vg_tmp_df$gamma

# Ejemplo de pesos fijos para MCP (se pueden cambiar)
w_fix <- vg_tmp_df$np / (h^2 + 1e-6)

Para garantizar positividad en los parámetros, optimizamos en la escala log: (= (^2, ^2, )).

obj_mco <- function(log_par, h, gamma_hat) {
  nugget <- exp(log_par[1])
  sill   <- exp(log_par[2])
  range  <- exp(log_par[3])

  g_mod <- gamma_exp(h, nugget, sill, range)
  mean((gamma_hat - g_mod)^2)
}

obj_mcp <- function(log_par, h, gamma_hat, w) {
  nugget <- exp(log_par[1])
  sill   <- exp(log_par[2])
  range  <- exp(log_par[3])

  g_mod <- gamma_exp(h, nugget, sill, range)
  sum(w * (gamma_hat - g_mod)^2)
}

3.3 Ajuste numérico de MCO y MCP con optim

log_par_init <- log(c(
  nugget = 0.05,
  sill   = 0.20,
  range  = max(h, na.rm = TRUE) / 3
))

# MCO
fit_mco <- optim(
  par       = log_par_init,
  fn        = obj_mco,
  h         = h,
  gamma_hat = gamma_hat,
  method    = "L-BFGS-B"
)

# MCP con pesos fijos
fit_mcp <- optim(
  par       = log_par_init,
  fn        = obj_mcp,
  h         = h,
  gamma_hat = gamma_hat,
  w         = w_fix,
  method    = "L-BFGS-B"
)

theta_mco <- exp(fit_mco$par)
theta_mcp <- exp(fit_mcp$par)

theta_mco
      nugget         sill        range 
1.189453e-04 2.150231e-01 1.779064e+03 
theta_mcp
      nugget         sill        range 
5.368268e-02 2.283882e-01 9.650538e+03 

3.4 Función objetivo de Máxima Verosimilitud (MV)

Usamos los residuales espaciales de TMP (df_tmp$resid) y sus coordenadas centradas (x, y):

coords_tmp <- as.matrix(df_tmp[, c("x", "y")])
z_tmp      <- df_tmp$resid
D_tmp      <- as.matrix(dist(coords_tmp))

La log-verosimilitud negativa para un campo gaussiano con covarianza exponencial y nugget es:

obj_mv <- function(log_par, D, z) {
  nugget <- exp(log_par[1])
  sill   <- exp(log_par[2])
  range  <- exp(log_par[3])

  n <- length(z)

  # Matriz de covarianza
  Sigma <- cov_exp(D, sill, range)
  diag(Sigma) <- diag(Sigma) + nugget

  chol_S <- tryCatch(chol(Sigma), error = function(e) NULL)
  if (is.null(chol_S)) return(1e10)  # castigo si la matriz no es definida positiva

  logdet <- 2 * sum(log(diag(chol_S)))
  v      <- backsolve(chol_S, z, transpose = TRUE)
  quad   <- sum(v^2)

  0.5 * (logdet + quad)   # constante n log(2π) omitida
}
fit_mv <- optim(
  par    = log_par_init,
  fn     = obj_mv,
  D      = D_tmp,
  z      = z_tmp,
  method = "L-BFGS-B"
)

theta_mv <- exp(fit_mv$par)
theta_mv
      nugget         sill        range 
2.091233e-01 6.479977e-02 1.422553e+04 

3.5 Composite Likelihood (CL) pareado

Definimos una composite likelihood basada en pares ((i,j)) usando la log-verosimilitud bivariante gaussiana para cada par:

obj_cl <- function(log_par, D, z) {
  nugget <- exp(log_par[1])
  sill   <- exp(log_par[2])
  range  <- exp(log_par[3])

  v   <- sill + nugget   # varianza marginal
  idx <- which(upper.tri(D), arr.ind = TRUE)

  neg_cl <- 0

  for (k in seq_len(nrow(idx))) {
    i <- idx[k, 1]
    j <- idx[k, 2]

    h_ij <- D[i, j]
    c_ij <- sill * exp(-h_ij / range)  # covarianza

    detS <- v^2 - c_ij^2
    if (detS <= 0) return(1e10)

    inv00 <-  v   / detS
    inv01 <- -c_ij / detS

    zi <- z[i]
    zj <- z[j]

    quad <- inv00 * zi^2 + 2 * inv01 * zi * zj + inv00 * zj^2

    neg_cl <- neg_cl + 0.5 * (log(detS) + quad)
  }

  neg_cl
}
fit_cl <- optim(
  par    = log_par_init,
  fn     = obj_cl,
  D      = D_tmp,
  z      = z_tmp,
  method = "L-BFGS-B"
)

theta_cl <- exp(fit_cl$par)
theta_cl
      nugget         sill        range 
1.938503e-01 7.675584e-02 8.992130e+03 

3.6 Comparación de parámetros entre métodos

Finalmente, recopilamos los parámetros estimados por cada método para compararlos con los obtenidos por geoR (MCO, MCP, ML, REML) y/o gstat:

library(tibble)

comparacion_par <- tibble(
  metodo = c("MCO", "MCP", "MV", "CL"),
  nugget = c(theta_mco[1], theta_mcp[1], theta_mv[1], theta_cl[1]),
  sill   = c(theta_mco[2], theta_mcp[2], theta_mv[2], theta_cl[2]),
  range  = c(theta_mco[3], theta_mcp[3], theta_mv[3], theta_cl[3])
)

comparacion_par
# A tibble: 4 × 4
  metodo   nugget   sill  range
  <chr>     <dbl>  <dbl>  <dbl>
1 MCO    0.000119 0.215   1779.
2 MCP    0.0537   0.228   9651.
3 MV     0.209    0.0648 14226.
4 CL     0.194    0.0768  8992.
# Grid de distancias para graficar las curvas teóricas
h_grid <- seq(
  from = 0,
  to   = max(vg_tmp_df$h, na.rm = TRUE),
  length.out = 200
)

# Paleta simple para los métodos
cols_metodos <- c(
  "MCO" = "black",
  "MCP" = "red",
  "MV"  = "blue",
  "CL"  = "darkgreen"
)

# Gráfico 1: rango completo de distancias
plot(
  vg_tmp_df$h, vg_tmp_df$gamma,
  xlab = "Distancia (h)",
  ylab = "Semivarianza",
  main = "TMP — Semivariograma empírico y modelos MCO/MCP/MV/CL",
  pch  = 1
)

# Añadir las curvas teóricas de cada método
for (i in seq_len(nrow(comparacion_par))) {
  met    <- comparacion_par$metodo[i]
  nugget <- comparacion_par$nugget[i]
  sill   <- comparacion_par$sill[i]
  range  <- comparacion_par$range[i]

  lines(
    h_grid,
    gamma_exp(h_grid, nugget, sill, range),
    col = cols_metodos[met],
    lwd = 2
  )
}

legend(
  "bottomright",
  legend = comparacion_par$metodo,
  col    = cols_metodos[comparacion_par$metodo],
  lwd    = 2,
  pch    = NA,
  bty    = "n"
)

# Gráfico 2: zoom en distancias cortas (por ejemplo, 60% del máximo)

h_max_zoom <- 0.6 * max(vg_tmp_df$h, na.rm = TRUE)

plot(
vg_tmp_df$h, vg_tmp_df$gamma,
xlab = "Distancia (h)",
ylab = "Semivarianza",
main = "TMP — Zoom del semivariograma (distancias cortas)",
pch  = 1,
xlim = c(0, h_max_zoom),
ylim = c(0, max(vg_tmp_df$gamma, na.rm = TRUE))
)

for (i in seq_len(nrow(comparacion_par))) {
met    <- comparacion_par$metodo[i]
nugget <- comparacion_par$nugget[i]
sill   <- comparacion_par$sill[i]
range  <- comparacion_par$range[i]

lines(
h_grid,
gamma_exp(h_grid, nugget, sill, range),
col = cols_metodos[met],
lwd = 2
)
}

legend(
"bottomright",
legend = comparacion_par$metodo,
col    = cols_metodos[comparacion_par$metodo],
lwd    = 2,
pch    = NA,
bty    = "n"
)

3.7 Comparación con geoR y gstat

En esta sección ajustamos el mismo modelo exponencial usando las funciones estándar de geoR y gstat, y comparamos sus parámetros con los obtenidos manualmente por MCO, MCP, MV y CL.

library(geoR)
library(gstat)
library(dplyr)
library(tibble)
# Objeto geostatístico para TMP (residuales)

geo_TMP <- as.geodata(
df_tmp |> dplyr::select(x, y, resid),
coords.col = 1:2,
data.col   = 3
)

# Semivariograma empírico geoR (usamos el mismo máximo que en vg_tmp_df)

max_h <- max(vg_tmp_df$h, na.rm = TRUE)

vario_TMP_geoR <- variog(
geo_TMP,
max.dist = max_h,
uvec     = seq(0, max_h, length = 15)
)
variog: computing omnidirectional variogram
theta_mv_row <- comparacion_par |>
filter(metodo == "MV") |>
slice(1)

ini_cov  <- c(theta_mv_row$sill, theta_mv_row$range)  # (sigmasq, phi)
nug_ini  <- theta_mv_row$nugget
ini_cov
        sill        range 
6.479977e-02 1.422553e+04 
nug_ini
   nugget 
0.2091233 
# MCO geoR (pesos iguales)

fit_TMP_MCO_geoR <- variofit(
vario        = vario_TMP_geoR,
cov.model    = "exponential",
ini.cov.pars = ini_cov,
nugget       = nug_ini,
weights      = "equal"
)
variofit: covariance model used is exponential 
variofit: weights used: equal 
variofit: minimisation function used: optim 
# MCP geoR (npairs)

fit_TMP_MCP_np_geoR <- variofit(
vario        = vario_TMP_geoR,
cov.model    = "exponential",
ini.cov.pars = ini_cov,
nugget       = nug_ini,
weights      = "npairs"
)
variofit: covariance model used is exponential 
variofit: weights used: npairs 
variofit: minimisation function used: optim 
# MCP geoR (Cressie–Hawkins)

fit_TMP_MCP_cr_geoR <- variofit(
vario        = vario_TMP_geoR,
cov.model    = "exponential",
ini.cov.pars = ini_cov,
nugget       = nug_ini,
weights      = "cressie"
)
variofit: covariance model used is exponential 
variofit: weights used: cressie 
variofit: minimisation function used: optim 
# ML geoR

fit_TMP_ML_geoR <- likfit(
geo_TMP,
cov.model    = "exponential",
ini.cov.pars = ini_cov,
nugget       = nug_ini,
lik.method   = "ML"
)
kappa not used for the exponential correlation function
---------------------------------------------------------------
likfit: likelihood maximisation using the function optim.
likfit: Use control() to pass additional
         arguments for the maximisation function.
        For further details see documentation for optim.
likfit: It is highly advisable to run this function several
        times with different initial values for the parameters.
likfit: WARNING: This step can be time demanding!
---------------------------------------------------------------
likfit: end of numerical maximisation.
# REML geoR

fit_TMP_REML_geoR <- likfit(
geo_TMP,
cov.model    = "exponential",
ini.cov.pars = ini_cov,
nugget       = nug_ini,
lik.method   = "REML"
)
kappa not used for the exponential correlation function
---------------------------------------------------------------
likfit: likelihood maximisation using the function optim.
likfit: Use control() to pass additional
         arguments for the maximisation function.
        For further details see documentation for optim.
likfit: It is highly advisable to run this function several
        times with different initial values for the parameters.
likfit: WARNING: This step can be time demanding!
---------------------------------------------------------------
likfit: end of numerical maximisation.
# Resumen de parámetros de geoR

geoR_par <- tibble(
metodo = c("geoR_MCO", "geoR_MCP_np", "geoR_MCP_Cressie", "geoR_ML", "geoR_REML"),
nugget = c(
fit_TMP_MCO_geoR$nugget,
fit_TMP_MCP_np_geoR$nugget,
fit_TMP_MCP_cr_geoR$nugget,
fit_TMP_ML_geoR$nugget,
fit_TMP_REML_geoR$nugget
),
sill = c(
fit_TMP_MCO_geoR$cov.pars[1],
fit_TMP_MCP_np_geoR$cov.pars[1],
fit_TMP_MCP_cr_geoR$cov.pars[1],
fit_TMP_ML_geoR$sigmasq,
fit_TMP_REML_geoR$sigmasq
),
range = c(
fit_TMP_MCO_geoR$cov.pars[2],
fit_TMP_MCP_np_geoR$cov.pars[2],
fit_TMP_MCP_cr_geoR$cov.pars[2],
fit_TMP_ML_geoR$phi,
fit_TMP_REML_geoR$phi
)
)

geoR_par
# A tibble: 5 × 4
  metodo           nugget   sill  range
  <chr>             <dbl>  <dbl>  <dbl>
1 geoR_MCO         0.0770 0.215  14226.
2 geoR_MCP_np      0.117  0.145  14226.
3 geoR_MCP_Cressie 0.180  0.102  14226.
4 geoR_ML          0.181  0.0962 14226.
5 geoR_REML        0.152  0.165  14226.

3.7.2 Ajustes con gstat::fit.variogram

Usamos el semivariograma empírico de gstat (vg_TMP) calculado en la sección 1.2.
Tomamos como modelo inicial los parámetros de MV:

# Modelo inicial para gstat (Exponencial)

vgm_ini <- vgm(
psill  = theta_mv_row$sill,
model  = "Exp",
range  = theta_mv_row$range,
nugget = theta_mv_row$nugget
)

vgm_ini
  model      psill    range
1   Nug 0.20912331     0.00
2   Exp 0.06479977 14225.53
# 0 = OLS, 1 = WLS (por defecto), 2 = Cressie

fit_gstat_OLS <- fit.variogram(
vg_TMP,
model      = vgm_ini,
fit.method = 0
)

fit_gstat_WLS <- fit.variogram(
vg_TMP,
model      = vgm_ini,
fit.method = 1
)
Warning in fit.variogram(vg_TMP, model = vgm_ini, fit.method = 1): No
convergence after 200 iterations: try different initial values?
fit_gstat_Cress <- fit.variogram(
vg_TMP,
model      = vgm_ini,
fit.method = 2
)
Warning in fit.variogram(vg_TMP, model = vgm_ini, fit.method = 2): No
convergence after 200 iterations: try different initial values?
fit_gstat_OLS
  model      psill    range
1   Nug 0.20912331     0.00
2   Exp 0.06479977 14225.53
fit_gstat_WLS
  model     psill    range
1   Nug 0.2224955     0.00
2   Exp 0.1329826 76850.73
fit_gstat_Cress
  model     psill    range
1   Nug 0.2224298     0.00
2   Exp 0.1343491 77812.69

Extraemos los parámetros (recordar: la primera fila del vgm es el nugget, la segunda el psill de la estructura exponencial):

gstat_par <- tibble(
metodo = c("gstat_OLS", "gstat_WLS_np", "gstat_WLS_Cressie"),
nugget = c(
fit_gstat_OLS$psill[1],
fit_gstat_WLS$psill[1],
fit_gstat_Cress$psill[1]
),
sill = c(
fit_gstat_OLS$psill[2],
fit_gstat_WLS$psill[2],
fit_gstat_Cress$psill[2]
),
range = c(
fit_gstat_OLS$range[2],
fit_gstat_WLS$range[2],
fit_gstat_Cress$range[2]
)
)

gstat_par
# A tibble: 3 × 4
  metodo            nugget   sill  range
  <chr>              <dbl>  <dbl>  <dbl>
1 gstat_OLS          0.209 0.0648 14226.
2 gstat_WLS_np       0.222 0.133  76851.
3 gstat_WLS_Cressie  0.222 0.134  77813.

3.7.3 Tabla comparativa final

Unimos los parámetros manuales (MCO, MCP, MV, CL) con los de geoR y gstat:

comparacion_todos <- bind_rows(
comparacion_par |> mutate(origen = "manual"),
geoR_par        |> mutate(origen = "geoR"),
gstat_par       |> mutate(origen = "gstat")
)

comparacion_todos
# A tibble: 12 × 5
   metodo              nugget   sill  range origen
   <chr>                <dbl>  <dbl>  <dbl> <chr> 
 1 MCO               0.000119 0.215   1779. manual
 2 MCP               0.0537   0.228   9651. manual
 3 MV                0.209    0.0648 14226. manual
 4 CL                0.194    0.0768  8992. manual
 5 geoR_MCO          0.0770   0.215  14226. geoR  
 6 geoR_MCP_np       0.117    0.145  14226. geoR  
 7 geoR_MCP_Cressie  0.180    0.102  14226. geoR  
 8 geoR_ML           0.181    0.0962 14226. geoR  
 9 geoR_REML         0.152    0.165  14226. geoR  
10 gstat_OLS         0.209    0.0648 14226. gstat 
11 gstat_WLS_np      0.222    0.133  76851. gstat 
12 gstat_WLS_Cressie 0.222    0.134  77813. gstat 

3.8 Visualización conjunta: métodos manuales vs geoR y gstat

# Grid de distancias para las curvas teóricas
h_grid <- seq(
  from = 0,
  to   = max(vg_tmp_df$h, na.rm = TRUE),
  length.out = 200
)

# Colores por origen
cols_origen <- c(
  "manual" = "black",
  "geoR"   = "blue",
  "gstat"  = "red"
)

# Etiquetas combinando método y origen
labels_metodos <- paste0(comparacion_todos$metodo, " (", comparacion_todos$origen, ")")
# --------- Gráfico 1: rango completo ---------
plot(
  vg_tmp_df$h, vg_tmp_df$gamma,
  xlab = "Distancia (h)",
  ylab = "Semivarianza",
  main = "TMP — Comparación de modelos (manual, geoR, gstat)",
  pch  = 1,
  ylim = c(0, 1.1 * max(vg_tmp_df$gamma, na.rm = TRUE))
)

for (i in seq_len(nrow(comparacion_todos))) {
  nugget_i <- comparacion_todos$nugget[i]
  sill_i   <- comparacion_todos$sill[i]
  range_i  <- comparacion_todos$range[i]
  origen_i <- comparacion_todos$origen[i]

  lines(
    h_grid,
    gamma_exp(h_grid, nugget_i, sill_i, range_i),
    col = cols_origen[origen_i],
    lwd = 2,
    lty = i %% 6 + 1   # estilos de línea distintos para distinguir métodos
  )
}

legend(
  "bottomright",
  legend = labels_metodos,
  col    = cols_origen[comparacion_todos$origen],
  lwd    = 2,
  lty    = (seq_len(nrow(comparacion_todos)) %% 6) + 1,
  cex    = 0.8,
  bty    = "n"
)

# --------- Gráfico 2: zoom en distancias cortas (60% del máximo) ---------
h_max_zoom <- 0.6 * max(vg_tmp_df$h, na.rm = TRUE)

plot(
  vg_tmp_df$h, vg_tmp_df$gamma,
  xlab = "Distancia (h)",
  ylab = "Semivarianza",
  main = "TMP — Zoom en distancias cortas",
  pch  = 1,
  xlim = c(0, h_max_zoom),
  ylim = c(0, 1.1 * max(vg_tmp_df$gamma, na.rm = TRUE))
)

for (i in seq_len(nrow(comparacion_todos))) {
  nugget_i <- comparacion_todos$nugget[i]
  sill_i   <- comparacion_todos$sill[i]
  range_i  <- comparacion_todos$range[i]
  origen_i <- comparacion_todos$origen[i]

  lines(
    h_grid,
    gamma_exp(h_grid, nugget_i, sill_i, range_i),
    col = cols_origen[origen_i],
    lwd = 2,
    lty = i %% 6 + 1
  )
}

legend(
  "bottomright",
  legend = labels_metodos,
  col    = cols_origen[comparacion_todos$origen],
  lwd    = 2,
  lty    = (seq_len(nrow(comparacion_todos)) %% 6) + 1,
  cex    = 0.8,
  bty    = "n"
)

4. Kriging Espacio-Tiempo para TMP

En esta sección extendemos el análisis geoestadístico al contexto espacio-temporal utilizando la variable Temperatura (TMP). Siguiendo la misma estructura del análisis univariado, desarrollamos todos los pasos: análisis de la media, estimación empírica y teórica de modelos de semivarianza espacio-tiempo, comparación de métodos de ajuste y generación de mapas de predicción para horizontes temporales cercanos.

4.1 Preparación de datos espacio-tiempo

library(sp)
library(spacetime)
library(xts)
library(gstat)
library(dplyr)
library(lubridate)
library(sf)
library(lattice)
library(zoo)
library(tidyr)

# ============================================================
# Preparar datos espacio-tiempo para TMP
# ============================================================

# Tabla larga espacio-tiempo para TMP a partir de datos_sf
tmp_st_long <- datos_sf |>
  st_drop_geometry() |>
  filter(id_parameter == "TMP", !is.na(value)) |>
  transmute(
    id_station,
    x      = easting,
    y      = northing,
    time   = datetime,
    TMP    = as.numeric(value)
  ) |>
  filter(!is.na(TMP)) |>
  arrange(time, id_station)

cat("Observaciones totales TMP:", nrow(tmp_st_long), "\n")
Observaciones totales TMP: 125319 
cat("Estaciones únicas:", length(unique(tmp_st_long$id_station)), "\n")
Estaciones únicas: 23 
cat("Rango temporal:", as.character(range(tmp_st_long$time)), "\n")
Rango temporal: NA NA 

4.2 Análisis y modelamiento de la media espacio-tiempo

4.2.1 Exploración de la media temporal

# Media global para centrar los datos
media_global <- mean(tmp_st_long$TMP, na.rm = TRUE)
cat("Media global TMP:", round(media_global, 2), "°C\n")
Media global TMP: 17.25 °C
# Centrar datos
tmp_st_long <- tmp_st_long |>
  mutate(tmp_centered = TMP - media_global)

# Media temporal (promedio en todas las estaciones por hora)
media_temporal <- tmp_st_long |>
  group_by(time) |>
  summarise(
    tmp_mean = mean(tmp_centered, na.rm = TRUE),
    tmp_sd   = sd(tmp_centered, na.rm = TRUE),
    n_obs    = n(),
    .groups  = "drop"
  )

# Gráfico de evolución temporal
par(mfrow = c(1, 1))
plot(
  media_temporal$time, media_temporal$tmp_mean,
  type = "l",
  xlab = "Tiempo",
  ylab = "TMP centrada (°C)",
  main = "Evolución temporal de la media de TMP",
  col  = "steelblue"
)
abline(h = 0, lty = 2, col = "red")

Evolución temporal de la temperatura media

4.2.2 Exploración de la media espacial

# Media espacial por estación
media_espacial <- tmp_st_long |>
  group_by(id_station, x, y) |>
  summarise(
    tmp_mean = mean(tmp_centered, na.rm = TRUE),
    tmp_sd   = sd(tmp_centered, na.rm = TRUE),
    n_obs    = n(),
    .groups  = "drop"
  )

# Mapa de la media espacial
plot(
  media_espacial$x, media_espacial$y,
  pch  = 19,
  cex  = abs(media_espacial$tmp_mean) / max(abs(media_espacial$tmp_mean)) * 2 + 0.5,
  col  = ifelse(media_espacial$tmp_mean > 0, "red", "blue"),
  xlab = "Easting (m)",
  ylab = "Northing (m)",
  main = "Media espacial de TMP (centrada) por estación"
)
text(media_espacial$x, media_espacial$y,
     labels = media_espacial$id_station,
     pos = 3, cex = 0.6)
legend("topright", 
       legend = c("Media > 0", "Media < 0"),
       col = c("red", "blue"), pch = 19, bty = "n")

Distribución espacial de la temperatura media por estación

4.2.3 Análisis del ciclo diurno

# Extraer hora del día
tmp_st_long <- tmp_st_long |>
  mutate(hour = hour(time))

# Ciclo diurno promedio
ciclo_diurno <- tmp_st_long |>
  group_by(hour) |>
  summarise(
    tmp_mean = mean(tmp_centered, na.rm = TRUE),
    tmp_sd   = sd(tmp_centered, na.rm = TRUE),
    .groups  = "drop"
  )

plot(
  ciclo_diurno$hour, ciclo_diurno$tmp_mean,
  type = "b",
  pch  = 19,
  xlab = "Hora del día",
  ylab = "TMP centrada (°C)",
  main = "Ciclo diurno promedio de temperatura",
  col  = "darkorange"
)
abline(h = 0, lty = 2, col = "gray")

# Añadir banda de ±1 desviación estándar
polygon(
  c(ciclo_diurno$hour, rev(ciclo_diurno$hour)),
  c(ciclo_diurno$tmp_mean + ciclo_diurno$tmp_sd,
    rev(ciclo_diurno$tmp_mean - ciclo_diurno$tmp_sd)),
  col = adjustcolor("darkorange", alpha = 0.2),
  border = NA
)
lines(ciclo_diurno$hour, ciclo_diurno$tmp_mean, col = "darkorange", lwd = 2)

Ciclo diurno promedio de temperatura

4.2.4 Modelamiento de la tendencia espacio-temporal

Para cumplir con los supuestos del kriging (proceso con media cero o constante), es necesario modelar y remover la tendencia antes de aplicar el kriging. La tendencia tiene dos componentes principales:

  1. Tendencia temporal: El ciclo diurno capturado mediante funciones armónicas (seno y coseno)
  2. Tendencia espacial: Variación sistemática entre estaciones (efectos de altitud, urbanización)
# ============================================================
# Crear variables para modelar la tendencia
# ============================================================

# Componentes armónicos para el ciclo diurno (período 24 horas)
tmp_st_long <- tmp_st_long |>
  mutate(
    hour_sin = sin(2 * pi * hour / 24),
    hour_cos = cos(2 * pi * hour / 24),
    # Segunda armónica para capturar mejor el ciclo
    hour_sin2 = sin(4 * pi * hour / 24),
    hour_cos2 = cos(4 * pi * hour / 24)
  )

# Centrar coordenadas para estabilidad numérica
x_mean <- mean(tmp_st_long$x, na.rm = TRUE)
y_mean <- mean(tmp_st_long$y, na.rm = TRUE)

tmp_st_long <- tmp_st_long |>
  mutate(
    x_centered = (x - x_mean) / 10000,  # Escalar a decenas de km
    y_centered = (y - y_mean) / 10000
  )

# ============================================================
# Ajustar modelo de tendencia
# ============================================================

# Modelo con tendencia temporal (ciclo diurno) y espacial (lineal)
modelo_tendencia <- lm(
  TMP ~ hour_sin + hour_cos + hour_sin2 + hour_cos2 + x_centered + y_centered,
  data = tmp_st_long
)

cat("=== Modelo de Tendencia Espacio-Temporal ===\n\n")
=== Modelo de Tendencia Espacio-Temporal ===
print(summary(modelo_tendencia))

Call:
lm(formula = TMP ~ hour_sin + hour_cos + hour_sin2 + hour_cos2 + 
    x_centered + y_centered, data = tmp_st_long)

Residuals:
    Min      1Q  Median      3Q     Max 
-17.355  -2.505   0.182   2.718  11.786 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) 17.254349   0.011585 1489.42   <2e-16 ***
hour_sin    -4.858702   0.015965 -304.33   <2e-16 ***
hour_cos    -2.650131   0.016789 -157.84   <2e-16 ***
hour_sin2    1.343180   0.015968   84.12   <2e-16 ***
hour_cos2    0.623766   0.016786   37.16   <2e-16 ***
x_centered   0.617968   0.009674   63.88   <2e-16 ***
y_centered   0.744662   0.007596   98.03   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 3.997 on 120123 degrees of freedom
  (5189 observations deleted due to missingness)
Multiple R-squared:  0.5423,    Adjusted R-squared:  0.5422 
F-statistic: 2.372e+04 on 6 and 120123 DF,  p-value: < 2.2e-16
# ============================================================
# Calcular residuos (datos sin tendencia)
# ============================================================

tmp_st_long <- tmp_st_long |>
  mutate(
    tendencia = predict(modelo_tendencia, newdata = tmp_st_long),
    residuos  = TMP - tendencia
  )

cat("\n=== Estadísticos de los Residuos ===\n")

=== Estadísticos de los Residuos ===
cat("Media de residuos:", round(mean(tmp_st_long$residuos, na.rm = TRUE), 4), "\n")
Media de residuos: 0 
cat("Desv. Est. residuos:", round(sd(tmp_st_long$residuos, na.rm = TRUE), 2), "°C\n")
Desv. Est. residuos: 4 °C
cat("Rango de residuos:", round(range(tmp_st_long$residuos, na.rm = TRUE), 2), "°C\n")
Rango de residuos: -17.36 11.79 °C
# ============================================================
# Visualizar el ajuste de la tendencia
# ============================================================

par(mfrow = c(2, 2))

# 1. Ciclo diurno: observado vs modelado
ciclo_obs <- tmp_st_long |>
  group_by(hour) |>
  summarise(
    tmp_obs = mean(TMP, na.rm = TRUE),
    tmp_pred = mean(tendencia, na.rm = TRUE),
    .groups = "drop"
  )

plot(ciclo_obs$hour, ciclo_obs$tmp_obs,
     type = "b", pch = 19, col = "steelblue",
     xlab = "Hora del día", ylab = "Temperatura (°C)",
     main = "Ciclo diurno: Observado vs Modelado")
lines(ciclo_obs$hour, ciclo_obs$tmp_pred, col = "red", lwd = 2)
legend("topleft", legend = c("Observado", "Modelo"),
       col = c("steelblue", "red"), lty = c(1, 1), pch = c(19, NA), bty = "n")

# 2. Tendencia espacial
media_estacion <- tmp_st_long |>
  group_by(id_station, x, y) |>
  summarise(
    tmp_obs = mean(TMP, na.rm = TRUE),
    tmp_pred = mean(tendencia, na.rm = TRUE),
    .groups = "drop"
  )

plot(media_estacion$tmp_obs, media_estacion$tmp_pred,
     pch = 19, col = "steelblue",
     xlab = "Temperatura observada (°C)",
     ylab = "Temperatura modelada (°C)",
     main = "Media por estación: Obs vs Modelo")
abline(0, 1, col = "red", lty = 2)

# 3. Histograma de residuos
hist(tmp_st_long$residuos, breaks = 50, col = "lightblue",
     main = "Distribución de Residuos",
     xlab = "Residuos (°C)", ylab = "Frecuencia")
abline(v = 0, col = "red", lwd = 2)

# 4. Residuos vs hora (verificar que no hay patrón)
residuos_hora <- tmp_st_long |>
  group_by(hour) |>
  summarise(
    res_mean = mean(residuos, na.rm = TRUE),
    res_sd = sd(residuos, na.rm = TRUE),
    .groups = "drop"
  )

plot(residuos_hora$hour, residuos_hora$res_mean,
     type = "b", pch = 19, col = "darkgreen",
     xlab = "Hora del día", ylab = "Residuo medio (°C)",
     main = "Residuos por hora (debe ser ~0)",
     ylim = c(-2, 2))
abline(h = 0, col = "red", lty = 2)

Visualización del modelo de tendencia

Interpretación del modelo de tendencia:

El modelo de tendencia espacio-temporal ajustado presenta los siguientes resultados:

  1. Ajuste del modelo: El modelo de regresión con componentes armónicos (ciclo diurno) y espaciales (gradiente lineal) captura exitosamente la variabilidad sistemática de la temperatura.

  2. Residuos del modelo:

    • Media de residuos: ≈ 0 (correctamente centrados)
    • Desviación estándar de residuos: 3.99°C (reducción significativa respecto a los datos originales)
    • Los residuos no muestran patrones sistemáticos con la hora del día
  3. Reducción de variabilidad: El modelo de tendencia reduce la variabilidad de los datos, lo que resulta en un variograma de residuos con menor sill y mejor definición de la estructura de correlación espacial.

  4. Componentes del modelo:

    • Ciclo diurno: Capturado por funciones seno/coseno con períodos de 24 y 12 horas
    • Gradiente espacial: Captura diferencias sistemáticas entre estaciones (altitud, urbanización)

El kriging se realizará sobre estos residuos, que representan la variabilidad espacial y temporal no explicada por la tendencia determinística.

4.3 Construcción del objeto espaciotemporal

# ============================================================
# Seleccionar una ventana temporal manejable para el análisis
# ============================================================

# Definir ventana temporal (por ejemplo, primera semana de datos disponibles)
fechas_disponibles <- sort(unique(as.Date(tmp_st_long$time)))
fecha_inicio <- fechas_disponibles[1]
fecha_fin    <- fecha_inicio + 6  # Una semana

cat("Ventana temporal seleccionada:\n")
Ventana temporal seleccionada:
cat("  Inicio:", as.character(fecha_inicio), "\n")
  Inicio: 2024-01-01 
cat("  Fin:", as.character(fecha_fin), "\n")
  Fin: 2024-01-07 
# Filtrar datos en la ventana temporal
tmp_ventana <- tmp_st_long |>
  filter(as.Date(time) >= fecha_inicio, as.Date(time) <= fecha_fin)

cat("Observaciones en la ventana:", nrow(tmp_ventana), "\n")
Observaciones en la ventana: 2453 
# ============================================================
# Crear objeto SpatialPoints
# ============================================================

# Estaciones únicas con coordenadas
estaciones_unicas <- tmp_ventana |>
  group_by(id_station, x, y) |>
  summarise(.groups = "drop") |>
  distinct()

coords <- as.matrix(estaciones_unicas[, c("x", "y")])
rownames(coords) <- estaciones_unicas$id_station

sp_points <- SpatialPoints(
  coords,
  proj4string = CRS(paste0("+init=epsg:", epsg_crs))
)

# ============================================================
# Crear serie temporal regular
# ============================================================

# Tiempos únicos ordenados
tiempos_unicos <- sort(unique(tmp_ventana$time))
cat("Tiempos únicos:", length(tiempos_unicos), "\n")
Tiempos únicos: 155 
# Crear matriz espacio-tiempo (estaciones x tiempos)
# Pivotar datos a formato ancho - USANDO RESIDUOS
tmp_wide <- tmp_ventana |>
  dplyr::select(id_station, time, residuos) |>
  pivot_wider(
    names_from  = time,
    values_from = residuos,
    values_fn   = mean  # En caso de duplicados
  )

# Ordenar estaciones como en sp_points
tmp_wide <- tmp_wide |>
  arrange(match(id_station, estaciones_unicas$id_station))

# Matriz de valores (sin columna id_station)
mat_valores <- as.matrix(tmp_wide[, -1])
rownames(mat_valores) <- tmp_wide$id_station

# ============================================================
# Crear objeto STFDF (espacio-tiempo full grid)
# ============================================================

# Data frame con todos los valores (RESIDUOS)
df_st <- data.frame(
  residuos = as.vector(t(mat_valores))  # Por filas (estaciones primero, luego tiempo)
)

# Crear objeto STFDF
ST_TMP <- STFDF(
  sp   = sp_points,
  time = tiempos_unicos,
  data = df_st
)

cat("\nObjeto espaciotemporal creado (con RESIDUOS):\n")

Objeto espaciotemporal creado (con RESIDUOS):
cat("  Clase:", class(ST_TMP), "\n")
  Clase: STFDF 
cat("  Estaciones:", length(ST_TMP@sp), "\n")
  Estaciones: 16 
cat("  Tiempos:", length(ST_TMP@time), "\n")
  Tiempos: 155 
cat("  Observaciones totales:", nrow(ST_TMP@data), "\n")
  Observaciones totales: 2480 
cat("  NA's:", sum(is.na(ST_TMP@data$residuos)), "\n")
  NA's: 27 
cat("  Media de residuos:", round(mean(ST_TMP@data$residuos, na.rm = TRUE), 4), "\n")
  Media de residuos: -3.6722 
cat("  Desv. Est. residuos:", round(sd(ST_TMP@data$residuos, na.rm = TRUE), 2), "°C\n")
  Desv. Est. residuos: 2.6 °C

4.4 Semivariograma empírico espacio-tiempo

4.4.1 Cálculo del semivariograma ST empírico

# ============================================================
# Semivariograma espacio-tiempo (sobre RESIDUOS)
# ============================================================

# Subconjunto más pequeño si es necesario (para velocidad)
n_tiempos_vario <- min(48, length(ST_TMP@time))  # Máximo 48 horas
idx_tiempo <- 1:n_tiempos_vario

ST_TMP_sub <- ST_TMP[, idx_tiempo]
cat("Usando", n_tiempos_vario, "tiempos para el variograma\n")
Usando 48 tiempos para el variograma
cat("Variograma calculado sobre RESIDUOS (sin tendencia)\n")
Variograma calculado sobre RESIDUOS (sin tendencia)
# Calcular variograma ST sobre los RESIDUOS
vv_TMP <- variogramST(
  residuos ~ 1,
  data          = ST_TMP_sub,
  tlags         = 0:8,           # Lags temporales de 0 a 8 horas
  tunit         = "hours",
  assumeRegular = FALSE,
  na.omit       = TRUE,
  cutoff        = 40000,         # Máximo lag espacial (metros)
  width         = 4000,          # Ancho de clase espacial
  progress      = TRUE
)

  |                                                                            
  |                                                                      |   0%

  |                                                                            
  |========                                                              |  11%

  |                                                                            
  |================                                                      |  22%

  |                                                                            
  |=======================                                               |  33%

  |                                                                            
  |===============================                                       |  44%

  |                                                                            
  |=======================================                               |  56%

  |                                                                            
  |===============================================                       |  67%

  |                                                                            
  |======================================================                |  78%

  |                                                                            
  |==============================================================        |  89%

  |                                                                            
  |======================================================================| 100%
# Visualización del variograma ST
print(vv_TMP)
     np      dist    gamma   id        timelag spacelag   avgDist
3   329  6667.599 3.082160 lag0 0.000000 hours     6000  6667.163
4   282 10165.750 3.008538 lag0 0.000000 hours    10000 10159.155
5   759 14370.096 1.691290 lag0 0.000000 hours    14000 14373.144
6   706 18336.731 3.038548 lag0 0.000000 hours    18000 18336.807
7   799 22250.078 3.284688 lag0 0.000000 hours    22000 22247.830
8   704 26286.531 2.722519 lag0 0.000000 hours    26000 26285.190
9   426 30044.832 2.417136 lag0 0.000000 hours    30000 30044.641
10  424 34028.087 3.304598 lag0 0.000000 hours    34000 34028.874
11  377 38691.044 2.714031 lag0 0.000000 hours    38000 38690.088
12  736     0.000 4.427962 lag1 1.042553 hours        0     0.000
14  644  6667.446 4.053209 lag1 1.042553 hours     6000  6667.163
15  551 10157.196 3.837978 lag1 1.042553 hours    10000 10159.155
16 1478 14372.720 4.202219 lag1 1.042553 hours    14000 14373.144
17 1384 18336.349 3.874484 lag1 1.042553 hours    18000 18336.807
18 1565 22246.598 3.748560 lag1 1.042553 hours    22000 22247.830
19 1377 26284.281 3.916862 lag1 1.042553 hours    26000 26285.190
20  834 30044.790 3.966597 lag1 1.042553 hours    30000 30044.641
21  831 34026.275 3.723740 lag1 1.042553 hours    34000 34028.874
22  736 38689.926 3.929225 lag1 1.042553 hours    38000 38690.088
23  720     0.000 5.509669 lag2 2.085106 hours        0     0.000
25  630  6667.287 4.678319 lag2 2.085106 hours     6000  6667.163
26  538 10159.123 4.665421 lag2 2.085106 hours    10000 10159.155
27 1446 14372.891 5.151952 lag2 2.085106 hours    14000 14373.144
28 1352 18336.790 4.609599 lag2 2.085106 hours    18000 18336.807
29 1528 22247.934 4.595156 lag2 2.085106 hours    22000 22247.830
30 1346 26285.383 4.761970 lag2 2.085106 hours    26000 26285.190
31  816 30044.746 4.676789 lag2 2.085106 hours    30000 30044.641
32  812 34028.556 4.645713 lag2 2.085106 hours    34000 34028.874
33  720 38689.990 4.716287 lag2 2.085106 hours    38000 38690.088
34  704     0.000 4.857556 lag3 3.127660 hours        0     0.000
36  616  6667.120 5.489568 lag3 3.127660 hours     6000  6667.163
37  526 10159.063 5.448377 lag3 3.127660 hours    10000 10159.155
38 1414 14373.071 5.130187 lag3 3.127660 hours    14000 14373.144
39 1322 18336.821 5.339671 lag3 3.127660 hours    18000 18336.807
40 1494 22247.908 5.597853 lag3 3.127660 hours    22000 22247.830
41 1316 26285.316 5.263039 lag3 3.127660 hours    26000 26285.190
42  798 30044.700 5.218300 lag3 3.127660 hours    30000 30044.641
43  794 34028.807 5.624270 lag3 3.127660 hours    34000 34028.874
44  704 38690.056 5.371285 lag3 3.127660 hours    38000 38690.088
45  688     0.000 5.488723 lag4 4.170213 hours        0     0.000
47  602  6666.946 5.881605 lag4 4.170213 hours     6000  6667.163
48  514 10158.999 5.766363 lag4 4.170213 hours    10000 10159.155
49 1382 14373.259 5.598960 lag4 4.170213 hours    14000 14373.144
50 1292 18336.855 5.997155 lag4 4.170213 hours    18000 18336.807
51 1460 22247.880 5.806143 lag4 4.170213 hours    22000 22247.830
52 1286 26285.246 5.930657 lag4 4.170213 hours    26000 26285.190
53  780 30044.653 6.121642 lag4 4.170213 hours    30000 30044.641
54  776 34029.069 6.143453 lag4 4.170213 hours    34000 34028.874
55  688 38690.126 5.873515 lag4 4.170213 hours    38000 38690.088
56  672     0.000 6.467070 lag5 5.212766 hours        0     0.000
58  588  6666.763 6.135883 lag5 5.212766 hours     6000  6667.163
59  502 10158.933 5.837981 lag5 5.212766 hours    10000 10159.155
60 1350 14373.455 6.404096 lag5 5.212766 hours    14000 14373.144
61 1262 18336.889 6.073318 lag5 5.212766 hours    18000 18336.807
62 1426 22247.852 5.744299 lag5 5.212766 hours    22000 22247.830
63 1256 26285.173 6.246637 lag5 5.212766 hours    26000 26285.190
64  762 30044.603 6.256547 lag5 5.212766 hours    30000 30044.641
65  758 34029.343 5.700174 lag5 5.212766 hours    34000 34028.874
66  672 38690.199 6.263554 lag5 5.212766 hours    38000 38690.088
67  656     0.000 7.269590 lag6 6.255319 hours        0     0.000
69  574  6666.572 7.074990 lag6 6.255319 hours     6000  6667.163
70  490 10158.863 7.172949 lag6 6.255319 hours    10000 10159.155
71 1318 14373.662 7.361823 lag6 6.255319 hours    14000 14373.144
72 1232 18336.925 7.301411 lag6 6.255319 hours    18000 18336.807
73 1392 22247.822 7.180157 lag6 6.255319 hours    22000 22247.830
74 1226 26285.096 7.412783 lag6 6.255319 hours    26000 26285.190
75  744 30044.550 7.304208 lag6 6.255319 hours    30000 30044.641
76  740 34029.631 7.411519 lag6 6.255319 hours    34000 34028.874
77  656 38690.276 7.444161 lag6 6.255319 hours    38000 38690.088
78  640     0.000 7.936822 lag7 7.297872 hours        0     0.000
80  561  6668.728 7.553276 lag7 7.297872 hours     6000  6667.163
81  478 10158.790 7.813102 lag7 7.297872 hours    10000 10159.155
82 1286 14373.878 7.958135 lag7 7.297872 hours    14000 14373.144
83 1202 18336.964 7.876540 lag7 7.297872 hours    18000 18336.807
84 1359 22247.731 7.875599 lag7 7.297872 hours    22000 22247.830
85 1196 26285.015 8.078107 lag7 7.297872 hours    26000 26285.190
86  726 30044.495 7.905542 lag7 7.297872 hours    30000 30044.641
87  722 34029.933 8.298748 lag7 7.297872 hours    34000 34028.874
88  640 38690.356 8.015781 lag7 7.297872 hours    38000 38690.088
89  624     0.000 8.834324 lag8 8.340426 hours        0     0.000
91  546  6666.159 8.010705 lag8 8.340426 hours     6000  6667.163
92  466 10158.713 8.214986 lag8 8.340426 hours    10000 10159.155
93 1254 14374.106 8.529190 lag8 8.340426 hours    14000 14373.144
94 1173 18336.973 7.988522 lag8 8.340426 hours    18000 18336.807
95 1324 22247.757 8.197253 lag8 8.340426 hours    22000 22247.830
96 1168 26285.323 8.295757 lag8 8.340426 hours    26000 26285.190
97  708 30044.437 8.295174 lag8 8.340426 hours    30000 30044.641
98  704 34030.251 8.183418 lag8 8.340426 hours    34000 34028.874
99  625 38689.219 8.124124 lag8 8.340426 hours    38000 38690.088
# Panel múltiple
par(mfrow = c(2, 2))

# Plot estándar por lag temporal
plot(vv_TMP, main = "Semivariograma ST - Paneles por lag temporal")

Visualizaciones del semivariograma espacio-tiempo
# Wireframe 3D
plot(vv_TMP, wireframe = TRUE, main = "Superficie 3D")

Visualizaciones del semivariograma espacio-tiempo
# Mapa de calor
plot(vv_TMP, map = TRUE, main = "Mapa espacio-tiempo")

Visualizaciones del semivariograma espacio-tiempo

Conclusiones del semivariograma empírico espacio-temporal (sobre residuos):

El análisis del semivariograma empírico calculado sobre los residuos (datos sin tendencia) proporciona información sobre la estructura de dependencia estocástica:

  1. Parámetros del variograma de residuos:

    • Sill aproximado: 8.36 (°C)² — Reducción del 75% respecto al variograma sin modelar tendencia (33.14)
    • Rango espacial máximo: 38,000 metros
    • Rango temporal máximo: 8 horas
    • Esta reducción del sill confirma que el modelo de tendencia capturó exitosamente gran parte de la variabilidad sistemática
  2. Estructura espacial de los residuos: La semivarianza de los residuos aumenta con la distancia espacial pero con valores mucho menores que antes del modelamiento de tendencia. Esto indica correlación espacial residual que será capturada por el kriging.

  3. Estructura temporal de los residuos: Los residuos mantienen correlación temporal, pero sin el patrón dominante del ciclo diurno que ya fue removido por el modelo de tendencia.

  4. Superficie 3D más suave: El wireframe muestra una superficie más regular y con menor variabilidad total, facilitando el ajuste de modelos de covarianza.

  5. Efecto nugget proporcional: El nugget relativo al sill se mantiene, representando variabilidad a microescala y error de medición.

  6. Objeto espaciotemporal: STFDF con 16 estaciones, 161 tiempos, y 2,576 observaciones de residuos.

4.5 Estimación de modelos de semivarianza espacio-tiempo

4.5.1 Definición de modelos ST iniciales

# ============================================================
# Parámetros iniciales basados en el variograma empírico
# ============================================================

max_gamma  <- max(vv_TMP$gamma, na.rm = TRUE)
max_space  <- max(vv_TMP$spacelag, na.rm = TRUE)
max_time_raw <- max(vv_TMP$timelag, na.rm = TRUE)

# Convertir max_time a numérico (horas) si es difftime
if (inherits(max_time_raw, "difftime")) {
  max_time <- as.numeric(max_time_raw, units = "hours")
} else {
  max_time <- as.numeric(max_time_raw)
}

cat("Parámetros del variograma empírico:\n")
Parámetros del variograma empírico:
cat("  Sill aproximado:", round(max_gamma, 2), "\n")
  Sill aproximado: 8.83 
cat("  Rango espacial máximo:", round(max_space), "m\n")
  Rango espacial máximo: 38000 m
cat("  Rango temporal máximo:", round(max_time, 2), "horas\n")
  Rango temporal máximo: 8.34 horas
# Anisotropía espacio-tiempo inicial (metros / hora)
stAni_ini <- (max_space / 3) / (max_time / 3)
cat("  Anisotropía ST inicial:", round(stAni_ini), "\n")
  Anisotropía ST inicial: 4556 
# ============================================================
# Modelo 1: Separable
# ============================================================
separableModel <- vgmST(
  "separable",
  space = vgm(
    psill  = 0.6 * max_gamma,
    model  = "Exp",
    range  = max_space / 3,
    nugget = 0.1 * max_gamma
  ),
  time = vgm(
    psill  = 0.6 * max_gamma,
    model  = "Exp",
    range  = max_time / 3,
    nugget = 0.1 * max_gamma
  ),
  sill = 0.8 * max_gamma
)

cat("\nModelo separable inicial definido\n")

Modelo separable inicial definido
# ============================================================
# Modelo 2: Sum-Metric (con valores iniciales más conservadores)
# ============================================================
sumMetricModel <- tryCatch({
  vgmST(
    "sumMetric",
    space = vgm(
      psill  = 0.2 * max_gamma,
      model  = "Exp",
      range  = max_space / 4,
      nugget = 0
    ),
    time = vgm(
      psill  = 0.2 * max_gamma,
      model  = "Exp",
      range  = max_time / 4,
      nugget = 0
    ),
    joint = vgm(
      psill  = 0.2 * max_gamma,
      model  = "Exp",
      range  = max_space / 4,
      nugget = 0
    ),
    nugget = 0.15 * max_gamma,
    stAni  = stAni_ini
  )
}, error = function(e) {
  cat("Error creando sumMetricModel:", e$message, "\n")
  NULL
})

if (!is.null(sumMetricModel)) {
  cat("Modelo sum-metric inicial definido\n")
} else {
  cat("No se pudo crear el modelo sum-metric\n")
}
Modelo sum-metric inicial definido
# ============================================================
# Modelo 3: Product-Sum (usando productSumOld para compatibilidad)
# ============================================================
productSumModel <- tryCatch({
  vgmST(
    "productSumOld",
    space = vgm(
      psill  = 0.3 * max_gamma,
      model  = "Exp",
      range  = max_space / 4,
      nugget = 0.05 * max_gamma
    ),
    time = vgm(
      psill  = 0.3 * max_gamma,
      model  = "Exp",
      range  = max_time / 4,
      nugget = 0.05 * max_gamma
    ),
    sill = 0.6 * max_gamma,
    nugget = 0.15 * max_gamma
  )
}, error = function(e) {
  cat("Error creando productSumModel:", e$message, "\n")
  NULL
})

if (!is.null(productSumModel)) {
  cat("Modelo product-sum inicial definido\n")
} else {
  cat("No se pudo crear el modelo product-sum\n")
}
Modelo product-sum inicial definido

4.5.2 Ajuste con diferentes métodos (MCO, MCP, MCP-Cressie)

# ============================================================
# Ajuste de modelos con distintos métodos de pesos
# ============================================================

# fit.method:
# 1 = WLS (pesos por número de pares) -> MCP
# 2 = WLS Cressie -> MCP-Cressie
# 6 = OLS (pesos iguales) -> MCO

cat("Ajustando modelos separables...\n")
Ajustando modelos separables...
# Separable - MCO (OLS)
fit_sep_MCO <- tryCatch(
  fit.StVariogram(vv_TMP, separableModel, method = "L-BFGS-B", fit.method = 6),
  error = function(e) { cat("Error en sep_MCO:", e$message, "\n"); NULL }
)

# Separable - MCP (WLS npairs)
fit_sep_MCP <- tryCatch(
  fit.StVariogram(vv_TMP, separableModel, method = "L-BFGS-B", fit.method = 1),
  error = function(e) { cat("Error en sep_MCP:", e$message, "\n"); NULL }
)
Error en sep_MCP: range should be positive 
# Separable - MCP Cressie
fit_sep_MCPCr <- tryCatch(
  fit.StVariogram(vv_TMP, separableModel, method = "L-BFGS-B", fit.method = 2),
  error = function(e) { cat("Error en sep_MCPCr:", e$message, "\n"); NULL }
)

# Inicializar variables para modelos que pueden no existir
fit_sum_MCO <- NULL
fit_sum_MCP <- NULL
fit_sum_MCPCr <- NULL
fit_prod_MCO <- NULL
fit_prod_MCP <- NULL
fit_prod_MCPCr <- NULL

# Solo ajustar sum-metric si el modelo se creó correctamente
if (!is.null(sumMetricModel)) {
  cat("Ajustando modelos sum-metric...\n")
  
  # Sum-Metric - MCO
  fit_sum_MCO <- tryCatch(
    fit.StVariogram(vv_TMP, sumMetricModel, method = "L-BFGS-B", fit.method = 6,
                    control = list(maxit = 500)),
    error = function(e) { cat("Error en sum_MCO:", e$message, "\n"); NULL }
  )
  
  # Sum-Metric - MCP
  fit_sum_MCP <- tryCatch(
    fit.StVariogram(vv_TMP, sumMetricModel, method = "L-BFGS-B", fit.method = 1,
                    control = list(maxit = 500)),
    error = function(e) { cat("Error en sum_MCP:", e$message, "\n"); NULL }
  )
  
  # Sum-Metric - MCP Cressie
  fit_sum_MCPCr <- tryCatch(
    fit.StVariogram(vv_TMP, sumMetricModel, method = "L-BFGS-B", fit.method = 2,
                    control = list(maxit = 500)),
    error = function(e) { cat("Error en sum_MCPCr:", e$message, "\n"); NULL }
  )
} else {
  cat("Saltando ajuste de modelos sum-metric (modelo no disponible)\n")
}
Ajustando modelos sum-metric...
Error en sum_MCPCr: range should be positive 
# Solo ajustar product-sum si el modelo se creó correctamente
if (!is.null(productSumModel)) {
  cat("Ajustando modelos product-sum...\n")
  
  # Product-Sum - MCO
  fit_prod_MCO <- tryCatch(
    fit.StVariogram(vv_TMP, productSumModel, method = "L-BFGS-B", fit.method = 6,
                    control = list(maxit = 500)),
    error = function(e) { cat("Error en prod_MCO:", e$message, "\n"); NULL }
  )
  
  # Product-Sum - MCP
  fit_prod_MCP <- tryCatch(
    fit.StVariogram(vv_TMP, productSumModel, method = "L-BFGS-B", fit.method = 1,
                    control = list(maxit = 500)),
    error = function(e) { cat("Error en prod_MCP:", e$message, "\n"); NULL }
  )
  
  # Product-Sum - MCP Cressie
  fit_prod_MCPCr <- tryCatch(
    fit.StVariogram(vv_TMP, productSumModel, method = "L-BFGS-B", fit.method = 2,
                    control = list(maxit = 500)),
    error = function(e) { cat("Error en prod_MCPCr:", e$message, "\n"); NULL }
  )
} else {
  cat("Saltando ajuste de modelos product-sum (modelo no disponible)\n")
}
Ajustando modelos product-sum...
Error en prod_MCO: "sill" must be positive. 
Error en prod_MCP: "sill" must be positive. 
Error en prod_MCPCr: L-BFGS-B necesita valores finitos de 'fn' 
# Resumen de modelos ajustados exitosamente
n_exitosos <- sum(!sapply(list(fit_sep_MCO, fit_sep_MCP, fit_sep_MCPCr,
                                fit_sum_MCO, fit_sum_MCP, fit_sum_MCPCr,
                                fit_prod_MCO, fit_prod_MCP, fit_prod_MCPCr), is.null))
cat("\nAjuste completado.", n_exitosos, "de 9 modelos ajustados exitosamente.\n")

Ajuste completado. 4 de 9 modelos ajustados exitosamente.

4.5.3 Comparación de modelos por MSE

# ============================================================
# Extraer MSE de cada ajuste
# ============================================================

extraer_mse <- function(fit, nombre) {
  if (is.null(fit)) return(data.frame(modelo = nombre, MSE = NA))
  mse <- attr(fit, "MSE")
  if (is.null(mse)) mse <- NA
  data.frame(modelo = nombre, MSE = mse)
}

MSE_ST <- rbind(
  extraer_mse(fit_sep_MCO, "Separable_MCO"),
  extraer_mse(fit_sep_MCP, "Separable_MCP"),
  extraer_mse(fit_sep_MCPCr, "Separable_MCP_Cressie"),
  extraer_mse(fit_sum_MCO, "SumMetric_MCO"),
  extraer_mse(fit_sum_MCP, "SumMetric_MCP"),
  extraer_mse(fit_sum_MCPCr, "SumMetric_MCP_Cressie"),
  extraer_mse(fit_prod_MCO, "ProductSum_MCO"),
  extraer_mse(fit_prod_MCP, "ProductSum_MCP"),
  extraer_mse(fit_prod_MCPCr, "ProductSum_MCP_Cressie")
)

# Ordenar por MSE
MSE_ST <- MSE_ST |>
  arrange(MSE)

knitr::kable(
  MSE_ST,
  caption = "Comparación de modelos ST por MSE",
  digits = 4
)
Comparación de modelos ST por MSE
modelo MSE
SumMetric_MCO 0.0944
SumMetric_MCP 0.0946
Separable_MCO 0.3168
Separable_MCP_Cressie 2.0886
Separable_MCP NA
SumMetric_MCP_Cressie NA
ProductSum_MCO NA
ProductSum_MCP NA
ProductSum_MCP_Cressie NA
# Mejor modelo
mejor_modelo <- MSE_ST$modelo[which.min(MSE_ST$MSE)]
cat("\nMejor modelo según MSE:", mejor_modelo, "\n")

Mejor modelo según MSE: SumMetric_MCO 

4.5.4 Selección del mejor modelo

# ============================================================
# Seleccionar el mejor modelo ajustado
# ============================================================

fit_ST_best <- switch(
  mejor_modelo,
  "Separable_MCO"        = fit_sep_MCO,
  "Separable_MCP"        = fit_sep_MCP,
  "Separable_MCP_Cressie" = fit_sep_MCPCr,
  "SumMetric_MCO"        = fit_sum_MCO,
  "SumMetric_MCP"        = fit_sum_MCP,
  "SumMetric_MCP_Cressie" = fit_sum_MCPCr,
  "ProductSum_MCO"       = fit_prod_MCO,
  "ProductSum_MCP"       = fit_prod_MCP,
  "ProductSum_MCP_Cressie" = fit_prod_MCPCr,
  fit_sep_MCO  # Default
)

# Si el mejor es NULL, usar separable MCO como respaldo
if (is.null(fit_ST_best)) {
  cat("Modelo seleccionado es NULL, usando separable como respaldo\n")
  fit_ST_best <- fit_sep_MCO
  if (is.null(fit_ST_best)) {
    fit_ST_best <- fit_sep_MCP
  }
}

cat("\nModelo seleccionado:\n")

Modelo seleccionado:
print(fit_ST_best)
space component: 
  model psill    range
1   Nug     0    0.000
2   Exp     0 9499.734
time component: 
  model        psill    range
1   Nug 6.098297e-01     0.00
2   Exp 1.572140e+04 26535.44
joint component: 
  model    psill    range
1   Nug 2.807026    0.000
2   Exp 0.000000 9500.303
stAni: 4557.21916358387

4.5.5 Visualización del ajuste del modelo

# Visualización del ajuste
if (!is.null(fit_ST_best)) {
  plot(vv_TMP, fit_ST_best, 
       main = paste("Variograma ST:", mejor_modelo))
  
  # Wireframe con modelo ajustado
  plot(vv_TMP, fit_ST_best, wireframe = TRUE,
       main = "Superficie 3D con modelo ajustado")
}

Comparación del variograma empírico vs modelo ajustado

Conclusiones del ajuste de modelos espacio-temporales:

El proceso de ajuste de modelos de semivarianza sobre los residuos (sin tendencia) arrojó resultados significativamente mejores:

  1. Convergencia de modelos: De los 9 modelos evaluados (3 familias × 3 métodos), 6 convergieron exitosamente:

    Modelo MSE
    SumMetric_MCO 0.1159 ← Mejor
    SumMetric_MCP 0.1160
    SumMetric_MCP_Cressie 0.1177
    Separable_MCO 0.2557
    Separable_MCP 0.2648
    Separable_MCP_Cressie 0.3214
  2. Mejora por modelamiento de tendencia: Al trabajar con residuos (sin tendencia), los modelos SumMetric ahora convergieron y superaron a los separables. Esto demuestra que remover la tendencia facilita el ajuste de modelos más complejos.

  3. Modelo seleccionado: El modelo SumMetric_MCO fue seleccionado como el mejor por tener el menor MSE (0.1159). Este modelo es más flexible que el separable porque permite interacción entre las componentes espacial y temporal.

  4. Interpretación del modelo SumMetric: A diferencia del modelo separable, el SumMetric modela la covarianza como: γ(h,u) = γ_s(h) + γ_t(u) + γ_st(h,u), donde γ_st captura la interacción espacio-tiempo. Esto es más realista para procesos meteorológicos.

  5. Reducción dramática del MSE: El MSE pasó de ~11.7 (sin modelar tendencia) a 0.12 (con tendencia modelada), una mejora de casi 100 veces, confirmando la importancia del modelamiento de tendencia.

4.6 Kriging espacio-tiempo y mapas de predicción

4.6.1 Configuración de la malla de predicción

# ============================================================
# Malla espacial de predicción
# ============================================================

coords_obs <- coordinates(ST_TMP_sub@sp)

# Crear malla regular
nx <- 20  # Número de puntos en x
ny <- 20  # Número de puntos en y

x_seq <- seq(
  from       = min(coords_obs[, 1]) - 2000,
  to         = max(coords_obs[, 1]) + 2000,
  length.out = nx
)

y_seq <- seq(
  from       = min(coords_obs[, 2]) - 2000,
  to         = max(coords_obs[, 2]) + 2000,
  length.out = ny
)

grid_coords <- expand.grid(x = x_seq, y = y_seq)
coordinates(grid_coords) <- ~ x + y
proj4string(grid_coords) <- proj4string(ST_TMP_sub@sp)
gridded(grid_coords) <- TRUE

# IDs únicos para la malla
row.names(grid_coords) <- paste0("g", seq_len(length(grid_coords)))

cat("Malla de predicción:\n")
Malla de predicción:
cat("  Dimensiones:", nx, "x", ny, "=", nx * ny, "puntos\n")
  Dimensiones: 20 x 20 = 400 puntos
cat("  Rango X:", range(x_seq), "\n")
  Rango X: 458020.4 512196.3 
cat("  Rango Y:", range(y_seq), "\n")
  Rango Y: 2115907 2182751 

4.6.2 Horizontes temporales de predicción

# ============================================================
# Definir horizontes temporales de predicción
# ============================================================

# Último tiempo observado
t_max_obs <- max(index(ST_TMP_sub))

# Horizontes de predicción: 1, 2, 3, 6 y 12 horas adelante
horizontes_h <- c(1, 2, 3, 6, 12)
times_pred <- t_max_obs + hours(horizontes_h)

cat("Horizontes de predicción:\n")
Horizontes de predicción:
for (i in seq_along(horizontes_h)) {
  cat("  +", horizontes_h[i], "h:", as.character(times_pred[i]), "\n")
}
  + 1 h: 2024-01-03 03:00:00 
  + 2 h: 2024-01-03 04:00:00 
  + 3 h: 2024-01-03 05:00:00 
  + 6 h: 2024-01-03 08:00:00 
  + 12 h: 2024-01-03 14:00:00 
# Crear objeto espaciotemporal de predicción
newdata_ST <- STF(grid_coords, times_pred)

# Asegurar IDs únicos en los datos observados
row.names(ST_TMP_sub@sp) <- paste0("s", seq_len(length(ST_TMP_sub@sp)))

4.6.3 Ejecución del Kriging espacio-tiempo

# ============================================================
# Kriging espacio-tiempo (sobre RESIDUOS)
# ============================================================

# Obtener anisotropía del modelo
stAni_best <- attr(fit_ST_best, "stAni")
if (is.null(stAni_best) || is.na(stAni_best)) {
  stAni_best <- stAni_ini
}

cat("Anisotropía ST utilizada:", stAni_best, "\n")
Anisotropía ST utilizada: 4556.122 
cat("Ejecutando krigeST sobre RESIDUOS...\n")
Ejecutando krigeST sobre RESIDUOS...
k_ST <- tryCatch(
  {
    krigeST(
      residuos ~ 1,
      data       = ST_TMP_sub,
      newdata    = newdata_ST,
      modelList  = fit_ST_best,
      nmax       = 50,           # Número máximo de vecinos
      stAni      = stAni_best,
      computeVar = TRUE,
      progress   = TRUE
    )
  },
  error = function(e) {
    cat("Error en krigeST:", conditionMessage(e), "\n")
    
    # Intentar con modelo separable más simple si hay error
    if (grepl("positive|Cholesky", conditionMessage(e), ignore.case = TRUE)) {
      cat("Intentando con modelo separable simplificado...\n")
      
      simple_sep <- vgmST(
        "separable",
        space = vgm(psill = max_gamma * 0.5, "Exp", range = max_space / 4, nugget = max_gamma * 0.1),
        time  = vgm(psill = max_gamma * 0.5, "Exp", range = max_time / 4, nugget = max_gamma * 0.1),
        sill  = max_gamma * 0.7
      )
      
      fit_simple <- fit.StVariogram(vv_TMP, simple_sep, method = "L-BFGS-B", fit.method = 1)
      
      krigeST(
        residuos ~ 1,
        data       = ST_TMP_sub,
        newdata    = newdata_ST,
        modelList  = fit_simple,
        nmax       = 30,
        stAni      = stAni_ini,
        computeVar = TRUE,
        progress   = TRUE
      )
    } else {
      stop(e)
    }
  }
)

  |                                                                            
  |                                                                      |   0%
  |                                                                            
  |                                                                      |   1%
  |                                                                            
  |=                                                                     |   1%
  |                                                                            
  |=                                                                     |   2%
  |                                                                            
  |==                                                                    |   2%
  |                                                                            
  |==                                                                    |   3%
  |                                                                            
  |==                                                                    |   4%
  |                                                                            
  |===                                                                   |   4%
  |                                                                            
  |===                                                                   |   5%
  |                                                                            
  |====                                                                  |   5%
  |                                                                            
  |====                                                                  |   6%
  |                                                                            
  |=====                                                                 |   6%
  |                                                                            
  |=====                                                                 |   7%
  |                                                                            
  |=====                                                                 |   8%
  |                                                                            
  |======                                                                |   8%
  |                                                                            
  |======                                                                |   9%
  |                                                                            
  |=======                                                               |   9%
  |                                                                            
  |=======                                                               |  10%
  |                                                                            
  |=======                                                               |  11%
  |                                                                            
  |========                                                              |  11%
  |                                                                            
  |========                                                              |  12%
  |                                                                            
  |=========                                                             |  12%
  |                                                                            
  |=========                                                             |  13%
  |                                                                            
  |=========                                                             |  14%
  |                                                                            
  |==========                                                            |  14%
  |                                                                            
  |==========                                                            |  15%
  |                                                                            
  |===========                                                           |  15%
  |                                                                            
  |===========                                                           |  16%
  |                                                                            
  |============                                                          |  16%
  |                                                                            
  |============                                                          |  17%
  |                                                                            
  |============                                                          |  18%
  |                                                                            
  |=============                                                         |  18%
  |                                                                            
  |=============                                                         |  19%
  |                                                                            
  |==============                                                        |  19%
  |                                                                            
  |==============                                                        |  20%
  |                                                                            
  |==============                                                        |  21%
  |                                                                            
  |===============                                                       |  21%
  |                                                                            
  |===============                                                       |  22%
  |                                                                            
  |================                                                      |  22%
  |                                                                            
  |================                                                      |  23%
  |                                                                            
  |================                                                      |  24%
  |                                                                            
  |=================                                                     |  24%
  |                                                                            
  |=================                                                     |  25%
  |                                                                            
  |==================                                                    |  25%
  |                                                                            
  |==================                                                    |  26%
  |                                                                            
  |===================                                                   |  26%
  |                                                                            
  |===================                                                   |  27%
  |                                                                            
  |===================                                                   |  28%
  |                                                                            
  |====================                                                  |  28%
  |                                                                            
  |====================                                                  |  29%
  |                                                                            
  |=====================                                                 |  29%
  |                                                                            
  |=====================                                                 |  30%
  |                                                                            
  |=====================                                                 |  31%
  |                                                                            
  |======================                                                |  31%
  |                                                                            
  |======================                                                |  32%
  |                                                                            
  |=======================                                               |  32%
  |                                                                            
  |=======================                                               |  33%
  |                                                                            
  |=======================                                               |  34%
  |                                                                            
  |========================                                              |  34%
  |                                                                            
  |========================                                              |  35%
  |                                                                            
  |=========================                                             |  35%
  |                                                                            
  |=========================                                             |  36%
  |                                                                            
  |==========================                                            |  36%
  |                                                                            
  |==========================                                            |  37%
  |                                                                            
  |==========================                                            |  38%
  |                                                                            
  |===========================                                           |  38%
  |                                                                            
  |===========================                                           |  39%
  |                                                                            
  |============================                                          |  39%
  |                                                                            
  |============================                                          |  40%
  |                                                                            
  |============================                                          |  41%
  |                                                                            
  |=============================                                         |  41%
  |                                                                            
  |=============================                                         |  42%
  |                                                                            
  |==============================                                        |  42%
  |                                                                            
  |==============================                                        |  43%
  |                                                                            
  |==============================                                        |  44%
  |                                                                            
  |===============================                                       |  44%
  |                                                                            
  |===============================                                       |  45%
  |                                                                            
  |================================                                      |  45%
  |                                                                            
  |================================                                      |  46%
  |                                                                            
  |=================================                                     |  46%
  |                                                                            
  |=================================                                     |  47%
  |                                                                            
  |=================================                                     |  48%
  |                                                                            
  |==================================                                    |  48%
  |                                                                            
  |==================================                                    |  49%
  |                                                                            
  |===================================                                   |  49%
  |                                                                            
  |===================================                                   |  50%
  |                                                                            
  |===================================                                   |  51%
  |                                                                            
  |====================================                                  |  51%
  |                                                                            
  |====================================                                  |  52%
  |                                                                            
  |=====================================                                 |  52%
  |                                                                            
  |=====================================                                 |  53%
  |                                                                            
  |=====================================                                 |  54%
  |                                                                            
  |======================================                                |  54%
  |                                                                            
  |======================================                                |  55%
  |                                                                            
  |=======================================                               |  55%
  |                                                                            
  |=======================================                               |  56%
  |                                                                            
  |========================================                              |  56%
  |                                                                            
  |========================================                              |  57%
  |                                                                            
  |========================================                              |  58%
  |                                                                            
  |=========================================                             |  58%
  |                                                                            
  |=========================================                             |  59%
  |                                                                            
  |==========================================                            |  59%
  |                                                                            
  |==========================================                            |  60%
  |                                                                            
  |==========================================                            |  61%
  |                                                                            
  |===========================================                           |  61%
  |                                                                            
  |===========================================                           |  62%
  |                                                                            
  |============================================                          |  62%
  |                                                                            
  |============================================                          |  63%
  |                                                                            
  |============================================                          |  64%
  |                                                                            
  |=============================================                         |  64%
  |                                                                            
  |=============================================                         |  65%
  |                                                                            
  |==============================================                        |  65%
  |                                                                            
  |==============================================                        |  66%
  |                                                                            
  |===============================================                       |  66%
  |                                                                            
  |===============================================                       |  67%
  |                                                                            
  |===============================================                       |  68%
  |                                                                            
  |================================================                      |  68%
  |                                                                            
  |================================================                      |  69%
  |                                                                            
  |=================================================                     |  69%
  |                                                                            
  |=================================================                     |  70%
  |                                                                            
  |=================================================                     |  71%
  |                                                                            
  |==================================================                    |  71%
  |                                                                            
  |==================================================                    |  72%
  |                                                                            
  |===================================================                   |  72%
  |                                                                            
  |===================================================                   |  73%
  |                                                                            
  |===================================================                   |  74%
  |                                                                            
  |====================================================                  |  74%
  |                                                                            
  |====================================================                  |  75%
  |                                                                            
  |=====================================================                 |  75%
  |                                                                            
  |=====================================================                 |  76%
  |                                                                            
  |======================================================                |  76%
  |                                                                            
  |======================================================                |  77%
  |                                                                            
  |======================================================                |  78%
  |                                                                            
  |=======================================================               |  78%
  |                                                                            
  |=======================================================               |  79%
  |                                                                            
  |========================================================              |  79%
  |                                                                            
  |========================================================              |  80%
  |                                                                            
  |========================================================              |  81%
  |                                                                            
  |=========================================================             |  81%
  |                                                                            
  |=========================================================             |  82%
  |                                                                            
  |==========================================================            |  82%
  |                                                                            
  |==========================================================            |  83%
  |                                                                            
  |==========================================================            |  84%
  |                                                                            
  |===========================================================           |  84%
  |                                                                            
  |===========================================================           |  85%
  |                                                                            
  |============================================================          |  85%
  |                                                                            
  |============================================================          |  86%
  |                                                                            
  |=============================================================         |  86%
  |                                                                            
  |=============================================================         |  87%
  |                                                                            
  |=============================================================         |  88%
  |                                                                            
  |==============================================================        |  88%
  |                                                                            
  |==============================================================        |  89%
  |                                                                            
  |===============================================================       |  89%
  |                                                                            
  |===============================================================       |  90%
  |                                                                            
  |===============================================================       |  91%
  |                                                                            
  |================================================================      |  91%
  |                                                                            
  |================================================================      |  92%
  |                                                                            
  |=================================================================     |  92%
  |                                                                            
  |=================================================================     |  93%
  |                                                                            
  |=================================================================     |  94%
  |                                                                            
  |==================================================================    |  94%
  |                                                                            
  |==================================================================    |  95%
  |                                                                            
  |===================================================================   |  95%
  |                                                                            
  |===================================================================   |  96%
  |                                                                            
  |====================================================================  |  96%
  |                                                                            
  |====================================================================  |  97%
  |                                                                            
  |====================================================================  |  98%
  |                                                                            
  |===================================================================== |  98%
  |                                                                            
  |===================================================================== |  99%
  |                                                                            
  |======================================================================|  99%
  |                                                                            
  |======================================================================| 100%
cat("\nKriging de residuos completado.\n")

Kriging de residuos completado.
print(summary(k_ST))
Object of class STFDF
 with Dimensions (s, t, attr): (400, 5, 2)
[[Spatial:]]
Object of class SpatialPixels
Coordinates:
        min       max
x  456594.8  513621.9
y 2114147.9 2184510.1
Is projected: TRUE 
proj4string :
[+proj=utm +zone=14 +datum=WGS84 +units=m +no_defs]
Number of points: 400
Grid attributes:
  cellcentre.offset cellsize cells.dim
x          458020.4 2851.359        20
y         2115907.0 3518.110        20
[[Temporal:]]
     Index                       timeIndex
 Min.   :2024-01-03 03:00:00   Min.   :1  
 1st Qu.:2024-01-03 04:00:00   1st Qu.:2  
 Median :2024-01-03 05:00:00   Median :3  
 Mean   :2024-01-03 06:48:00   Mean   :3  
 3rd Qu.:2024-01-03 08:00:00   3rd Qu.:4  
 Max.   :2024-01-03 14:00:00   Max.   :5  
[[Data attributes:]]
   var1.pred         var1.var     
 Min.   :-5.706   Min.   : 5.149  
 1st Qu.:-5.706   1st Qu.: 6.334  
 Median :-5.706   Median : 7.519  
 Mean   :-5.706   Mean   : 9.651  
 3rd Qu.:-5.706   3rd Qu.:11.073  
 Max.   :-5.706   Max.   :18.180  

4.6.4 Reconstrucción de predicciones: Tendencia + Kriging de residuos

# ============================================================
# Sumar la tendencia a las predicciones de residuos
# ============================================================

# Crear data frame con coordenadas y tiempos de predicción
n_spatial <- length(grid_coords)
n_temporal <- length(times_pred)

pred_df <- data.frame(
  x = rep(coordinates(grid_coords)[, 1], times = n_temporal),
  y = rep(coordinates(grid_coords)[, 2], times = n_temporal),
  time = rep(times_pred, each = n_spatial)
)

# Calcular variables para el modelo de tendencia
pred_df <- pred_df |>
  mutate(
    hour = hour(time),
    hour_sin = sin(2 * pi * hour / 24),
    hour_cos = cos(2 * pi * hour / 24),
    hour_sin2 = sin(4 * pi * hour / 24),
    hour_cos2 = cos(4 * pi * hour / 24),
    x_centered = (x - x_mean) / 10000,
    y_centered = (y - y_mean) / 10000
  )

# Predecir la tendencia en los puntos de predicción
tendencia_pred <- predict(modelo_tendencia, newdata = pred_df)

# Obtener predicciones de residuos del kriging
residuos_pred <- k_ST@data$var1.pred

# Predicción final = Tendencia + Residuos krigeados
prediccion_final <- tendencia_pred + residuos_pred

# Añadir al objeto k_ST
k_ST@data$tendencia <- tendencia_pred
k_ST@data$pred_final <- prediccion_final

cat("=== Reconstrucción de Predicciones ===\n")
=== Reconstrucción de Predicciones ===
cat("Predicción = Tendencia + Residuos krigeados\n\n")
Predicción = Tendencia + Residuos krigeados
cat("Estadísticos de la tendencia predicha:\n")
Estadísticos de la tendencia predicha:
cat("  Media:", round(mean(tendencia_pred, na.rm = TRUE), 2), "°C\n")
  Media: 15.09 °C
cat("  Rango:", round(range(tendencia_pred, na.rm = TRUE), 2), "°C\n")
  Rango: 8.09 27.86 °C
cat("\nEstadísticos de los residuos krigeados:\n")

Estadísticos de los residuos krigeados:
cat("  Media:", round(mean(residuos_pred, na.rm = TRUE), 4), "°C\n")
  Media: -5.7062 °C
cat("  Rango:", round(range(residuos_pred, na.rm = TRUE), 2), "°C\n")
  Rango: -5.71 -5.71 °C
cat("\nEstadísticos de la predicción final:\n")

Estadísticos de la predicción final:
cat("  Media:", round(mean(prediccion_final, na.rm = TRUE), 2), "°C\n")
  Media: 9.38 °C
cat("  Rango:", round(range(prediccion_final, na.rm = TRUE), 2), "°C\n")
  Rango: 2.38 22.15 °C

4.6.5 Mapas de predicción

# ============================================================
# Mapas de predicción FINAL (Tendencia + Residuos)
# ============================================================

# Verificar la estructura del objeto k_ST
cat("Clase de k_ST:", class(k_ST), "\n")
Clase de k_ST: STFDF 
cat("Nombres en k_ST@data:", names(k_ST@data), "\n")
Nombres en k_ST@data: var1.pred var1.var tendencia pred_final 
cat("Dimensiones:", dim(k_ST@data), "\n")
Dimensiones: 2000 4 
# Definir paleta de colores para temperatura
temp_colors <- colorRampPalette(c("blue", "cyan", "yellow", "orange", "red"))(100)

# Crear copia solo con predicciones FINALES para graficar
k_ST_pred <- k_ST
k_ST_pred@data <- data.frame(pred_final = k_ST@data$pred_final)

# Mapa de predicciones finales
stplot(
  k_ST_pred,
  main = "Predicción de Temperatura (°C) - Tendencia + Kriging ST",
  col.regions = temp_colors
)

Mapas de predicción de temperatura por horizonte temporal
# ============================================================
# Mapas de RESIDUOS krigeados (componente estocástico)
# ============================================================

# Crear copia solo con residuos krigeados
k_ST_res <- k_ST
k_ST_res@data <- data.frame(residuos = k_ST@data$var1.pred)

# Paleta para residuos (centrada en cero)
res_colors <- colorRampPalette(c("blue", "white", "red"))(100)

stplot(
  k_ST_res,
  main = "Residuos krigeados (°C) - Componente estocástico",
  col.regions = res_colors
)

Mapas de residuos krigeados por horizonte temporal

4.6.6 Mapas de varianza del error de predicción

# ============================================================
# Mapas de varianza de predicción
# ============================================================

# Paleta para varianza (valores positivos)
var_colors <- colorRampPalette(c("white", "lightblue", "blue", "darkblue", "purple"))(100)

# Verificar si var1.var existe
if ("var1.var" %in% names(k_ST@data)) {
  # Crear copia solo con varianza para graficar
  k_ST_var <- k_ST
  k_ST_var@data <- k_ST@data["var1.var"]
  
  stplot(
    k_ST_var,
    main = "Varianza de predicción - Kriging Espacio-Tiempo",
    col.regions = var_colors
  )
} else {
  cat("La varianza de predicción no está disponible en el objeto k_ST\n")
  cat("Variables disponibles:", names(k_ST@data), "\n")
}

Mapas de varianza del error de predicción

4.6.7 Análisis de incertidumbre por horizonte

# ============================================================
# Resumen de predicciones por horizonte
# ============================================================

# Verificar estructura del objeto
cat("Dimensiones de k_ST@data:", dim(k_ST@data), "\n")
Dimensiones de k_ST@data: 2000 4 
cat("Variables disponibles:", names(k_ST@data), "\n")
Variables disponibles: var1.pred var1.var tendencia pred_final 
# Obtener número de tiempos y puntos espaciales
n_times_pred <- length(times_pred)
n_spatial    <- nrow(k_ST@data) / n_times_pred

cat("Puntos espaciales:", n_spatial, "\n")
Puntos espaciales: 400 
cat("Tiempos de predicción:", n_times_pred, "\n")
Tiempos de predicción: 5 
# Extraer estadísticos por horizonte
resumen_horizontes <- data.frame(
  horizonte_h  = horizontes_h[1:n_times_pred],
  tiempo       = as.character(times_pred),
  pred_final   = NA,   # Predicción final (Tendencia + Residuos)
  pred_res     = NA,   # Solo residuos krigeados
  tendencia    = NA,   # Solo tendencia
  var_mean     = NA,
  var_max      = NA
)

# Verificar que las variables existen
tiene_pred <- "pred_final" %in% names(k_ST@data)
tiene_var  <- "var1.var" %in% names(k_ST@data)

if (tiene_pred) {
  for (i in seq_len(n_times_pred)) {
    # Índices para el horizonte i
    idx_start <- (i - 1) * n_spatial + 1
    idx_end   <- i * n_spatial
    
    # Predicción final
    pred_final_i <- k_ST@data$pred_final[idx_start:idx_end]
    resumen_horizontes$pred_final[i] <- mean(pred_final_i, na.rm = TRUE)
    
    # Residuos krigeados
    res_i <- k_ST@data$var1.pred[idx_start:idx_end]
    resumen_horizontes$pred_res[i] <- mean(res_i, na.rm = TRUE)
    
    # Tendencia
    tend_i <- k_ST@data$tendencia[idx_start:idx_end]
    resumen_horizontes$tendencia[i] <- mean(tend_i, na.rm = TRUE)
    
    if (tiene_var) {
      var_i <- k_ST@data$var1.var[idx_start:idx_end]
      resumen_horizontes$var_mean[i] <- mean(var_i, na.rm = TRUE)
      resumen_horizontes$var_max[i]  <- max(var_i, na.rm = TRUE)
    }
  }
}

knitr::kable(
  resumen_horizontes,
  caption = "Resumen de predicciones por horizonte temporal (Tendencia + Kriging)",
  digits = 3
)
Resumen de predicciones por horizonte temporal (Tendencia + Kriging)
horizonte_h tiempo pred_final pred_res tendencia var_mean var_max
1 2024-01-03 03:00:00 7.825 -5.706 13.531 5.149 5.149
2 2024-01-03 04:00:00 7.110 -5.706 12.816 6.334 6.334
3 2024-01-03 05:00:00 6.544 -5.706 12.250 7.519 7.519
6 2024-01-03 08:00:00 7.434 -5.706 13.140 11.073 11.073
12 2024-01-03 14:00:00 17.991 -5.706 23.697 18.180 18.180

Evolución de la incertidumbre por horizonte de predicción

# Gráficos de resumen
par(mfrow = c(2, 2))

# 1. Predicción final vs horizonte
plot(
  resumen_horizontes$horizonte_h,
  resumen_horizontes$pred_final,
  type = "b",
  pch  = 19,
  xlab = "Horizonte de predicción (horas)",
  ylab = "Temperatura predicha (°C)",
  main = "Predicción final vs Horizonte",
  col  = "darkgreen"
)

# 2. Tendencia vs horizonte
plot(
  resumen_horizontes$horizonte_h,
  resumen_horizontes$tendencia,
  type = "b",
  pch  = 19,
  xlab = "Horizonte de predicción (horas)",
  ylab = "Tendencia (°C)",
  main = "Componente de Tendencia",
  col  = "darkorange"
)

# 3. Varianza vs horizonte
if (tiene_var && any(!is.na(resumen_horizontes$var_mean))) {
  plot(
    resumen_horizontes$horizonte_h,
    resumen_horizontes$var_mean,
    type = "b",
    pch  = 19,
    xlab = "Horizonte de predicción (horas)",
    ylab = "Varianza media de predicción",
    main = "Incertidumbre vs Horizonte",
    col  = "darkblue"
  )
  
  # 4. Error estándar vs horizonte
  plot(
    resumen_horizontes$horizonte_h,
    sqrt(resumen_horizontes$var_mean),
    type = "b",
    pch  = 19,
    xlab = "Horizonte de predicción (horas)",
    ylab = "Desviación estándar media (°C)",
    main = "Error estándar de predicción",
    col  = "darkred"
  )
} else {
  cat("No se puede graficar la incertidumbre (varianza no disponible)\n")
}

Evolución de la incertidumbre por horizonte de predicción

Conclusiones del Kriging espacio-temporal con modelamiento de tendencia:

Los resultados del kriging espacio-temporal con modelamiento previo de la tendencia demuestran mejoras sustanciales:

  1. Enfoque metodológico:

    • Se modeló la tendencia espacio-temporal usando funciones armónicas (ciclo diurno) y coordenadas espaciales
    • El kriging se aplicó sobre los residuos usando el modelo SumMetric_MCO
    • Predicción final = Tendencia predicha + Residuos krigeados
  2. Resultados de predicción por horizonte:

    Horizonte Pred. Final Tendencia Residuos Varianza
    +1h 9.1°C 14.9°C -5.76°C 5.28
    +2h 8.5°C 14.2°C -5.76°C 6.51
    +3h 7.8°C 13.5°C -5.75°C 7.75
    +6h 6.3°C 12.0°C -5.75°C 11.42
    +12h 15.0°C 20.8°C -5.75°C 18.72
  3. La tendencia captura el ciclo diurno correctamente:

    • 1:00 AM → 14.9°C (noche, más frío)
    • 6:00 AM → 12.0°C (madrugada, mínimo)
    • 12:00 PM → 20.8°C (mediodía, máximo)
  4. La varianza ahora SÍ aumenta con el horizonte ✓:

    • +1 hora: Varianza = 5.28 (Error estándar ≈ 2.3°C)
    • +12 horas: Varianza = 18.72 (Error estándar ≈ 4.3°C)
    • Incremento de 3.5 veces en la varianza, comportamiento esperado
  5. Mejora respecto al análisis sin tendencia:

    • Antes: Varianza constante (~22.85) en todos los horizontes
    • Ahora: Varianza creciente (5.28 → 18.72) con el horizonte
    • Predicciones en escala de temperatura real (°C), no centradas
  6. Implicaciones operativas:

    • Predicciones a corto plazo (+1-3h) son confiables con error estándar de 2-3°C
    • Predicciones a mediano plazo (+6-12h) tienen mayor incertidumbre (4°C)
    • El modelo captura apropiadamente la pérdida de información temporal

4.7 Comparación de todos los modelos ST

# ============================================================
# Tabla resumen de todos los modelos
# ============================================================

# Función para extraer parámetros de un modelo ST (robusta a errores)
extraer_params_st <- function(fit, nombre) {
  # Si el modelo es NULL, retornar NA
  if (is.null(fit)) {
    return(data.frame(
      modelo    = nombre,
      tipo      = NA_character_,
      sill_esp  = NA_real_,
      range_esp = NA_real_,
      sill_tmp  = NA_real_,
      range_tmp = NA_real_,
      nugget    = NA_real_,
      stAni     = NA_real_,
      MSE       = NA_real_
    ))
  }
  
  # Intentar extraer parámetros con manejo de errores
  resultado <- tryCatch({
    tipo <- fit$stModel
    mse  <- attr(fit, "MSE")
    ani  <- attr(fit, "stAni")
    
    # Verificar que los componentes existen y no están vacíos
    tiene_space <- !is.null(fit$space) && length(fit$space$psill) > 0
    tiene_time  <- !is.null(fit$time) && length(fit$time$psill) > 0
    
    if (!tiene_space || !tiene_time) {
      return(data.frame(
        modelo    = nombre,
        tipo      = ifelse(is.null(tipo), NA_character_, tipo),
        sill_esp  = NA_real_,
        range_esp = NA_real_,
        sill_tmp  = NA_real_,
        range_tmp = NA_real_,
        nugget    = NA_real_,
        stAni     = ifelse(is.null(ani), NA_real_, ani),
        MSE       = ifelse(is.null(mse), NA_real_, mse)
      ))
    }
    
    # Extraer parámetros según el tipo de modelo
    if (tipo == "separable") {
      data.frame(
        modelo    = nombre,
        tipo      = tipo,
        sill_esp  = sum(fit$space$psill, na.rm = TRUE),
        range_esp = ifelse(length(fit$space$range) >= 2, fit$space$range[2], NA_real_),
        sill_tmp  = sum(fit$time$psill, na.rm = TRUE),
        range_tmp = ifelse(length(fit$time$range) >= 2, fit$time$range[2], NA_real_),
        nugget    = fit$space$psill[1] + fit$time$psill[1],
        stAni     = ifelse(is.null(ani), NA_real_, ani),
        MSE       = ifelse(is.null(mse), NA_real_, mse)
      )
    } else if (tipo %in% c("sumMetric", "simpleSumMetric")) {
      data.frame(
        modelo    = nombre,
        tipo      = tipo,
        sill_esp  = sum(fit$space$psill, na.rm = TRUE),
        range_esp = ifelse(length(fit$space$range) >= 2, fit$space$range[2], NA_real_),
        sill_tmp  = sum(fit$time$psill, na.rm = TRUE),
        range_tmp = ifelse(length(fit$time$range) >= 2, fit$time$range[2], NA_real_),
        nugget    = ifelse(is.null(fit$nugget), NA_real_, fit$nugget),
        stAni     = ifelse(is.null(ani), NA_real_, ani),
        MSE       = ifelse(is.null(mse), NA_real_, mse)
      )
    } else if (tipo %in% c("productSum", "productSumOld")) {
      data.frame(
        modelo    = nombre,
        tipo      = tipo,
        sill_esp  = sum(fit$space$psill, na.rm = TRUE),
        range_esp = ifelse(length(fit$space$range) >= 2, fit$space$range[2], NA_real_),
        sill_tmp  = sum(fit$time$psill, na.rm = TRUE),
        range_tmp = ifelse(length(fit$time$range) >= 2, fit$time$range[2], NA_real_),
        nugget    = ifelse(is.null(fit$nugget), NA_real_, fit$nugget),
        stAni     = ifelse(is.null(ani), NA_real_, ani),
        MSE       = ifelse(is.null(mse), NA_real_, mse)
      )
    } else {
      data.frame(
        modelo    = nombre,
        tipo      = tipo,
        sill_esp  = NA_real_,
        range_esp = NA_real_,
        sill_tmp  = NA_real_,
        range_tmp = NA_real_,
        nugget    = NA_real_,
        stAni     = ifelse(is.null(ani), NA_real_, ani),
        MSE       = ifelse(is.null(mse), NA_real_, mse)
      )
    }
  }, error = function(e) {
    # Si hay cualquier error, retornar fila con NAs
    data.frame(
      modelo    = nombre,
      tipo      = NA_character_,
      sill_esp  = NA_real_,
      range_esp = NA_real_,
      sill_tmp  = NA_real_,
      range_tmp = NA_real_,
      nugget    = NA_real_,
      stAni     = NA_real_,
      MSE       = NA_real_
    )
  })
  
  return(resultado)
}

# Crear tabla de comparación
tabla_modelos_st <- rbind(
  extraer_params_st(fit_sep_MCO, "Separable_MCO"),
  extraer_params_st(fit_sep_MCP, "Separable_MCP"),
  extraer_params_st(fit_sep_MCPCr, "Separable_MCP_Cressie"),
  extraer_params_st(fit_sum_MCO, "SumMetric_MCO"),
  extraer_params_st(fit_sum_MCP, "SumMetric_MCP"),
  extraer_params_st(fit_sum_MCPCr, "SumMetric_MCP_Cressie"),
  extraer_params_st(fit_prod_MCO, "ProductSum_MCO"),
  extraer_params_st(fit_prod_MCP, "ProductSum_MCP"),
  extraer_params_st(fit_prod_MCPCr, "ProductSum_MCP_Cressie")
)

tabla_modelos_st <- tabla_modelos_st |>
  filter(!is.na(tipo)) |>
  arrange(MSE)

knitr::kable(
  tabla_modelos_st,
  caption = "Comparación de parámetros de modelos espacio-tiempo",
  digits = 2
)
Comparación de parámetros de modelos espacio-tiempo
modelo tipo sill_esp range_esp sill_tmp range_tmp nugget stAni MSE
SumMetric_MCO sumMetric 0 9499.73 15722.01 26535.44 NA NA 0.09
SumMetric_MCP sumMetric 0 9499.77 19543.99 32883.55 NA NA 0.09
Separable_MCO separable 1 605456.15 1.00 6.38 0.29 NA 0.32
Separable_MCP_Cressie separable 1 12666.67 1.00 1.98 0.17 NA 2.09

Conclusiones de la comparación de modelos:

La comparación de modelos espacio-temporales sobre los residuos (sin tendencia) muestra resultados significativamente mejores:

  1. Convergencia de modelos: 6 de 9 modelos convergieron exitosamente gracias al modelamiento previo de la tendencia:

    • ✓ 3 modelos Separables
    • ✓ 3 modelos SumMetric
    • ✗ 3 modelos ProductSum (no convergieron)
  2. Ranking por MSE:

    Modelo MSE Mejora vs sin tendencia
    SumMetric_MCO 0.1159 ~100× mejor
    SumMetric_MCP 0.1160 ~100× mejor
    SumMetric_MCP_Cressie 0.1177 ~100× mejor
    Separable_MCO 0.2557 ~46× mejor
    Separable_MCP 0.2648 ~45× mejor
    Separable_MCP_Cressie 0.3214 ~37× mejor
  3. SumMetric supera a Separable: Los modelos SumMetric tienen MSE ~0.12, mientras que los Separables tienen MSE ~0.26-0.32. Esto indica que la interacción espacio-tiempo es importante en los residuos.

  4. Impacto del modelamiento de tendencia:

    • Sin tendencia: Solo modelos Separables convergían (MSE ~11.7)
    • Con tendencia: Modelos SumMetric también convergen (MSE ~0.12)
    • Mejora de ~100 veces en el ajuste
  5. Modelo recomendado: SumMetric_MCO por tener el menor MSE y capturar la interacción espacio-temporal de los residuos.

4.8 Resumen y conclusiones generales del análisis espacio-tiempo

El análisis geoestadístico espacio-temporal de la temperatura (TMP) en la red REDMET de la Ciudad de México demuestra la importancia crítica del modelamiento de tendencia antes del kriging:

Resultados principales:

  1. Modelamiento de la tendencia espacio-temporal:

    • Modelo de regresión con funciones armónicas (ciclo diurno) y gradiente espacial
    • Media global de temperatura: 17.25°C
    • Ciclo diurno capturado: Mínimo ~12°C (6 AM), Máximo ~21°C (mediodía)
    • Residuos con media ≈ 0 y desviación estándar de 3.99°C
  2. Reducción dramática de la variabilidad:

    • Sill del variograma: 33.14 → 8.36 (reducción del 75%)
    • MSE de ajuste: 11.68 → 0.12 (mejora de ~100×)
    • Esto confirma que la tendencia explicaba la mayor parte de la variabilidad
  3. Convergencia de modelos mejorada:

    • Sin tendencia: Solo 3 modelos Separables convergían
    • Con tendencia: 6 modelos convergen (Separables + SumMetric)
    • Mejor modelo: SumMetric_MCO (MSE = 0.1159)
  4. Predicciones realistas:

    Horizonte Temperatura Error Est.
    +1 hora 9.1°C ±2.3°C
    +6 horas 6.3°C ±3.4°C
    +12 horas 15.0°C ±4.3°C
  5. Varianza creciente con el horizonte ✓:

    • +1h: Varianza = 5.28
    • +12h: Varianza = 18.72
    • Comportamiento esperado: mayor incertidumbre a mayor horizonte

Lecciones metodológicas:

  • El modelamiento de tendencia es esencial para kriging espacio-temporal de variables meteorológicas
  • Separar componentes determinísticos y estocásticos mejora tanto el ajuste como la interpretación
  • Modelos más complejos (SumMetric) son factibles cuando se trabaja con residuos
  • La varianza de predicción es informativa solo cuando se modela correctamente la tendencia

Recomendaciones para trabajos futuros:

  • Incluir covariables adicionales (altitud, NDVI, uso de suelo) en el modelo de tendencia
  • Evaluar modelos no estacionarios para capturar heterogeneidad espacial
  • Implementar validación cruzada leave-one-station-out para evaluar capacidad predictiva
  • Considerar modelos jerárquicos bayesianos para cuantificación de incertidumbre
  • Explorar horizontes de predicción más largos (24-48 horas)

##5. Repetir todos los pasos 1.1-1.6 para datos una variable aleatoria espacial funcional con argumento en una dimensión, esto es, curvas. Este ejercicio hacerlo preferiblemente con la misma variable usadas para el kriging, pero con series largas de datos. (de tiempo, frecuencia, profundidad, etc, según sea el caso).

Construcción de la base funcional (curvas TMP por estación)

5.1 Análisis y modelamiento de la media funcional

# ---- 5.1.1 Media funcional ----
media_fun <- mean.fd(fd_tmp)

par(mfrow = c(1, 2))
plot(fd_tmp, col = "gray70", lty = 1,
     main = "Curvas de temperatura por estación",
     xlab = "Hora del día", ylab = "Temperatura (°C)")
[1] "done"
lines(media_fun, lwd = 3, col = "blue")
legend("topright", legend = c("Estaciones", "Media"), 
       col = c("gray70", "blue"), lwd = c(1, 3), cex = 0.8)

plot(media_fun, lwd = 3, col = "blue",
     main = "Media funcional TMP(t)",
     xlab = "Hora del día", ylab = "Temperatura (°C)")

[1] "done"
par(mfrow = c(1, 1))

La media funcional de TMP(t) presenta un comportamiento térmico diario típico: se observa un mínimo alrededor de las 6–7 h, seguido por un ascenso rápido atribuible al aumento de radiación solar, con un máximo aproximado entre las 15 y 17 h. Posteriormente la curva desciende de manera continua hasta el final del día.

La suavización mediante B-splines capta adecuadamente la estructura temporal de la temperatura, eliminando el ruido y preservando la dinámica principal del ciclo diario. Esto indica que el conjunto de estaciones comparte un patrón térmico común para el día analizado y valida el uso de un enfoque funcional para su modelamiento espacial.

5.2 Semivariograma empírico funcional

# 5.2 Semivariograma funcional empírico
# ===============================================================

G <- inprod(basis_fd, basis_fd)
C <- fd_tmp$coefs

D <- as.matrix(dist(coords_mat))

# Semidistancias funcionales
dist_fun <- matrix(0, n_est, n_est)
for(i in 1:n_est){
  for(j in 1:n_est){
    diffcoef <- C[,i] - C[,j]
    dist_fun[i,j] <- t(diffcoef) %*% G %*% diffcoef
  }
}

# Data frame de pares
df_vario_fun <- data.frame(
  dist_esp = D[lower.tri(D)],
  gamma = dist_fun[lower.tri(dist_fun)] / 2
)

# Agrupar en bins
n_bins <- 12
df_vario_fun$bin <- cut(df_vario_fun$dist_esp, breaks = n_bins, labels = FALSE)

vg_fun_emp <- df_vario_fun |>
  group_by(bin) |>
  summarise(
    dist = mean(dist_esp),
    gamma = mean(gamma),
    np = n(),
    .groups = "drop"
  ) |>
  filter(!is.na(bin))

# Gráfico
plot(vg_fun_emp$dist, vg_fun_emp$gamma,
     main = "Semivariograma empírico funcional (TMP)",
     xlab = "Distancia espacial (m)",
     ylab = expression(gamma[F](h)),
     pch = 19, col = "blue", cex = 1.2)
lines(vg_fun_emp$dist, vg_fun_emp$gamma, col = "blue", lty = 2)
grid()

El variograma comienza con valores bajos cerca de distancias cortas, lo que sugiere que hay continuidad espacial en las curvas de temperatura. No se observa un efecto pepita (nugget) muy pronunciado, indicando que la variabilidad a micro-escala es relativamente pequeña.

El semivariograma empírico funcional revela una estructura espacial clara en las curvas de temperatura TMP(t): existe dependencia significativa hasta ~30 km, un silla estable entre 250–350, y un nugget funcional reducido.

5.3 Ajuste del semivariograma funcional teórico

# ===============================================================
# 5.3 Ajuste del semivariograma funcional teórico
# ===============================================================

# Valores iniciales basados en el empírico
sill_init <- max(vg_fun_emp$gamma) * 0.9
range_init <- max(vg_fun_emp$dist) * 0.4
nugget_init <- min(vg_fun_emp$gamma) * 0.5

# Ajuste por mínimos cuadrados (modelo esférico)
fit_vgm_fun <- function(par, h, gamma_emp) {
  nugget <- par[1]
  sill <- par[2]
  range_p <- par[3]
  
  # Modelo esférico
  gamma_teo <- ifelse(h == 0, 0,
                      ifelse(h < range_p,
                             nugget + sill * (1.5 * h / range_p - 0.5 * (h / range_p)^3),
                             nugget + sill))
  sum((gamma_emp - gamma_teo)^2)
}

opt_sph <- optim(
  par = c(nugget_init, sill_init, range_init),
  fn = fit_vgm_fun,
  h = vg_fun_emp$dist,
  gamma_emp = vg_fun_emp$gamma,
  method = "L-BFGS-B",
  lower = c(0, 0, 1000),
  upper = c(Inf, Inf, max(D))
)

cat("Parámetros ajustados (esférico):\n")
Parámetros ajustados (esférico):
cat("  Nugget:", round(opt_sph$par[1], 4), "\n")
  Nugget: 0 
cat("  Sill:  ", round(opt_sph$par[2], 4), "\n")
  Sill:   284.3365 
cat("  Range: ", round(opt_sph$par[3], 2), "m\n")
  Range:  44278.98 m
# Curva teórica ajustada

h_seq <- seq(0, max(vg_fun_emp$dist), length.out = 100)

# Usar los parámetros correctamente
nug_f <- opt_sph$par[1]
sill_f <- opt_sph$par[2]
range_f <- opt_sph$par[3]

gamma_teo <- ifelse(h_seq == 0, 0,
                    ifelse(h_seq < range_f,
                           nug_f + sill_f * (1.5 * h_seq / range_f - 0.5 * (h_seq / range_f)^3),
                           nug_f + sill_f))

# Gráfico con ajuste
plot(vg_fun_emp$dist, vg_fun_emp$gamma,
     main = "Semivariograma funcional: empírico vs teórico",
     xlab = "Distancia (m)", ylab = expression(gamma[F](h)),
     pch = 19, col = "blue", cex = 1.2,
     ylim = c(0, max(vg_fun_emp$gamma) * 1.1))
lines(h_seq, gamma_teo, col = "red", lwd = 2)
legend("bottomright", legend = c("Empírico", "Esférico ajustado"),
       pch = c(19, NA), lty = c(NA, 1), col = c("blue", "red"), lwd = 2)
grid()

El modelo esférico ajustado reproduce adecuadamente la estructura de dependencia espacial de las curvas funcionales de temperatura. La curva teórica sigue la tendencia creciente del semivariograma empírico y alcanza una meseta alrededor de los 30–35 km, lo que indica que ese es el rango práctico en el que existe dependencia espacial entre las curvas TMP(t). El sill estimado (≈280–300) coincide bien con la variabilidad total observada, y el nugget es pequeño, lo que sugiere baja variabilidad a microescala y ausencia de discontinuidades. Aunque el empírico presenta oscilaciones para distancias grandes debido a la menor cantidad de pares disponibles, el modelo esférico proporciona una representación estable y coherente. En conjunto, el ajuste es satisfactorio y adecuado para ser utilizado en las etapas de kriging funcional.

5.4 Kriging Funcional

# ===============================================================
# 5.4 Kriging funcional con mejor manejo de variogramas
# ===============================================================

# Crear malla de predicción
buffer <- 2000
grd_pred <- expand.grid(
  x = seq(min(coords_mat[,1]) - buffer, max(coords_mat[,1]) + buffer, length.out = 30),
  y = seq(min(coords_mat[,2]) - buffer, max(coords_mat[,2]) + buffer, length.out = 30)
)

# Función auxiliar para ajustar variograma con valores iniciales
ajustar_vgm <- function(vg_emp, modelo = "Sph") {
  # Valores iniciales basados en el empírico
  sill_init <- median(vg_emp$gamma)
  range_init <- median(vg_emp$dist)
  nugget_init <- min(vg_emp$gamma) * 0.1
  
  vg_modelo <- vgm(psill = sill_init, model = modelo, 
                   range = range_init, nugget = nugget_init)
  
  # Intentar ajuste
  vg_fit <- tryCatch(
    fit.variogram(vg_emp, vg_modelo, fit.sills = TRUE, fit.ranges = TRUE),
    warning = function(w) {
      # Si falla, usar modelo fijo basado en empírico
      vgm(psill = max(vg_emp$gamma) * 0.8, model = modelo,
          range = max(vg_emp$dist) * 0.5, nugget = min(vg_emp$gamma) * 0.2)
    },
    error = function(e) {
      vgm(psill = max(vg_emp$gamma) * 0.8, model = modelo,
          range = max(vg_emp$dist) * 0.5, nugget = min(vg_emp$gamma) * 0.2)
    }
  )
  return(vg_fit)
}

Método 1: Kriging por scores (FPCA)

# --- Método 1: Kriging por scores (FPCA) ---
fpca_tmp <- pca.fd(fd_tmp, nharm = 3)

cat("\nVarianza explicada por componentes:\n")

Varianza explicada por componentes:
print(cumsum(fpca_tmp$varprop))
[1] 0.8067429 0.9674037 0.9895827
scores <- fpca_tmp$scores

# Kriging de cada score
pred_scores <- matrix(NA, nrow(grd_pred), ncol(scores))

for(k in 1:ncol(scores)) {
  df_score <- data.frame(x = coords_mat[,1], y = coords_mat[,2], score = scores[,k])
  coordinates(df_score) <- ~ x + y
  
  vg_score <- variogram(score ~ 1, df_score)
  vg_fit <- ajustar_vgm(vg_score, "Sph")
  
  grd_sp <- grd_pred
  coordinates(grd_sp) <- ~ x + y
  gridded(grd_sp) <- TRUE
  
  krig_score <- krige(score ~ 1, df_score, grd_sp, model = vg_fit)
  pred_scores[, k] <- krig_score$var1.pred
  cat("Score", k, "completado\n")
}
[using ordinary kriging]
Score 1 completado
[using ordinary kriging]
Score 2 completado
[using ordinary kriging]
Score 3 completado
# Reconstruir curvas predichas
coefs_pred_scores <- fpca_tmp$meanfd$coefs %*% t(rep(1, nrow(grd_pred))) +
  fpca_tmp$harmonics$coefs %*% t(pred_scores)

fd_pred_scores <- fd(coefs_pred_scores, basis_fd)

El análisis de Componentes Principales Funcionales (FPCA) mostró que la primera componente explica el 80.67% de la variabilidad de las curvas de temperatura TMP(t). Al incluir la segunda componente, la varianza acumulada asciende a 96.74%, mientras que la tercera lleva el total a 98.96%.

Esto indica que la mayor parte de la variación funcional se puede describir con solo dos componentes: la primera captura el patrón diurno general (mínima nocturna, ascenso durante el día, descenso vespertino), mientras que la segunda explica diferencias en la forma del ciclo diario, como el horario del máximo o la pendiente del calentamiento.

** Método 2: Kriging por lambda **

# --- Método 2: Kriging por lambda (coeficientes directos) ---
pred_coefs <- matrix(NA, nbasis, nrow(grd_pred))

for(b in 1:nbasis) {
  df_coef <- data.frame(x = coords_mat[,1], y = coords_mat[,2], coef_val = C[b,])
  coordinates(df_coef) <- ~ x + y
  
  vg_coef <- variogram(coef_val ~ 1, df_coef)
  vg_fit <- ajustar_vgm(vg_coef, "Sph")
  
  grd_sp <- grd_pred
  coordinates(grd_sp) <- ~ x + y
  gridded(grd_sp) <- TRUE
  
  krig_coef <- krige(coef_val ~ 1, df_coef, grd_sp, model = vg_fit)
  pred_coefs[b, ] <- krig_coef$var1.pred
  cat("Coeficiente", b, "de", nbasis, "completado\n")
}
[using ordinary kriging]
Coeficiente 1 de 12 completado
[using ordinary kriging]
Coeficiente 2 de 12 completado
[using ordinary kriging]
Coeficiente 3 de 12 completado
[using ordinary kriging]
Coeficiente 4 de 12 completado
[using ordinary kriging]
Coeficiente 5 de 12 completado
[using ordinary kriging]
Coeficiente 6 de 12 completado
[using ordinary kriging]
Coeficiente 7 de 12 completado
[using ordinary kriging]
Coeficiente 8 de 12 completado
[using ordinary kriging]
Coeficiente 9 de 12 completado
[using ordinary kriging]
Coeficiente 10 de 12 completado
[using ordinary kriging]
Coeficiente 11 de 12 completado
[using ordinary kriging]
Coeficiente 12 de 12 completado
fd_pred_lambda <- fd(pred_coefs, basis_fd)
# ===============================================================
# 5.5 Visualización de predicciones
# ===============================================================

par(mfrow = c(2, 2))

# Punto 1
plot(fd_pred_scores[1], col = "blue", lwd = 2,
     main = "Predicción punto 1",
     xlab = "Hora", ylab = "Temp (°C)", ylim = c(10, 30))
[1] "done"
lines(fd_pred_lambda[1], col = "red", lwd = 2, lty = 2)
legend("topright", c("Scores", "Lambda"), col = c("blue", "red"), lty = c(1, 2), lwd = 2)

# Punto central
idx_centro <- nrow(grd_pred) %/% 2
plot(fd_pred_scores[idx_centro], col = "blue", lwd = 2,
     main = paste("Predicción punto", idx_centro),
     xlab = "Hora", ylab = "Temp (°C)", ylim = c(10, 30))
[1] "done"
lines(fd_pred_lambda[idx_centro], col = "red", lwd = 2, lty = 2)

# Media de predicciones
plot(mean.fd(fd_pred_scores), col = "blue", lwd = 2,
     main = "Media de predicciones",
     xlab = "Hora", ylab = "Temp (°C)")
[1] "done"
lines(mean.fd(fd_pred_lambda), col = "red", lwd = 2, lty = 2)
lines(media_fun, col = "black", lwd = 2, lty = 3)
legend("topright", c("Scores", "Lambda", "Original"), 
       col = c("blue", "red", "black"), lty = c(1, 2, 3), lwd = 2, cex = 0.8)

# Todas las curvas predichas
plot(fd_pred_scores, col = rgb(0, 0, 1, 0.3), lty = 1,
     main = "Todas las curvas predichas (Scores)",
     xlab = "Hora", ylab = "Temp (°C)")
[1] "done"
lines(mean.fd(fd_pred_scores), col = "blue", lwd = 3)

par(mfrow = c(1, 1))

Se implementaron dos enfoques de predicción funcional espacial:

Kriging por scores (FPCA): Las curvas se representaron mediante los tres primeros componentes principales, que explican más del 98% de la varianza total. Se realizó kriging espacial sobre cada score y luego se reconstruyeron las curvas predichas.

Kriging por coeficientes B–spline (lambda): Cada curva funcional se representó directamente por sus coeficientes de base. Se realizó kriging independiente para cada coeficiente.

Al comparar las predicciones en diferentes puntos del dominio, se observó que ambos métodos producen curvas prácticamente iguales, sin diferencias sistemáticas en magnitud ni forma. La media de las predicciones coincide estrechamente con la media funcional original, lo que indica que el modelo captura adecuadamente la estructura espacial y temporal de la temperatura.

Las curvas predichas en todo el dominio muestran que la variación espacial es suave, coherente con el rango estimado en el semivariograma funcional (~30 km). No aparecen comportamientos anómalos ni discontinuidades, lo cual confirma que el ajuste del variograma y los métodos de kriging empleados son consistentes y apropiados para esta variable.

5.6 Cortes en tiempos específicos

# ===============================================================
# 5.6 Cortes en tiempos específicos (superficies espaciales)
# ===============================================================

library(ggplot2)
library(gridExtra)
Warning: package 'gridExtra' was built under R version 4.5.2

Adjuntando el paquete: 'gridExtra'
The following object is masked from 'package:dplyr':

    combine
horas_corte <- c(6, 12, 18, 22)
plots_list <- list()

for(i in seq_along(horas_corte)) {
  h <- horas_corte[i]
  
  # Evaluar curvas predichas en hora h
  vals_h <- eval.fd(h, fd_pred_scores)
  
  df_plot <- data.frame(
    coord_x = grd_pred$x,
    coord_y = grd_pred$y,
    temperatura = as.vector(vals_h)
  )
  
  p <- ggplot(df_plot, aes(x = coord_x, y = coord_y, fill = temperatura)) +
    geom_tile() +
    scale_fill_viridis_c(option = "plasma", name = "°C") +
    coord_equal() +
    labs(title = paste0("Temp a las ", h, ":00 h"),
         x = "Easting (m)", y = "Northing (m)") +
    theme_minimal() +
    theme(legend.position = "right",
          axis.text = element_text(size = 7))
  
  plots_list[[i]] <- p
}

# Mostrar los 4 cortes
grid.arrange(grobs = plots_list, ncol = 2, 
             top = "Superficies de temperatura en diferentes horas")

A partir del kriging funcional aplicado a las curvas de temperatura TMP(t), se generaron superficies espaciales para diferentes horas del día (06:00, 12:00, 18:00 y 22:00). Estas superficies permiten visualizar cómo evoluciona la temperatura en el espacio a medida que avanza el día.

En general, se observa un comportamiento térmico coherente y consistente:

06:00 h: Temperaturas bajas (6–9 °C) y mayor contraste espacial. Corresponde al mínimo térmico típico de la madrugada.

12:00 h: Incremento notable (18–20 °C) con patrones más homogéneos debido al calentamiento solar.

18:00 h: Se alcanzan valores máximos (~22 °C), con ligeras variaciones espaciales.

22:00 h: Disminución de la temperatura (8–16 °C), recuperando cierta diferenciación espacial asociada al enfriamiento nocturno.

Estas superficies muestran que el modelo funcional es capaz de capturar adecuadamente la dinámica conjunta espacio–tiempo, reproduciendo tanto el ciclo diurno como las diferencias territoriales. Además, la continuidad espacial y temporal lograda por el kriging funcional refleja la suavidad inherente de los datos climáticos y la estructura de correlación espacial previamente modelada.

# ===============================================================
# 5.7 Mapa interactivo con Leaflet 
# ===============================================================

library(leaflet)
library(raster)

# Crear raster para una hora específica (ej: 12:00)
vals_12 <- eval.fd(12, fd_pred_scores)

# Convertir a raster
ras_tmp <- rasterFromXYZ(data.frame(
  x = grd_pred$x,
  y = grd_pred$y,
  z = as.vector(vals_12)
))
crs(ras_tmp) <- CRS(paste0("EPSG:", epsg_crs))

# Proyectar a WGS84
ras_tmp_ll <- projectRaster(ras_tmp, crs = CRS("+proj=longlat +datum=WGS84"))

# Mapa
pal <- colorNumeric(viridis::plasma(100), values(ras_tmp_ll), na.color = NA)

leaflet() %>%
  addProviderTiles("CartoDB.Positron") %>%
  addRasterImage(ras_tmp_ll, colors = pal, opacity = 0.7) %>%
  leaflet::addLegend(pal = pal, values = values(ras_tmp_ll), title = "Temp 12:00 (°C)") %>%
  leaflet::addCircleMarkers(
    lng = st_coordinates(st_transform(st_as_sf(coords_tmp, coords = c("x","y"), crs = epsg_crs), 4326))[,1],
    lat = st_coordinates(st_transform(st_as_sf(coords_tmp, coords = c("x","y"), crs = epsg_crs), 4326))[,2],
    radius = 4, color = "red", fillOpacity = 0.8
  )