Explorando la Autocorrelación Espacial: Índice Global y Local de Moran.

1. Introducción.

La estadística espacial es una rama de la estadística que se enfoca en el análisis de fenómenos geográficos y su distribución en el espacio. A diferencia de la estadística clásica, que generalmente se preocupa por la relación entre variables independientes de la ubicación, la estadística espacial incorpora la dimensión espacial en su análisis, proporcionando herramientas para entender y modelar las dependencias espaciales y la variabilidad en los datos geográficos.

En este contexto, el concepto fundamental es el de autocorrelación espacial, que mide el grado de dependencia entre observaciones geográficas cercanas. En otras palabras, examina si: los valores de una variable en una ubicación están correlacionados con los valores de la misma variable en ubicaciones cercanas.

Este concepto es fundamental en geografía y ciencias espaciales porque muchos fenómenos naturales y sociales exhiben patrones espaciales, donde la proximidad influye en la similitud, o disimilitud.

En términos de resultados, podemos encontrar:

  • Autocorrelación Espacial Positiva: Se presenta cuando las ubicaciones cercanas tienden a tener valores similares. Esto implica que un alto valor en una ubicación está asociado con altos valores en ubicaciones vecinas, y un bajo valor está asociado con bajos valores en vecindades. Este patrón indica agrupamientos o clústeres espaciales de valores similares.

  • Autocorrelación Espacial Negativa: Ocurre cuando las ubicaciones cercanas tienden a tener valores disimilares. En este caso, un alto valor en una ubicación está asociado con bajos valores en ubicaciones vecinas, y viceversa. Este patrón sugiere una dispersión espacial o una alternancia sistemática de valores altos y bajos.

  • Ausencia de Autocorrelación Espacial: Se da cuando no hay un patrón discernible de asociación espacial entre los valores de la variable. Esto significa que los valores de la variable en ubicaciones cercanas no están correlacionados de manera significativa, sugiriendo una distribución aleatoria en el espacio.

En la figura se muestra los resultados posibles de la autocorrelación asociado al patro espacial.

El concepto de autocorrelación esta estrechamente relacionado con La primera ley de Tobler, enunciada por el geógrafo Waldo Tobler en 1970, establece que “todo está relacionado con todo lo demás, pero las cosas más cercanas están más relacionadas que las distantes”. Esta ley subraya la importancia de la proximidad en la relación entre observaciones espaciales, y es el fundamento teórico detrás de la autocorrelación espacial.

2. Indicadores de autocorrelación.

2.1. Índice global de Moran.

El Índice Global de Moran es una medida que evalúa la autocorrelación espacial en todo el espacio de estudio, proporcionando una única estadística que resume el grado de autocorrelación para el conjunto de datos completo. La fórmula del Índice Global de Moran es: \[ I = \frac{N}{W} \frac{\sum_{i} \sum_{j} w_{ij} (x_i - \bar{x})(x_j - \bar{x})}{\sum_{i} (x_i - \bar{x})^2} \] donde: \(( N )\) es el número de ubicaciones,
\(( W )\) es la suma de todos los pesos espaciales \(( w_{ij} )\),
\(( x_i )\) es el valor de la variable en la ubicación \(i\),
\(( \bar{x} )\) es el valor medio de la variable, \(( w_{ij})\) es el peso espacial entre las ubicaciones \(i\) y \(j\).

2.2 Índice Local de Moran.

Sabemos que Indice Golbal de Moran, provee un índice que evalua la autocorrelación espacial en todo el territorio. Sin embargo, a menudo existe interés por proporcionar una medida de similaridad entre cada área cercana. El índice local de asociación espacial, mas conocido como LISA (local indicators of spatial association)(Anselin, 1995) es un indicador que proporciona el alcance de la agrupación espacial significativa de valores similares alrededor de cada observación.

De este modo, el Índice Local de Moran, también conocido como \(I_i\) de Moran, evalúa la autocorrelación espacial en una escala local, permitiendo identificar agrupamientos espaciales y puntos calientes o fríos específicos dentro del área de estudio. La fórmula del Índice Local de Moran es:

\[ I_i = \frac{\sum_{j} w_{ij} (x_j - \bar{x})}{S_0} \cdot (x_i - \bar{x}) \]

donde: \(( x_i)\) es el valor de la variable en la ubicación $( i ),$( {x} )$ es el valor medio de la variable,$( w_{ij} )$ es el peso espacial entre las ubicaciones \(( i )\) y \(( j )\),
\(( S_0 )\) es la suma de todos los pesos espaciales \(( w_{ij})\).

Existen otros indicadores que complementan el índice local de Moran.

  • Índice Local de Geary \((G_i)\)
  • Índice de Localización de Getis-Ord \((G_i)*\)
  • Índice de Localización de Getis-Ord \((G_i)\)

Para mayor profundidad sobre estos conceptos, se sugiere la lectura de

  • La autocorrelación espacial y el desarrollo de la geografía cuantitativa. Siabato & Manrique. Revista colombiana de Geografía.

  • Spatial statistics for data science: Theory and Practice with R. Moraga Paula. CRC Press.

3. Librerias y Datos.

Comenzaremos con la ejecución de las siguientes librerias.

library(spData)
## To access larger datasets in this package, install the spDataLarge
## package with: `install.packages('spDataLarge',
## repos='https://nowosad.github.io/drat/', type='source')`
library(sf)
## Linking to GEOS 3.11.2, GDAL 3.8.2, PROJ 9.3.1; sf_use_s2() is TRUE
library(mapview)

La base de datos corresponde a un archivo shape con información censal (Censo 2017) de la Región Metropolitana, agregada a nivel de manzana censal.

map <- st_read("zona_censal/zona_censal_info_shape.shp", options = "ENCODING=ISO-8859-1")
## options:        ENCODING=ISO-8859-1 
## Reading layer `zona_censal_info_shape' from data source 
##   `C:\Users\cesco\OneDrive\Escritorio\Estadística espacial\zona_censal\zona_censal_info_shape.shp' 
##   using driver `ESRI Shapefile'
## Simple feature collection with 1865 features and 26 fields
## Geometry type: POLYGON
## Dimension:     XY
## Bounding box:  xmin: -71.27263 ymin: -34.03867 xmax: -70.29771 ymax: -33.06787
## CRS:           NA
st_crs(map) <- 4326  

El nombre de los campos en la base de datos, se muestra a continuación.

names(map)
##  [1] "tasa_1"     "REGION"     "NOM_REGION" "PROVINCIA"  "NOM_PROVIN"
##  [6] "COMUNA"     "NOM_COMUNA" "URBANO"     "DISTRITO"   "ZONA"      
## [11] "GEOCODIGO"  "Shape_Leng" "Shape_Area" "region_1"   "provinci_1"
## [16] "comuna_1"   "personas"   "honbres"    "mujeres"    "X0_5A"     
## [21] "X6_14A"     "X15_64A"    "X65_YMAS"   "inmigrante" "pueblo_ori"
## [26] "tasa"       "geometry"

Para este ejercicio, utilizaremos la variable “inmigrante”, que podemos visualizar cartográficamente mediante la función mapview(). Esta visualización corresponde a un gráfico dinámico que también incluye un conjunto de mapas base.

mapview(map, zcol = "inmigrante")

Ahora, haremos algunas manipulaciones:

  1. Calculemos la tasa de migrantes en cada zona censal.
  2. Por simplicidad, a las zonas con “not avaible”, asignaremos un cero. Esto no tiene implicancia en el resultado, pues solo una zona censal presenta not avaible.
map$tasa <- (map$inmigrante / map$personas)*100

map$tasa <- ifelse(is.na(map$tasa), 0 , map$tasa)

mapview(map , zcol = "tasa")

4. Obtención de resultados.

Usaremos la libreria spdep (spatial dependence) para obtener la matriz de vecinos espaciales (objeto nb), a través de la función poly2nb(), contiguidad tipo queen (reina). Recuerden, que existen otras contiguidades como torre, y orden.

Por su parte, el objeto nbw representa a la matriz de pesos espaciales, estilo “W” que pondera los pesos de manera que cada vecino recibe el mismo peso (peso binario). Es decir, si un polígono es vecino de otro, su peso es 1, de lo contrario, es 0.

Finalmente, la claúsula zero.policy = TRUE, permite que los polígonos sin vecinos sean incluidos en el cálculo, asignándoles un peso de 0 en la matriz de pesos espaciales. Esto es útil si algunos polígonos no tienen vecinos y deseas que se les asigne un peso de 0 en la matriz resultante.

library(spdep)
nb <- poly2nb(map,  queen = TRUE)

nbw <- nb2listw(nb, style = "W", zero.policy = TRUE)

La función moran.test() del paquete spdep realiza la prueba de Moran global para evaluar la autocorrelación espacial de una variable.

gmoran <- moran.test(map$tasa, nbw, alternative = "greater")

gmoran
## 
##  Moran I test under randomisation
## 
## data:  map$tasa  
## weights: nbw  
## n reduced by no-neighbour observations  
## 
## Moran I statistic standard deviate = 55.651, p-value < 2.2e-16
## alternative hypothesis: greater
## sample estimates:
## Moran I statistic       Expectation          Variance 
##      0.8022957893     -0.0005464481      0.0002081238

El valor de 0.8023 es positivo y relativamente alto, lo que indica una autocorrelación espacial positiva significativa. Esto significa que los valores similares están agrupados en el espacio, sugiriendo que áreas con valores altos tienden a estar cerca de otras áreas con valores altos, y lo mismo para valores bajos.

La siguiente secuencia de códigos nos permiten obtener los mismo resultados de forma indivdual.

gmoran[["estimate"]][["Moran I statistic"]]
## [1] 0.8022958
gmoran[["statistic"]]
## Moran I statistic standard deviate 
##                           55.65053
gmoran[["p.value"]]
## [1] 0

La función moran.mc() realiza una prueba de Moran basada en simulaciones (también conocida como prueba de Moran con permutaciones o prueba de Moran Monte Carlo). Esta prueba compara el índice de Moran calculado con una distribución simulada para determinar su significancia.

gmoranMC <- moran.mc(map$tasa , nbw, nsim = 999)

gmoranMC
## 
##  Monte-Carlo simulation of Moran I
## 
## data:  map$tasa 
## weights: nbw  
## number of simulations + 1: 1000 
## 
## statistic = 0.8023, observed rank = 1000, p-value = 0.001
## alternative hypothesis: greater

La interpretación del p - valor, indica si el índice de Moran observado es significativamente diferente de lo esperado bajo la hipótesis nula. Un valor p bajo (por ejemplo, menor que 0.05) indica que el índice observado es significativamente diferente de la distribución simulada y sugiere la presencia de autocorrelación espacial significativa. Como es en este ejemplo que estamos desarrollando.

Ahora, podemos graficar el histograma de los residuos, con la finalidad de proporcionar una visualización gráfica de la distribución empírica del índice de Moran bajo la hipótesis nula, obtenida a partir de las simulaciones. Esto te permite ver cómo se distribuyen los valores simulados y comparar esta distribución con el índice de Moran observado.

hist(gmoranMC$res)

abline( v = gmoranMC$statistic, col = "red")

La función moran.plot() genera un gráfico de Moran para una variable espacial. El gráfico de Moran muestra la relación entre los valores de la variable en un área y los valores promedio de la variable en los sitios vecinos.

moran.plot(map$tasa , nbw)

La finalidad del gráfico de Moran es visualizar la relación entre los valores observados y los valores esperados en función de los vecinos, lo que ayuda a interpretar la autocorrelación espacial de los datos. Línea de Regresiónen el gráfico indica la relación entre los valores observados y los valores promedio de los vecinos. Una pendiente positiva de la línea de regresión indica autocorrelación espacial positiva (valores similares tienden a agruparse), mientras que una pendiente negativa indica autocorrelación espacial negativa (valores opuestos tienden a agruparse).

La función localmoran(), permite calcular el Índice Local de Moran para cada ubicación en los datos. Este índice mide la autocorrelación espacial en un nivel local, permitiendo identificar clusters de valores altos o bajos y áreas de valores inesperadamente diferentes.

lmoran <- localmoran(map$tasa , nbw , alternative = "greater")

head(lmoran)
##           Ii          E.Ii      Var.Ii      Z.Ii Pr(z > E(Ii))
## 1 0.07636340 -4.670323e-05 0.021739303 0.5182365     0.3021466
## 2 0.03336343 -8.468651e-06 0.007892712 0.3756365     0.3535936
## 3 0.06840846 -2.989059e-05 0.011124918 0.6488602     0.2582144
## 4 0.06326282 -1.827202e-05 0.011346706 0.5940720     0.2762320
## 5 0.06408855 -2.337027e-05 0.008698193 0.6874233     0.2459080
## 6 0.00000000  0.000000e+00 0.000000000       NaN           NaN

Para visualziar los resultados, usaremos el paquete tmap.

library(tmap)

tmap_mode("plot")
map$lmI <- lmoran[ ,"Ii"]
map$lmZ <- lmoran[ , "Z.Ii"]
map$lmp <- lmoran[ , "Pr(z > E(Ii))"]
P1 <- tm_shape(map) + 
  tm_polygons(col = "tasa" , title = "tasa" , style = "quantile") +
  tm_layout(legend.outside = TRUE)
P2 <- tm_shape(map) + 
  tm_polygons(col = "lmI" , title = "Local Moran's I" , style = "quantile") +
  tm_layout(legend.outside = TRUE)
P3 <- tm_shape(map) + 
  tm_polygons(col = "lmZ" , title = "Z - score" , breaks = c(-Inf, 0.05, Inf)) +
  tm_layout(legend.outside = TRUE)
P4 <- tm_shape(map) + 
  tm_polygons(col = "lmp" , title = "p - value" , breaks = c(-Inf, 0.05, Inf)) +
  tm_layout(legend.outside = TRUE)
tmap_arrange(P1, P2, P3, P4)
## Variable(s) "lmI" contains positive and negative values, so midpoint is set to 0. Set midpoint = NA to show the full spectrum of the color palette.
## Variable(s) "lmZ" contains positive and negative values, so midpoint is set to 0. Set midpoint = NA to show the full spectrum of the color palette.
## Variable(s) "lmI" contains positive and negative values, so midpoint is set to 0. Set midpoint = NA to show the full spectrum of the color palette.
## Variable(s) "lmZ" contains positive and negative values, so midpoint is set to 0. Set midpoint = NA to show the full spectrum of the color palette.

tm_shape(map) + tm_polygons(col = "lmZ",
                            tittle = "Local Moran's I", style = "fixed",
                            breaks = c(-Inf, -1.96 , 1.96, Inf),
                            labels = c("Autocorrelacion Negativa" , "Ausencia de Autocorrelación" , "Autocorrelación Positiva"),
                            palette = c("blue", "white", "red")) +
  tm_layout(legend.outside = TRUE)

lmoran <- localmoran(map$tasa , nbw, alternative = "two.sided")

head(lmoran)
##           Ii          E.Ii      Var.Ii      Z.Ii Pr(z != E(Ii))
## 1 0.07636340 -4.670323e-05 0.021739303 0.5182365      0.6042933
## 2 0.03336343 -8.468651e-06 0.007892712 0.3756365      0.7071871
## 3 0.06840846 -2.989059e-05 0.011124918 0.6488602      0.5164287
## 4 0.06326282 -1.827202e-05 0.011346706 0.5940720      0.5524640
## 5 0.06408855 -2.337027e-05 0.008698193 0.6874233      0.4918160
## 6 0.00000000  0.000000e+00 0.000000000       NaN            NaN
map$lmp <- lmoran[ , 5]
mp <- moran.plot(as.vector(scale(map$tasa)) , nbw)

head(mp)
##            x         wx is_inf labels       dfb.1_         dfb.x        dffit
## 1 -0.2949714 -0.2587452  FALSE      1 -0.002411918  0.0007116377 -0.002514712
## 2 -0.1256069 -0.2654753  FALSE      2 -0.011078001  0.0013918469 -0.011165094
## 3 -0.2359792 -0.2897365  FALSE      3 -0.007171177  0.0016927024 -0.007368244
## 4 -0.1845015 -0.3427014  FALSE      4 -0.012899917  0.0023806919 -0.013117757
## 5 -0.2086596 -0.3069793  FALSE      5 -0.009550986  0.0019934395 -0.009756799
## 6 -0.7083809  0.0000000  FALSE      6  0.033491749 -0.0237312794  0.041047179
##       cov.r       cook.d          hat
## 1 1.0016466 3.163568e-06 0.0005828712
## 2 1.0013739 6.235548e-05 0.0005446571
## 3 1.0015383 2.715869e-05 0.0005660676
## 4 1.0012961 8.606965e-05 0.0005544553
## 5 1.0014520 4.761878e-05 0.0005595508
## 6 0.9996357 8.419427e-04 0.0008054009
map$quadrant <- NA

# Manejamos los valores NA en las condiciones lógicas y realizar las asignaciones

cond1 <- (mp$x >= 0 & mp$wx >= 0) & (!is.na(map$lmp) & map$lmp <= 0.05)
map[cond1, "quadrant"] <- 1

cond2 <- (mp$x <= 0 & mp$wx <= 0) & (!is.na(map$lmp) & map$lmp <= 0.05)
map[cond2, "quadrant"] <- 2

cond3 <- (mp$x >= 0 & mp$wx <= 0) & (!is.na(map$lmp) & map$lmp <= 0.05)
map[cond3, "quadrant"] <- 3

cond4 <- (mp$x <= 0 & mp$wx >= 0) & (!is.na(map$lmp) & map$lmp <= 0.05)
map[cond4, "quadrant"] <- 4

cond5 <- (!is.na(map$lmp) & map$lmp > 0.05)
map[cond5, "quadrant"] <- 5

Creamos la cartografía final.

tm_shape(map) + tm_fill(col = "quadrant", title = "",
                        breaks = c(1, 2, 3, 4, 5, 6),
                        palette = c("red", "blue" , "lightpink" , "skyblue2" , "white"),
                        labels = c("High - High" , "Low - Low" , "High - Low", 
                                   "Low - High" , "Non - significant")) +
  tm_legend(text.size = 1) + tm_borders(alpha = 0.5) +
  tm_layout(frame = FALSE , title = "Clusters") +
  tm_layout(legend.outside = TRUE)