library(fields)
library(geoR)
library(akima)
library(dplyr)
library(sqldf)
library(tidyr)
library(sp)
library(sf)
library(maps)
library(ggplot2)
library(gstat)
library(knitr)
library(kableExtra)
library(mvtnorm)
La calidad del aire es un factor crítico en la salud pública y el
bienestar ambiental.
En particular, el ozono troposférico (O\(_3\)) y el dióxido de nitrógeno (NO\(_2\)) son contaminantes atmosféricos
regulados bajo los Estándares Nacionales de Calidad del Aire Ambiental
(NAAQS, por sus siglas en inglés).
En el presente informe se analizan datos oficiales de la Agencia de
Protección Ambiental de Estados Unidos (EPA) correspondientes al estado
de Texas en el año 2024.
El problema que se plantea es determinar si los niveles de O\(_3\) y NO\(_2\) cumplen con los límites normativos
establecidos, y en qué medida podrían representar un riesgo para la
salud en un momento específico del tiempo.
Se utilizarán dos variables provenientes de la red de monitoreo de calidad del aire en Texas:
Ambas variables se registran diariamente durante el año 2024.
Para efectos de este análisis preliminar, se toma como referencia un
momento específico; es decir, desde el 01 de enero de
2024 hasta el 31 de diciembre de 2024.
load("OZ_coor.RData")
load("NO_coor.RData")
texas <- st_read("Texas_State/State_Boundary.shp")
## Reading layer `State_Boundary' from data source
## `G:\Mi unidad\Camilo UNAL\Estadística Espacial\Proyecto\Primera Entrega\Texas_State\State_Boundary.shp'
## using driver `ESRI Shapefile'
## Simple feature collection with 1 feature and 3 fields
## Geometry type: POLYGON
## Dimension: XY
## Bounding box: xmin: -11871800 ymin: 2978934 xmax: -10409240 ymax: 4369694
## Projected CRS: WGS 84 / Pseudo-Mercator
oz_sf <- st_as_sf(OZ_coor, coords = c("Site.Longitude", "Site.Latitude"), crs = 4326)
no_sf <- st_as_sf(NO_coor, coords = c("Site.Longitude", "Site.Latitude"), crs = 4326)
Se presenta el mapa de Texas con las localizaciones de las estaciones desde las cuales se tomaron los datos.
ggplot() +
geom_sf(data = texas, fill = "gray95", color = "black") +
geom_sf(data = oz_sf, color = "blue", shape = 16, size = 3) +
geom_sf(data = no_sf, color = "red", shape = 17, size = 3) +
labs(title = "Estaciones de Monitoreo de O3 y NO2 en Texas (2024)",
caption = "Fuente: EPA") +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")
Primero, se importan los datos correspondientes al Ozono en el estado de Texas, Estados Unidos.
OZ <- read.csv("OZ.csv")[,-1]
OZ$Ozone <- 1000*OZ$Ozone
OZ$OzC <- residuals(lm(Ozone ~ 1, data = OZ))
OZg <- as.geodata(OZ,
coords.col = 1:2,
data.col = 4)
head(OZ)
## Easting Northing Ozone OzC
## 1 -1663915 3669343 34 -0.3378378
## 2 -1689317 3670665 37 2.6621622
## 3 -1822422 3509765 37 2.6621622
## 4 -1813926 3522080 39 4.6621622
## 5 -1797108 3475404 38 3.6621622
## 6 -1497689 3445674 30 -4.3378378
par(mfrow = c(2, 1), mar = c(2.75, 3, 0.5, 0.5), mgp = c(1.7, 0.7, 0))
plot(x = OZ$Ozone, y = OZ$Northing,
xlab = "Ozono", ylab = "Norte")
plot(x = OZ$Easting, y = OZ$Ozone,
xlab = "Este", ylab = "Ozono")
De estos gráficos es posible ver que la variable presenta estacionariedad con respecto a la media y, además, no se evidencian problemas con respecto a heteroscedasticidad. Por lo tanto, no es necesario ajustar un modelo lineal para intentar alcanzar estacionariedad.
A pesar de haber ajustado el modelo lineal, el Ozono en Texas parece ser estacionario desde un comienzo.
De esta forma, se halla el semivariograma empírico para estos dos casos.
vargrm1 <- variog(OZg, trend = "cte")
## variog: computing omnidirectional variogram
plot(vargrm1, pch = 19, col = "springgreen3",
xlab = "Distancia",
ylab = "Semivarianza",
main = "Variograma empírico de Ozono (O3) en Texas 2024")
lines(vargrm1$u, vargrm1$v, col = "springgreen3")
Como se espera, a puntos cercanos hay menor variabilidad en cuanto a la semivarianza. Se puede ver que se presenta un efecto pepita pequeño, pues la semivarianza no pasa por el orígen. Además, se puede ver que el rango ocurre en aproximadamente \(400.000\) unidades de distancia.
vg1_at <- variog(OZg, trend = "cte",
estimator.type = "modulus")
## variog: computing omnidirectional variogram
plot(vg1_at, pch = 19, col = "springgreen4",
xlab = "Distancia",
ylab = "Semivarianza",
main = "Variograma empírico de Ozono (O3) en Texas 2024
(Robusto)")
lines(vg1_at$u, vg1_at$v, col = "springgreen4")
Es posible ver que es similar al semivariograma basado en el promedio. Esto se puede estar dando debido a que no hay presencia de datos atípicos que estén afectando el nivel medio de la variable.
Con base en la sección de Modelos válidos para el semivariograma, vemos que los que más se asimilan al semivariograma empírico son el Gaussiano y Matern.
Por lo tanto, con ayuda de la función eyefit, inicialmente ajustamos a “ojo” los parámetros del modelo gaussiano que se ajusta a los datos de Ozono:
# initial <- eyefit(vargrm1)
# (silla = 34.88, rango = 9e+05, nugget = 10) Guassiano
# Con el Matern
#cov.model sigmasq phi tausq kappa kappa2 practicalRange
# matern 46.5 1036220.5 10 0.7 <NA> 3571724.96606027
initialOZ1 <- c(c0 = 10, cs = 34.88, a = 9e+05)
initialOZ2 <- c(c0 = 10, cs = 46.5, a = 1036220.5, kappa = 0.7)
initialOZ3 <- c(c0 = 10, cs = 28.42, a = 1727035.38)
Así,
\[ \boldsymbol{\theta}^{(0)}_1=( c_0^{(0)}=10, c_s^{(0)}=34.88, a^{(0)}=900000) \quad \text{(Gaussiano)} \\ \boldsymbol{\theta}^{(0)}_2=(c_0^{(0)}=10, c_s^{(0)}=46.5, a^{(0)}=1036220.5, \kappa^{(0)} = 0.7) \quad \text{(Matern)} \]
Entonces, vemos que el semivariograma teórico ajustado se ve de la siguiente forma:
gaussian_model <- function(h, c0, cs, a){
ifelse(h == 0,
0,
c0 + cs*(1 - exp(-(h/a)^2)))
}
matern_model <- function(h, c0, cs, a, kappa){
ifelse(h == 0,
0,
c0 + cs * (1 - ((h / a)^kappa * besselK(h / a, nu = kappa)) /
(2^(kappa - 1) * gamma(kappa)))
)
}
cubic_model <- function(h, c0, cs, a) {
ifelse(
h == 0,
0,
ifelse(
h <= a,
c0 + cs * (7*(h/a)^2 - 8.75*(h/a)^3 + 3.5*(h/a)^5 - 0.75*(h/a)^7),
c0 + cs
)
)
}
fitvar1 <- gaussian_model(h = vargrm1$u, c0 = 10, cs = 34.88, a = 9e5)
fitvar2 <- matern_model(h = vargrm1$u, c0 = 10, cs = 46.5, a = 1036220.5, kappa = 0.7)
fitvar3 <- cubic_model(h = vargrm1$u, c0 = 10, cs = 28.42, a = 1934000)
plot(vargrm1, pch = 19,
xlab = "Distancia",
ylab = "Semivarianza",
main = "Estimación teórica")
lines(x = vargrm1$u,
y = fitvar1,
col = "orange2",
lwd = 2)
lines(x = vargrm1$u,
y = fitvar2,
col = "orchid3",
lwd = 2)
lines(x = vargrm1$u,
y = fitvar3,
col = "steelblue2",
lwd = 2)
legend("topleft",
legend = c("Gaussiano", "Matern", "Cúbico"),
col = c("orange2", "orchid3", "steelblue2"),
lty = 1,
lwd = 2)
Luego, podemos calcular el Error Cuadrático Medio (MSE) con respecto al semivariograma empírico:
mse_1 <- mean((fitvar1 - vargrm1$v)^2)
mse_2 <- mean((fitvar2 - vargrm1$v)^2)
mse_3 <- mean((fitvar3 - vargrm1$v)^2)
Obteniendo los siguientes resultados:
| Modelo | MSE |
|---|---|
| Gaussiano | 22.89337 |
| Matern | 29.72176 |
| Cúbico | 28.05368 |
Ahora, con los valores iniciales escogidos anteriormente, realizamos la estimación de los parámetros mediante regresión no lineal.
Para el modelo Gaussiano,
data_OZ <- data.frame(h = vargrm1$u,
gamma_est = vargrm1$v,
n = vargrm1$n)
nlinear_gauss <- nls(gamma_est ~ c0 + cs*(1 - exp(-(h/a)^2)),
data = data_OZ,
start = list(c0 = initialOZ1[1],
cs = initialOZ1[2],
a = initialOZ1[3]))
# Matriz de pesos de Cressie
W0_gauss <- (8*gaussian_model(c0 = initialOZ1[1],
cs = initialOZ1[2],
a = initialOZ1[3],
h = data_OZ$h))/data_OZ$n
nlinear_gauss0 <- nls(gamma_est ~ c0 + cs*(1 - exp(-(h/a)^2)),
data = data_OZ,
start = list(c0 = initialOZ1[1],
cs = initialOZ1[2],
a = initialOZ1[3]),
weights = 1/W0_gauss)
# 1/n
W1_gauss <- data_OZ$n
nlinear_gauss1 <- nls(gamma_est ~ c0 + cs*(1 - exp(-(h/a)^2)),
data = data_OZ,
start = list(c0 = initialOZ1[1],
cs = initialOZ1[2],
a = initialOZ1[3]),
weights = 1/W1_gauss)
# h/n
W2_gauss <- data_OZ$n/data_OZ$h
nlinear_gauss2 <- nls(gamma_est ~ c0 + cs*(1 - exp(-(h/a)^2)),
data = data_OZ,
start = list(c0 = initialOZ1[1],
cs = initialOZ1[2],
a = initialOZ1[3]),
weights = 1/W2_gauss)
Y comparamos su MSE:
| Peso | MSE |
|---|---|
| \(1\) | 22.5775 |
| Cressie | 34.7773 |
| \(n\) | 25.8338 |
| \(\frac{n}{h}\) | 31.8803 |
Además de comparar sus gráficas:
plot(vargrm1,
xlab = "Distancia", y = "Semivarianza",
main = "Semivariograma Teórico con el Modelo Gaussiano",
type = "p", pch = 19)
lines(x = data_OZ$h, y = fitted(nlinear_gauss),
lty = "solid", lwd = 2, col = "dodgerblue2")
lines(x = data_OZ$h, y = fitted(nlinear_gauss0),
lty = "dashed", lwd = 2, col = "tomato3")
lines(x = data_OZ$h, y = fitted(nlinear_gauss1),
lty = "dotdash", lwd = 2, col = "springgreen3")
lines(x = data_OZ$h, y = fitted(nlinear_gauss2),
lty = "longdash", lwd = 2, col = "darkorchid2")
legend("bottomright",
legend = c("1", "Cressie", expression(n), expression(frac(n,h))),
col = c("dodgerblue2", "tomato3", "springgreen3", "darkorchid2"),
lwd = 2,
cex = 0.9)
Tanto con la gráfica como con el MSE, nos damos cuenta que el modelo no lineal en el que no se considera una matriz de ponderación se ajusta de mejor forma al semivariograma empírico. Veamos el resúmen de este ajuste:
summary(nlinear_gauss)
##
## Formula: gamma_est ~ c0 + cs * (1 - exp(-(h/a)^2))
##
## Parameters:
## Estimate Std. Error t value Pr(>|t|)
## c0 9.819e+00 3.220e+00 3.049 0.01226 *
## cs 3.451e+01 8.195e+00 4.211 0.00180 **
## a 8.577e+05 2.548e+05 3.366 0.00717 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 5.418 on 10 degrees of freedom
##
## Number of iterations to convergence: 9
## Achieved convergence tolerance: 3.892e-06
Y también los coeficientes estimados por este método:
| Parámetro | Estimación |
|---|---|
| \(c_0\) | 9.8194 |
| \(c_s\) | 34.509 |
| \(a\) | 857720.5474 |
Primero, definimos la función que queremos optimizar. En este caso, queremos minimizar la suma de cuadrados entre el semivariograma empírico y el teórico:
fn_gauss <- function(par, h, gamma){
c0 <- par[1]
cs <- par[2]
a <- par[3]
gamma_hat <- gaussian_model(c0, cs, a, h)
sum((gamma - gamma_hat)^2)
}
Ahora,
optim_gauss <- optim(
par = c(10, 34.88, 900000),
fn = fn_gauss,
h = data_OZ$h,
gamma = data_OZ$gamma_est,
method = "L-BFGS-B",
lower = c(0, 0, 0))
optim_gauss_fit <- optim_gauss$par
Y así encontramos que, con la función optim,
| Parámetro | Estimación |
|---|---|
| \(c_0\) | 9.5136 |
| \(c_s\) | 25.1184 |
| \(a\) | 9e+05 |
Por lo tanto, graficamos el semivariograma teórico con estos parámetros y lo comparamos con el mejor modelo no lineal hallado:
plot(vargrm1,
xlab = "Distancia", y = "Semivarianza",
main = "Semivariograma Teórico con el Modelo Gaussiano\n (comparación optim y nls))",
type = "p", pch = 19)
lines(x = data_OZ$h, y = fitted(nlinear_gauss),
lty = "solid", lwd = 2, col = "dodgerblue2")
lines(x = data_OZ$h,
y = gaussian_model(c0 = optim_gauss_fit[1],
cs = optim_gauss_fit[2],
a = optim_gauss_fit[3],
h = data_OZ$h),
lty = "dashed", lwd = 2, col = "tomato3")
legend("topleft",
legend = c("nls", "optim"),
col = c("dodgerblue2", "tomato3"),
lwd = 2,
cex = 0.9)
| Método | MSE |
|---|---|
| nls | 22.5775210133 |
| optim | 57.8922389274 |
Como nos podemos dar cuenta, ambas estimaciones son muy distintas. Las obtenidas con ayuda de la función optim no se ajustaron muy bien al semivariograma empírico.
Por lo tanto, los semivariogramas teóricos que mejor se ajustaron al empírico fueron los de la regresión no lineal (sin considerar ponderaciones y con los pesos \(1/n\)).
Para este método, es necesario obtener la matriz de distancias y también la matriz de covarianza \(\Sigma(\theta)\).
dist_matrix_OZ <- as.matrix(dist(OZ[, c("Easting", "Northing")]))
sigma2_OZ <- sum(initialOZ1[1:2])
Suponiendo que las observaciones provienen de una distribución normal, se quiere maximizar
\[ l(\theta \, |\,Z) \] Realizamos esto con la ayuda de la función optim:
loglik_gaussOZ <- function(par, Z, coords) {
c0 <- par[1]
cs <- par[2]
a <- par[3]
h <- as.matrix(dist(coords))
Sigma <- cs * exp(-(h / a)^2)
diag(Sigma) <- diag(Sigma) + c0
n <- length(Z)
L <- try(chol(Sigma), silent = TRUE)
if (inherits(L, "try-error")) return(1e6)
SinvZ <- backsolve(L, forwardsolve(t(L), Z))
logdet <- 2 * sum(log(diag(L)))
nll <- 0.5 * (n * log(2*pi) + logdet + sum(Z * SinvZ))
return(nll)
}
optim_gaussOZ <- optim(
par = initialOZ1,
fn = loglik_gaussOZ,
Z = OZ$OzC,
coords = OZ[, c("Easting", "Northing")],
method = "L-BFGS-B",
lower = c(0, 0, 1)
)
optim_gauss <- optim_gaussOZ$par
fit_ml_gaussOZ <- gaussian_model(h = data_OZ$h,
c0 = optim_gauss[1],
cs = optim_gauss[2],
a = optim_gauss[3])
plot(vargrm1)
lines(x = data_OZ$h, y = fit_ml_gaussOZ)
mse_4 <- mean((data_OZ$gamma_est - fit_ml_gaussOZ)^2)
Se ajustan modelos no lineales con ayuda de la función nls().
nlinear_cubic <- nls(gamma_est ~ c0 + cs * (7*(h/a)^2 - 8.75*(h/a)^3
+ 3.5*(h/a)^5 - 0.75*(h/a)^7),
data = data_OZ,
start = list(c0 = initialOZ3[1],
cs = initialOZ3[2],
a = initialOZ3[3]))
# Matriz de pesos de Cressie
W0_cubic <- (8*cubic_model(c0 = initialOZ3[1],
cs = initialOZ3[2],
a = initialOZ3[3],
h = data_OZ$h))/data_OZ$n
nlinear_cubic0 <- nls(gamma_est ~ c0 + cs * (7*(h/a)^2 - 8.75*(h/a)^3
+ 3.5*(h/a)^5 - 0.75*(h/a)^7),
data = data_OZ,
start = list(c0 = initialOZ3[1],
cs = initialOZ3[2],
a = initialOZ3[3]),
weights = 1/W0_cubic)
# 1/n
W1_cubic <- data_OZ$n
nlinear_cubic1 <- nls(gamma_est ~ c0 + cs * (7*(h/a)^2 - 8.75*(h/a)^3
+ 3.5*(h/a)^5 - 0.75*(h/a)^7),
data = data_OZ,
start = list(c0 = initialOZ3[1],
cs = initialOZ3[2],
a = initialOZ3[3]),
weights = 1/W1_cubic)
# h/n
W2_cubic <- data_OZ$n/data_OZ$h
nlinear_cubic2 <- nls(gamma_est ~ c0 + cs * (7*(h/a)^2 - 8.75*(h/a)^3
+ 3.5*(h/a)^5 - 0.75*(h/a)^7),
data = data_OZ,
start = list(c0 = initialOZ3[1],
cs = initialOZ3[2],
a = initialOZ3[3]),
weights = 1/W2_cubic)
| Peso | MSE |
|---|---|
| \(1\) | 22.9361 |
| Cressie | 36.6435 |
| \(n\) | 26.1399 |
| \(\frac{n}{h}\) | 32.1966 |
De esta forma y, al igual que con el modelo gaussiano, se tiene que el ajuste con un menor Error Cuadrático Medio (MSE) fue el que no considera ponderaciones.
De todas formas, si lo comparamos con el obtenido con modelo gaussiano, vemos que este último sigue siendo mejor.
plot(vargrm1,
xlab = "Distancia", y = "Semivarianza",
main = "Comparación de Semivariogramas Teóricos",
type = "p", pch = 19)
lines(x = data_OZ$h, y = fitted(nlinear_gauss),
lty = "solid", lwd = 2, col = "dodgerblue2")
lines(x = data_OZ$h, y = fitted(nlinear_cubic),
lty = "solid", lwd = 2, col = "tomato3")
legend("bottomright",
legend = c(
paste0("Gaussiano (MSE = ", round(temp_nlinear_gauss[1], 4), ")"),
paste0("Cúbico (MSE = ", round(temp_nlinear_cubic[1], 4), ")")
),
col = c("dodgerblue2", "tomato3"),
lwd = 2,
cex = 0.9)
Se define la función a optimizar:
fn_cubic <- function(par, h, gamma_est) {
c0 <- par[1]
cs <- par[2]
a <- par[3]
gamma <- cubic_model(h, c0, cs, a)
mean((gamma_est - gamma)^2)
}
fit_cubic <- optim(
par = initialOZ3,
fn = fn_cubic,
h = data_OZ$h,
gamma_est = data_OZ$gamma_est,
method = "L-BFGS-B", # permite restricciones
lower = c(0, 0, 0), # no negativos
upper = c(Inf, Inf, Inf)
)
optim_cubic <- fit_cubic$par
optim_cubic_svm <- cubic_model(c0 = optim_cubic[1],
cs = optim_cubic[2],
a = optim_cubic[3],
h = data_OZ$h)
temp_gauss <- c(mean((data_OZ$gamma_est - fitted(nlinear_cubic))^2),
mean((data_OZ$gamma_est - optim_cubic_svm)^2))
temp_peso <- c("nls", "optim")
kable(cbind(temp_peso, round(temp_gauss, 4)),
col.names = c("Método", "MSE"),
format = "html",
align = "c",
booktabs = TRUE,
escape = FALSE,
caption = "Comparación nls y optim (Modelo Cúbico)") %>%
kable_styling(position = "center",
full_width = FALSE)
| Método | MSE |
|---|---|
| nls | 22.9361 |
| optim | 23.5749 |
Así, podemos ver que, aunque se aproximan más los MSE, el obtenido por nls es mejor.
loglik_cubicOZ <- function(par, Z, coords) {
c0 <- par[1]
cs <- par[2]
a <- par[3]
h <- as.matrix(dist(coords))
Sigma <- cs * (7*(h/a)^2 - 8.75*(h/a)^3 + 3.5*(h/a)^5 - 0.75*(h/a)^7)
diag(Sigma) <- diag(Sigma) + c0
n <- length(Z)
L <- try(chol(Sigma), silent = TRUE)
if (inherits(L, "try-error")) return(1e6)
SinvZ <- backsolve(L, forwardsolve(t(L), Z))
logdet <- 2 * sum(log(diag(L)))
nll <- 0.5 * (n * log(2*pi) + logdet + sum(Z * SinvZ))
return(nll)
}
optim_cubicOZ <- optim(
par = initialOZ3,
fn = loglik_cubicOZ,
Z = OZ$OzC,
coords = OZ[, c("Easting", "Northing")],
method = "L-BFGS-B",
lower = c(0, 0, 1)
)
ml_cubic <- optim_cubicOZ$par
fit_ml_cubicOZ <- cubic_model(h = data_OZ$h,
c0 = ml_cubic[1],
cs = ml_cubic[2],
a = ml_cubic[3])
plot(vargrm1)
lines(x = data_OZ$h, y = fit_ml_cubicOZ)
mean((data_OZ$gamma_est - fit_ml_cubicOZ)^2)
## [1] 24.53791
Se procede a importar los datos relacionados al dióxido de nitrógeno en el estado de Texas.
NO <- read.csv("NO.csv")[,-1]
NOg <- as.geodata(NO)
head(NO)
## Easting Northing NO
## 1 -1689317 3670665 5.9
## 2 -1822422 3509765 10.4
## 3 -1813926 3522080 1.0
## 4 -1797108 3475404 8.3
## 5 -1798947 3506464 19.9
## 6 -1497689 3445674 20.1
A forma de exploración, analizamos la relación que hay entre el dióxido de carbono y las coordenadas.
par(mfrow = c(2, 1), mar = c(2.75, 3, 0.5, 0.5), mgp = c(1.7, 0.7, 0))
plot(x = NO$NO, y = NO$Northing,
xlab = "Dióxido de Nitrógeno", ylab = "Norte")
plot(x = NO$Easting, y = NO$NO,
xlab = "Este", ylab = "Dióxido de Nitrógeno")
Parece ser estacionario con respecto a la media pues no tiene una tendencia que dependa del espacio.
Al igual que con la variable de ozono, el dióxido de nitrógeno parece tener un comportamiento estacionario a través del espacio desde un principio.
vgOZ_1 <- variog(NOg, trend = "cte")
## variog: computing omnidirectional variogram
plot(vgOZ_1, pch = 19, col = "#CD4F39",
xlab = "Distancia",
ylab = "Semivarianza",
main = "Variograma empírico de NO2 en Texas")
lines(vgOZ_1$u, vgOZ_1$v, col = "#CD4F39")
vgOZ_at <- variog(NOg, trend = "cte",
estimator.type = "modulus")
## variog: computing omnidirectional variogram
plot(vgOZ_at, pch = 19, col = "#912CEE",
xlab = "Distancia",
ylab = "Semivarianza",
main = "Variograma empírico de NO2 en Texas")
lines(vgOZ_at$u, vgOZ_at$v, col = "#912CEE")