Modelos de Regresión Espacial y Hurdle: Homicidios en el Sur de EE.UU. (1990)

Autor/a

Jairo Andrés Angel, Sara Nathalia Reina

Fecha de publicación

13 de diciembre de 2025

1. Introducción

Este documento implementa un análisis completo de regresión espacial sobre datos de homicidios en condados del sur de Estados Unidos para el año 1990. Se combinan múltiples metodologías:

  1. Metodología de Crime Mapping in R (Maczokni et al.)
  2. Tutorial SpatFD de la profesora Bohorquez
  3. Modelos Hurdle Espaciales con MESF (Montilla, Bohorquez & Rentería, 2023)

Modelos Implementados

Regresión Espacial Clásica:

  • OLS: Mínimos Cuadrados Ordinarios (modelo base)
  • SLX: Spatial Lag of X
  • SAR: Spatial Autoregressive (Lag Model)
  • SEM: Spatial Error Model
  • SDEM: Spatial Durbin Error Model
  • SDM: Spatial Durbin Model
  • Manski: Modelo Manski
  • SARAR/SAC: Modelo combinado

Modelos para Datos de Conteo con Exceso de Ceros:

  • Hurdle Poisson: Para datos con exceso de ceros y sobredispersión
  • Hurdle + MESF: Incorpora Moran Eigenvector Spatial Filtering para capturar dependencia espacial

Marco Metodológico (Montilla et al., 2023)

La metodología propuesta combina:

  1. Modelos Hurdle: Separan el proceso en dos partes - probabilidad de cero y distribución de conteos positivos
  2. Moran Eigenvector Spatial Filtering (MESF): Los eigenvectores de la matriz de pesos espaciales capturan patrones de autocorrelación a diferentes escalas
  3. Validación de supuestos: Tests de sobredispersión, exceso de ceros, y autocorrelación espacial en residuales

2. Configuración del Entorno

# --- SOLUCIÓN PARA POSIT CLOUD ---
# Si tienes problemas de instalación, ejecuta esto primero en la CONSOLA:
# unlink("/cloud/lib/x86_64-pc-linux-gnu-library/4.5/00LOCK-s2", recursive = TRUE)
# unlink("/cloud/lib/x86_64-pc-linux-gnu-library/4.5/00LOCK-sf", recursive = TRUE)

# Eliminar archivos de bloqueo si existen (problema común en Posit Cloud)
lock_dirs <- list.files(path = .libPaths()[1], pattern = "^00LOCK", full.names = TRUE)
if(length(lock_dirs) > 0) {
  sapply(lock_dirs, unlink, recursive = TRUE)
  cat("Archivos de bloqueo eliminados:", lock_dirs, "\n")
}

# Lista de paquetes necesarios
paquetes <- c("sf", "tmap", "ggplot2", "spdep", "spatialreg", 
              "dplyr", "readr", "knitr", "kableExtra", "viridis",
              "gridExtra")

# Función para instalar paquetes de forma segura
instalar_si_falta <- function(pkg) {
  if (!requireNamespace(pkg, quietly = TRUE)) {
    cat("Instalando", pkg, "...\n")
    tryCatch({
      install.packages(pkg, repos = "https://cloud.r-project.org", 
                       dependencies = TRUE, quiet = TRUE)
    }, error = function(e) {
      cat("Error instalando", pkg, ":", conditionMessage(e), "\n")
    })
  }
}

# Instalar paquetes faltantes
invisible(sapply(paquetes, instalar_si_falta))

# Cargar librerías
library(sf)
library(tmap)
library(ggplot2)
library(spdep)
library(spatialreg)
library(dplyr)
library(readr)
library(knitr)
library(kableExtra)
library(viridis)
library(gridExtra)

# Configurar modo de visualización de tmap
tmap_mode("plot")

# Configuración para evitar notación científica
options(scipen = 999)
Nota para usuarios de Posit Cloud

Si encuentras errores de instalación con el mensaje failed to lock directory, ejecuta el siguiente código en la consola de R (no en el documento) antes de renderizar:

# Eliminar archivos de bloqueo
lock_path <- .libPaths()[1]
lock_files <- list.files(lock_path, pattern = "^00LOCK", full.names = TRUE)
unlink(lock_files, recursive = TRUE)

# Reinstalar paquetes problemáticos
install.packages(c("s2", "sf"), repos = "https://cloud.r-project.org")

3. Descarga y Preparación de Datos

3.1 Descarga de Datos

Los datos provienen del GeoDa Center y contienen información sobre homicidios y características socioeconómicas de condados del sur de EE.UU.

# Crear directorio temporal
dir.create("data", showWarnings = FALSE)

# URL de descarga
url_datos <- "https://geodacenter.github.io/data-and-lab/data/south.zip"

# Descargar y extraer
temp_file <- tempfile(fileext = ".zip")
download.file(url_datos, temp_file, mode = "wb")
unzip(temp_file, exdir = "data")

# Leer el shapefile
south_sf <- st_read("data/SOUTH/south.shp", quiet = TRUE)

cat("Número de condados:", nrow(south_sf), "\n")
Número de condados: 1412 
cat("Número de variables:", ncol(south_sf), "\n")
Número de variables: 70 

3.2 Selección de Variables para 1990

# Verificar variables disponibles
cat("Variables disponibles en el dataset:\n")
Variables disponibles en el dataset:
print(names(south_sf))
 [1] "NAME"       "STATE_NAME" "STATE_FIPS" "CNTY_FIPS"  "FIPS"      
 [6] "STFIPS"     "COFIPS"     "FIPSNO"     "SOUTH"      "HR60"      
[11] "HR70"       "HR80"       "HR90"       "HC60"       "HC70"      
[16] "HC80"       "HC90"       "PO60"       "PO70"       "PO80"      
[21] "PO90"       "RD60"       "RD70"       "RD80"       "RD90"      
[26] "PS60"       "PS70"       "PS80"       "PS90"       "UE60"      
[31] "UE70"       "UE80"       "UE90"       "DV60"       "DV70"      
[36] "DV80"       "DV90"       "MA60"       "MA70"       "MA80"      
[41] "MA90"       "POL60"      "POL70"      "POL80"      "POL90"     
[46] "DNL60"      "DNL70"      "DNL80"      "DNL90"      "MFIL59"    
[51] "MFIL69"     "MFIL79"     "MFIL89"     "FP59"       "FP69"      
[56] "FP79"       "FP89"       "BLK60"      "BLK70"      "BLK80"     
[61] "BLK90"      "GI59"       "GI69"       "GI79"       "GI89"      
[66] "FH60"       "FH70"       "FH80"       "FH90"       "geometry"  
# Variables de interés para 1990 (solo las que existen en el dataset)
vars_90 <- c("NAME", "STATE_NAME", "FIPSNO", 
             "HR90",   # Tasa de homicidios 1990
             "RD90",   # Resource Deprivation
             "PS90",   # Population Structure
             "UE90",   # Unemployment Rate
             "DV90",   # Divorce Rate
             "MA90",   # Median Age
             "POL90",  # Log de población
             "DNL90",  # Log de densidad poblacional
             "BLK90",  # % población negra
             "FH90")   # % hogares con jefa de familia

# Filtrar solo variables que existen
vars_90 <- vars_90[vars_90 %in% names(south_sf)]
cat("\nVariables seleccionadas:\n")

Variables seleccionadas:
print(vars_90)
 [1] "NAME"       "STATE_NAME" "FIPSNO"     "HR90"       "RD90"      
 [6] "PS90"       "UE90"       "DV90"       "MA90"       "POL90"     
[11] "DNL90"      "BLK90"      "FH90"      
south_90 <- south_sf %>%
  select(all_of(vars_90))

# Resumen estadístico de variables principales
vars_resumen <- c("HR90", "RD90", "PS90", "UE90", "DV90", "MA90")
vars_resumen <- vars_resumen[vars_resumen %in% names(south_90)]

south_90 %>%
  st_drop_geometry() %>%
  select(all_of(vars_resumen)) %>%
  summary() %>%
  kable(caption = "Resumen estadístico de variables principales (1990)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Resumen estadístico de variables principales (1990)
HR90 RD90 PS90 UE90 DV90 MA90
Min. : 0.000 Min. :-2.2006 Min. :-3.83513 Min. : 0.000 Min. : 2.318 Min. :22.00
1st Qu.: 4.805 1st Qu.:-0.1021 1st Qu.:-0.41711 1st Qu.: 5.131 1st Qu.: 6.345 1st Qu.:31.90
Median : 8.221 Median : 0.3720 Median : 0.01665 Median : 6.682 Median : 7.177 Median :33.90
Mean : 9.549 Mean : 0.5536 Mean : 0.09122 Mean : 7.232 Mean : 7.247 Mean :34.04
3rd Qu.:13.038 3rd Qu.: 1.1203 3rd Qu.: 0.56207 3rd Qu.: 8.668 3rd Qu.: 8.040 3rd Qu.:35.90
Max. :64.261 Max. : 5.5831 Max. : 3.03403 Max. :25.491 Max. :16.047 Max. :55.40

3.3 Descripción de Variables

Variable Descripción
HR90 Tasa de homicidios por 100,000 habitantes
RD90 Índice de privación de recursos (componente principal)
PS90 Estructura poblacional (componente principal)
UE90 Tasa de desempleo
DV90 Tasa de divorcios (% hombres >14 años divorciados)
MA90 Edad mediana
POL90 Log de población
DNL90 Log de densidad poblacional
BLK90 Porcentaje de población negra
FH90 Porcentaje de hogares con jefa de familia

4. Mapa de Valores Observados

ggplot(south_90) +
  geom_sf(aes(fill = HR90)) +
  scale_fill_viridis_c(option = "viridis", name = "Tasa de\nHomicidios") +
  labs(title = "Tasa de Homicidios en el Sur de EE.UU. (1990)",
       subtitle = "Por 100,000 habitantes") +
  theme_minimal() +
  theme(legend.position = "right")

5. Matriz de Vecindades

5.1 Construcción de Matriz de Vecindad

# Centroides de las áreas
centroids <- st_centroid(south_90)

# Matriz de distancias entre centroides
W_dist <- st_distance(centroids)

# Matriz W de vecindades (criterio Queen)
# Usar el índice de fila como row.names si FIPSNO no existe
if("FIPSNO" %in% names(south_90)) {
  vecinos <- poly2nb(south_90, row.names = south_90$FIPSNO, queen = TRUE)
} else {
  vecinos <- poly2nb(south_90, queen = TRUE)
}

# Resumen de la estructura de vecindad
summary(vecinos)
Neighbour list object:
Number of regions: 1412 
Number of nonzero links: 8096 
Percentage nonzero weights: 0.4060702 
Average number of links: 5.733711 
Link number distribution:

  1   2   3   4   5   6   7   8   9  10  11 
 16  29  54 141 306 437 319  86  18   5   1 
16 least connected regions:
54029 51840 51660 51790 51820 51540 51560 51580 51530 51131 51115 51770 51720 51690 51590 48141 with 1 link
1 most connected region:
51041 with 11 links

5.2 Tipos de Matrices de Vecindad

Las matrices de vecindad pueden definirse con diferentes estilos de ponderación:

  • B (Binary): 1 si son vecinos, 0 si no
  • W (Row-standardized): Pesos normalizados por fila (suma = 1)
  • C (Globally standardized): Pesos normalizados globalmente
  • U (Unweighted): Similar a B pero sin normalización
# Diferentes estilos de ponderación
W_listw_B <- nb2listw(vecinos, style = "B")  # Binaria
W_listw_W <- nb2listw(vecinos, style = "W")  # Row-standardized
W_listw_C <- nb2listw(vecinos, style = "C")  # Globally standardized

# Usaremos la estandarizada por filas (W) para los análisis
W_listw <- W_listw_W

5.3 Visualización de la Red de Vecindad

# Graficar la red de vecindad
coords <- st_coordinates(centroids)

plot(st_geometry(south_90), border = "gray80", 
     main = "Red de Vecindad (Criterio Queen)")
plot(vecinos, coords, add = TRUE, col = "red", lwd = 0.5)

6. Pruebas de Autocorrelación Espacial

6.1 Índice de Moran Global

El índice de Moran evalúa si los valores de una variable tienden a estar agrupados espacialmente:

  • Valor cercano a 1: Autocorrelación positiva (valores similares agrupados)
  • Valor cercano a -1: Autocorrelación negativa (valores opuestos cercanos)
  • Valor cercano a 0: Sin autocorrelación (distribución aleatoria)
# Test de Moran's I
moran_result <- moran.test(south_90$HR90, W_listw)
moran_result

    Moran I test under randomisation

data:  south_90$HR90  
weights: W_listw    

Moran I statistic standard deviate = 15.926, p-value <
0.00000000000000022
alternative hypothesis: greater
sample estimates:
Moran I statistic       Expectation          Variance 
     0.2569933829     -0.0007087172      0.0002618233 
# Diagrama de Moran
moran.plot(south_90$HR90, W_listw,
           xlab = "Tasa de Homicidios (HR90)",
           ylab = "HR90 Espacialmente Rezagado",
           main = paste("Diagrama de Moran\nI =", 
                       round(moran_result$estimate[1], 4),
                       ", p-valor =", 
                       format(moran_result$p.value, scientific = TRUE, digits = 3)))

6.2 Índice G Local de Getis-Ord

El índice Local G permite identificar patrones locales de agrupamiento (clusters de valores altos o bajos).

# Crear vecindades basadas en distancia para Local G
# Usamos dnearneigh con distancia máxima basada en los datos
bbox <- st_bbox(south_90)
max_dist <- sqrt((bbox["xmax"] - bbox["xmin"])^2 + (bbox["ymax"] - bbox["ymin"])^2) / 10

nearng <- dnearneigh(coords, 0, max_dist)
W_lw_g <- nb2listw(nearng, style = "B", zero.policy = TRUE)

# Calcular Local G
local_g <- localG(south_90$HR90, W_lw_g)
local_g_values <- as.numeric(local_g)
# Simulación Monte Carlo para significancia
set.seed(123)
n_sim <- 999
sim_G <- matrix(0, n_sim, length(local_g_values))

for(i in 1:n_sim) {
  sim_G[i,] <- as.numeric(localG(sample(south_90$HR90), W_lw_g))
}

# Calcular p-valores
mc_pvalor_G <- (colSums(sweep(sim_G, 2, local_g_values, ">=")) + 1) / (n_sim + 1)
# Agregar resultados al dataset
south_90$local_G <- local_g_values
south_90$pvalor_G <- mc_pvalor_G

# Mapas
p1 <- ggplot(south_90) +
  geom_sf(aes(fill = local_G)) +
  scale_fill_viridis_c(option = "plasma", name = "Local G") +
  labs(title = "G de Getis-Ord Local") +
  theme_minimal()

p2 <- ggplot(south_90) +
  geom_sf(aes(fill = pvalor_G)) +
  scale_fill_viridis_c(option = "viridis", name = "P-valor", 
                       direction = -1) +
  labs(title = "P-valor del Local G\n(Simulación Monte Carlo)") +
  theme_minimal()

grid.arrange(p1, p2, ncol = 2)

Interpretación del Local G
  • Valores altos de G (colores claros): Clusters de valores altos (“hot spots”)
  • Valores bajos de G (colores oscuros): Clusters de valores bajos (“cold spots”)
  • P-valores < 0.05: Patrones estadísticamente significativos

7. Modelos de Regresión Espacial

7.1 Especificación del Modelo

Modelamos la tasa de homicidios (HR90) en función de:

  • RD90: Privación de recursos
  • PS90: Estructura poblacional
  • UE90: Desempleo
  • DV90: Tasa de divorcios
  • MA90: Edad mediana
# Fórmula del modelo
formula_reg <- HR90 ~ RD90 + PS90 + UE90 + DV90 + MA90

7.2 Modelo OLS (Mínimos Cuadrados Ordinarios)

\[y = X\beta + \varepsilon\]

modelo_ols <- lm(formula_reg, data = south_90)
summary(modelo_ols)

Call:
lm(formula = formula_reg, data = south_90)

Residuals:
    Min      1Q  Median      3Q     Max 
-19.583  -3.514  -0.747   2.591  41.833 

Coefficients:
            Estimate Std. Error t value             Pr(>|t|)    
(Intercept)  8.96250    1.78133   5.031    0.000000549912583 ***
RD90         4.58778    0.21457  21.381 < 0.0000000000000002 ***
PS90         1.95589    0.20540   9.522 < 0.0000000000000002 ***
UE90        -0.52440    0.07003  -7.489    0.000000000000122 ***
DV90         0.46159    0.11517   4.008    0.000064485378531 ***
MA90        -0.04948    0.04890  -1.012                0.312    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 5.861 on 1406 degrees of freedom
Multiple R-squared:  0.3092,    Adjusted R-squared:  0.3067 
F-statistic: 125.8 on 5 and 1406 DF,  p-value: < 0.00000000000000022

7.3 Modelo SLX (Spatial Lag of X)

\[y = X\beta + WX\theta + \varepsilon\]

El modelo SLX incluye el “lag” espacial de las variables explicativas.

modelo_slx <- lmSLX(formula_reg, data = south_90, listw = W_listw)
summary(modelo_slx)

Call:
lm(formula = formula(paste("y ~ ", paste(colnames(x)[-1], collapse = "+"))), 
    data = as.data.frame(x), weights = weights)

Coefficients:
             Estimate                                         
(Intercept)  15.0327887101723209894998944946564733982086181641
RD90          4.1471703139389610726084356429055333137512207031
PS90          1.3984688547848649342597582290181890130043029785
UE90         -0.1352848684120694366939119390735868364572525024
DV90          0.5387180388166109556991045792528893798589706421
MA90          0.0484281721586381080912886432088271249085664749
lag.RD90      0.9300822156391969075528436405875254422426223755
lag.PS90      1.1760051309886028203521846080548129975795745850
lag.UE90     -0.7224994753409048620085286529501900076866149902
lag.DV90     -0.0621252623186750313477588747446134220808744431
lag.MA90     -0.2191006770657276669922453038452658802270889282
             Std. Error                                       
(Intercept)   2.6291253902794369246009864582447335124015808105
RD90          0.2912514297141030605864386870962334796786308289
PS90          0.2629707866528966930808053348300745710730552673
UE90          0.0954080929743684885702137421503721270710229874
DV90          0.1431311333687948217363583580663544125854969025
MA90          0.0611992230972255024656902833157801069319248199
lag.RD90      0.4030810904788399429143908037076471373438835144
lag.PS90      0.3723153603593776783675650676741497591137886047
lag.UE90      0.1290691408377768467641288907543639652431011200
lag.DV90      0.2085307578941125172811155152885476127266883850
lag.MA90      0.0946643706838764253941320703233941458165645599
             t value                                          
(Intercept)   5.7177907017110962684114383591804653406143188477
RD90         14.2391414799573272631505460594780743122100830078
PS90          5.3179627767199377785800606943666934967041015625
UE90         -1.4179600932639320198802579398034140467643737793
DV90          3.7638075388429870216100425750482827425003051758
MA90          0.7913200479963874434119475154147949069738388062
lag.RD90      2.3074320220139981074680690653622150421142578125
lag.PS90      3.1586264124409559883588372031226754188537597656
lag.UE90     -5.5977708587135701634451834252104163169860839844
lag.DV90     -0.2979189398535678767743206662999000400304794312
lag.MA90     -2.3144999061726787559223339485470205545425415039
             Pr(>|t|)                                         
(Intercept)   0.0000000131706221614514921449366757766341606839
RD90          0.0000000000000000000000000000000000000000004536
PS90          0.0000001220007703709876140823510637378745968817
UE90          0.1564247927304177054885769848624477162957191467
DV90          0.0001742604115799845326643946474831636805902235
MA90          0.4288911969594312312281658705614972859621047974
lag.RD90      0.0211759230799394744526598088896207627840340137
lag.PS90      0.0016189968452972199573769440661408225423656404
lag.UE90      0.0000000260873413175081067050600780454772120720
lag.DV90      0.7658092049611119334073805475782137364149093628
lag.MA90      0.0207843202854591443951015605762222548946738243

7.4 Modelo SAR / Lag (Spatial Autoregressive)

\[y = \rho Wy + X\beta + \varepsilon\]

Incluye un “lag” espacial de la variable dependiente.

modelo_sar <- lagsarlm(formula_reg, data = south_90, listw = W_listw)
summary(modelo_sar)

Call:lagsarlm(formula = formula_reg, data = south_90, listw = W_listw)

Residuals:
      Min        1Q    Median        3Q       Max 
-18.24865  -3.48359  -0.75025   2.49541  43.19796 

Type: lag 
Coefficients: (asymptotic standard errors) 
              Estimate Std. Error z value              Pr(>|z|)
(Intercept)  4.9940707  1.7942624  2.7834               0.00538
RD90         4.0171649  0.2258833 17.7842 < 0.00000000000000022
PS90         1.7885917  0.2017486  8.8654 < 0.00000000000000022
UE90        -0.4365541  0.0687029 -6.3542       0.0000000002095
DV90         0.4720836  0.1125464  4.1946       0.0000273390008
MA90        -0.0090491  0.0479334 -0.1888               0.85026

Rho: 0.23147, LR test value: 45.421, p-value: 0.000000000015888
Asymptotic standard error: 0.033998
    z-value: 6.8084, p-value: 0.0000000000098661
Wald statistic: 46.355, p-value: 0.0000000000098661

Log likelihood: -4474.661 for lag model
ML residual variance (sigma squared): 32.789, (sigma: 5.7262)
Number of observations: 1412 
Number of parameters estimated: 8 
AIC: 8965.3, (AIC for lm: 9008.7)
LM test for residual autocorrelation
test value: 2.0534, p-value: 0.15187

7.5 Modelo SEM (Spatial Error Model)

\[y = X\beta + u, \quad u = \lambda Wu + \varepsilon\]

Modela la dependencia espacial en los errores.

modelo_sem <- errorsarlm(formula_reg, data = south_90, listw = W_listw)
summary(modelo_sem)

Call:errorsarlm(formula = formula_reg, data = south_90, listw = W_listw)

Residuals:
      Min        1Q    Median        3Q       Max 
-18.12072  -3.47386  -0.65447   2.47020  41.88765 

Type: error 
Coefficients: (asymptotic standard errors) 
             Estimate Std. Error z value              Pr(>|z|)
(Intercept)  6.518273   1.963847  3.3191              0.000903
RD90         4.395116   0.238312 18.4427 < 0.00000000000000022
PS90         1.763051   0.225642  7.8135  0.000000000000005551
UE90        -0.380113   0.078674 -4.8315  0.000001354990324831
DV90         0.492009   0.125188  3.9302  0.000084892029053707
MA90        -0.011567   0.053045 -0.2181              0.827379

Lambda: 0.29815, LR test value: 51.977, p-value: 0.00000000000056166
Asymptotic standard error: 0.037839
    z-value: 7.8794, p-value: 0.0000000000000033307
Wald statistic: 62.086, p-value: 0.0000000000000033307

Log likelihood: -4471.384 for error model
ML residual variance (sigma squared): 32.409, (sigma: 5.6929)
Number of observations: 1412 
Number of parameters estimated: 8 
AIC: 8958.8, (AIC for lm: 9008.7)

7.6 Modelo SDEM (Spatial Durbin Error Model)

\[y = X\beta + WX\theta + u, \quad u = \lambda Wu + \varepsilon\]

Combina SLX con errores espaciales.

modelo_sdem <- errorsarlm(formula_reg, data = south_90, listw = W_listw, 
                          etype = "emixed")
summary(modelo_sdem)

Call:errorsarlm(formula = formula_reg, data = south_90, listw = W_listw, 
    etype = "emixed")

Residuals:
      Min        1Q    Median        3Q       Max 
-18.29736  -3.31703  -0.70071   2.42281  39.82279 

Type: error 
Coefficients: (asymptotic standard errors) 
             Estimate Std. Error z value              Pr(>|z|)
(Intercept) 14.338990   3.229221  4.4404        0.000008979762
RD90         4.249230   0.273336 15.5458 < 0.00000000000000022
PS90         1.431901   0.247596  5.7832        0.000000007329
UE90        -0.168723   0.089161 -1.8923              0.058445
DV90         0.526574   0.134766  3.9073        0.000093323688
MA90         0.038096   0.057416  0.6635              0.507008
lag.RD90     0.676837   0.423292  1.5990              0.109825
lag.PS90     1.060172   0.396778  2.6720              0.007541
lag.UE90    -0.633270   0.134966 -4.6921        0.000002704312
lag.DV90    -0.035639   0.222687 -0.1600              0.872848
lag.MA90    -0.201125   0.099402 -2.0234              0.043037

Lambda: 0.25192, LR test value: 38.191, p-value: 0.00000000064149
Asymptotic standard error: 0.038961
    z-value: 6.466, p-value: 0.00000000010066
Wald statistic: 41.809, p-value: 0.00000000010066

Log likelihood: -4453.534 for error model
ML residual variance (sigma squared): 31.761, (sigma: 5.6357)
Number of observations: 1412 
Number of parameters estimated: 13 
AIC: 8933.1, (AIC for lm: 8969.3)

7.7 Modelo SDM (Spatial Durbin Model)

\[y = \rho Wy + X\beta + WX\theta + \varepsilon\]

Incluye lag espacial de Y y de las X.

modelo_sdm <- lagsarlm(formula_reg, data = south_90, listw = W_listw, 
                       type = "mixed")
summary(modelo_sdm)

Call:lagsarlm(formula = formula_reg, data = south_90, listw = W_listw, 
    type = "mixed")

Residuals:
     Min       1Q   Median       3Q      Max 
-18.2203  -3.3323  -0.6957   2.4432  39.7975 

Type: mixed 
Coefficients: (asymptotic standard errors) 
             Estimate Std. Error z value              Pr(>|z|)
(Intercept) 11.447968   2.610466  4.3854         0.00001157663
RD90         4.157334   0.284145 14.6310 < 0.00000000000000022
PS90         1.380872   0.256539  5.3827         0.00000007337
UE90        -0.117725   0.093195 -1.2632             0.2065128
DV90         0.541709   0.139601  3.8804             0.0001043
MA90         0.053048   0.059707  0.8885             0.3742892
lag.RD90    -0.437239   0.434859 -1.0055             0.3146690
lag.PS90     0.539073   0.373505  1.4433             0.1489405
lag.UE90    -0.515520   0.128267 -4.0191         0.00005841487
lag.DV90    -0.179017   0.204286 -0.8763             0.3808624
lag.MA90    -0.190709   0.092380 -2.0644             0.0389815

Rho: 0.25853, LR test value: 41.637, p-value: 0.00000000010991
Asymptotic standard error: 0.0386
    z-value: 6.6978, p-value: 0.000000000021161
Wald statistic: 44.86, p-value: 0.000000000021161

Log likelihood: -4451.811 for mixed model
ML residual variance (sigma squared): 31.663, (sigma: 5.627)
Number of observations: 1412 
Number of parameters estimated: 13 
AIC: 8929.6, (AIC for lm: 8969.3)
LM test for residual autocorrelation
test value: 9.6502, p-value: 0.0018933

7.8 Modelo Manski

\[y = \rho Wy + X\beta + WX\theta + u, \quad u = \lambda Wu + \varepsilon\]

El modelo más general, combina SAR, SLX y SEM.

modelo_manski <- sacsarlm(formula_reg, data = south_90, listw = W_listw, 
                          type = "sacmixed")
summary(modelo_manski)

Call:sacsarlm(formula = formula_reg, data = south_90, listw = W_listw, 
    type = "sacmixed")

Residuals:
      Min        1Q    Median        3Q       Max 
-17.14167  -3.13856  -0.68459   2.35011  38.53581 

Type: sacmixed 
Coefficients: (asymptotic standard errors) 
             Estimate Std. Error z value              Pr(>|z|)
(Intercept)  7.672981   2.098181  3.6570             0.0002552
RD90         3.994958   0.294907 13.5465 < 0.00000000000000022
PS90         1.337302   0.265078  5.0449          0.0000004537
UE90        -0.058496   0.097325 -0.6010             0.5478142
DV90         0.567282   0.144768  3.9186          0.0000890816
MA90         0.068930   0.062342  1.1057             0.2688645
lag.RD90    -1.709186   0.510445 -3.3484             0.0008127
lag.PS90    -0.124408   0.378545 -0.3286             0.7424209
lag.UE90    -0.350645   0.130469 -2.6876             0.0071975
lag.DV90    -0.342162   0.190427 -1.7968             0.0723643
lag.MA90    -0.170012   0.086455 -1.9665             0.0492440

Rho: 0.54887
Asymptotic standard error: 0.071442
    z-value: 7.6827, p-value: 0.000000000000015543
Lambda: -0.40636
Asymptotic standard error: 0.11045
    z-value: -3.6791, p-value: 0.00023407

LR test value: 98.362, p-value: < 0.000000000000000222

Log likelihood: -4448.191 for sacmixed model
ML residual variance (sigma squared): 29.089, (sigma: 5.3935)
Number of observations: 1412 
Number of parameters estimated: 14 
AIC: 8924.4, (AIC for lm: 9008.7)

7.9 Modelo SARAR/SAC (Kelejian-Prucha, Cliff-Ord)

\[y = \rho Wy + X\beta + u, \quad u = \lambda Wu + \varepsilon\]

Combina SAR con errores espaciales (sin lag de X).

modelo_sarar <- sacsarlm(formula_reg, data = south_90, listw = W_listw, 
                         type = "sac")
summary(modelo_sarar)

Call:sacsarlm(formula = formula_reg, data = south_90, listw = W_listw, 
    type = "sac")

Residuals:
      Min        1Q    Median        3Q       Max 
-17.30883  -3.39551  -0.69542   2.41314  40.68520 

Type: sac 
Coefficients: (asymptotic standard errors) 
              Estimate Std. Error z value              Pr(>|z|)
(Intercept)  7.9648684  2.2982205  3.4657             0.0005289
RD90         4.4069416  0.2539651 17.3526 < 0.00000000000000022
PS90         1.6001308  0.2377221  6.7311      0.00000000001684
UE90        -0.2943434  0.0838465 -3.5105             0.0004473
DV90         0.4798727  0.1306602  3.6727             0.0002400
MA90        -0.0090772  0.0554436 -0.1637             0.8699524

Rho: -0.218
Asymptotic standard error: 0.080071
    z-value: -2.7225, p-value: 0.0064787
Lambda: 0.49388
Asymptotic standard error: 0.062205
    z-value: 7.9395, p-value: 0.0000000000000019984

LR test value: 53.397, p-value: 0.000000000002541

Log likelihood: -4470.673 for sac model
ML residual variance (sigma squared): 31.047, (sigma: 5.572)
Number of observations: 1412 
Number of parameters estimated: 9 
AIC: 8959.3, (AIC for lm: 9008.7)

8. Comparación de Modelos

8.1 Criterios de Información

# Tabla comparativa de AIC
comparacion_aic <- data.frame(
  Modelo = c("OLS", "SLX", "SAR (Lag)", "SEM (Error)", 
             "SDEM", "SDM", "Manski", "SARAR/SAC"),
  AIC = c(AIC(modelo_ols), 
          AIC(modelo_slx),
          AIC(modelo_sar), 
          AIC(modelo_sem),
          AIC(modelo_sdem),
          AIC(modelo_sdm),
          AIC(modelo_manski),
          AIC(modelo_sarar)),
  LogLik = c(logLik(modelo_ols),
             logLik(modelo_slx),
             logLik(modelo_sar),
             logLik(modelo_sem),
             logLik(modelo_sdem),
             logLik(modelo_sdm),
             logLik(modelo_manski),
             logLik(modelo_sarar))
)

# Ordenar por AIC
comparacion_aic <- comparacion_aic %>%
  arrange(AIC)

comparacion_aic %>%
  kable(digits = 2, 
        caption = "Comparación de Modelos por AIC (menor es mejor)") %>%
  kable_styling(bootstrap_options = c("striped", "hover")) %>%
  row_spec(1, bold = TRUE, background = "#d4edda")
Comparación de Modelos por AIC (menor es mejor)
Modelo AIC LogLik
Manski 8924.38 -4448.19
SDM 8929.62 -4451.81
SDEM 8933.07 -4453.53
SEM (Error) 8958.77 -4471.38
SARAR/SAC 8959.35 -4470.67
SAR (Lag) 8965.32 -4474.66
SLX 8969.26 -4472.63
OLS 9008.74 -4497.37

8.2 Tests de Lagrange Multiplier

Para decidir entre modelos espaciales:

lm_tests <- lm.LMtests(modelo_ols, W_listw, 
                       test = c("LMerr", "LMlag", "RLMerr", "RLMlag", "SARMA"))

# Tabla de resultados
lm_df <- data.frame(
  Test = c("LM Error", "LM Lag", "Robust LM Error", "Robust LM Lag", "SARMA"),
  Estadístico = c(lm_tests$LMerr$statistic,
                  lm_tests$LMlag$statistic,
                  lm_tests$RLMerr$statistic,
                  lm_tests$RLMlag$statistic,
                  lm_tests$SARMA$statistic),
  P_valor = c(lm_tests$LMerr$p.value,
              lm_tests$LMlag$p.value,
              lm_tests$RLMerr$p.value,
              lm_tests$RLMlag$p.value,
              lm_tests$SARMA$p.value)
)

lm_df %>%
  kable(digits = 4, col.names = c("Test", "Estadístico", "P-valor"),
        caption = "Tests de Multiplicadores de Lagrange") %>%
  kable_styling(bootstrap_options = c("striped", "hover")) %>%
  row_spec(which(lm_df$P_valor < 0.05), bold = TRUE, color = "red")
Tests de Multiplicadores de Lagrange
Test Estadístico P-valor
LM Error 56.4296 0
LM Lag 56.4296 0
Robust LM Error 56.4296 0
Robust LM Lag 56.4296 0
SARMA 56.4296 0
Criterio de Selección
  1. Si LM Error y LM Lag son ambos significativos → usar versiones Robustas
  2. Elegir el modelo con el test robusto más significativo
  3. Verificar con AIC: modelo con menor AIC es preferido

9. Análisis del Modelo Seleccionado

# Seleccionar el mejor modelo según AIC
mejor_modelo_nombre <- comparacion_aic$Modelo[1]
cat("Mejor modelo según AIC:", mejor_modelo_nombre, "\n")
Mejor modelo según AIC: Manski 
# Obtener el modelo correspondiente
modelos_lista <- list(
  "OLS" = modelo_ols,
  "SLX" = modelo_slx,
  "SAR (Lag)" = modelo_sar,
  "SEM (Error)" = modelo_sem,
  "SDEM" = modelo_sdem,
  "SDM" = modelo_sdm,
  "Manski" = modelo_manski,
  "SARAR/SAC" = modelo_sarar
)

modelo_final <- modelos_lista[[mejor_modelo_nombre]]

9.1 Valores Ajustados

# Extraer valores ajustados
south_90$fitted <- fitted(modelo_final)

ggplot(south_90) +
  geom_sf(aes(fill = fitted)) +
  scale_fill_viridis_c(option = "viridis", name = "HR90\nAjustado") +
  labs(title = paste("Valores Ajustados - Modelo", mejor_modelo_nombre),
       subtitle = "Tasa de Homicidios Estimada (1990)") +
  theme_minimal()

9.2 Residuales del Modelo Final

# Extraer residuales
south_90$residuales <- residuals(modelo_final)
south_90$res_std <- scale(south_90$residuales)[,1]

# Mapa de residuales
breaks_sd <- c(-Inf, -2, -1, 0, 1, 2, Inf)

ggplot(south_90) +
  geom_sf(aes(fill = res_std)) +
  scale_fill_gradient2(low = "blue", mid = "white", high = "red",
                       midpoint = 0, name = "Residuales\n(Desv. Est.)") +
  labs(title = paste("Residuales del Modelo", mejor_modelo_nombre),
       subtitle = "Azul: sobreestimación | Rojo: subestimación") +
  theme_minimal()

9.3 Test de Moran en Residuales del Modelo Final

# Test de Moran's I en residuales
moran_residuales <- moran.test(south_90$residuales, W_listw)
moran_residuales

    Moran I test under randomisation

data:  south_90$residuales  
weights: W_listw    

Moran I statistic standard deviate = -0.15455, p-value = 0.5614
alternative hypothesis: greater
sample estimates:
Moran I statistic       Expectation          Variance 
    -0.0032083620     -0.0007087172      0.0002615806 
Verificación de Autocorrelación Residual

Si el p-valor del test de Moran en los residuales es > 0.05, el modelo ha capturado adecuadamente la dependencia espacial. Si sigue siendo significativo, considerar un modelo espacial más complejo.

10. Impactos (para modelos espaciales)

Para modelos con lag espacial, los coeficientes no se interpretan directamente. Calculamos impactos directos, indirectos y totales:

# Solo calcular impactos si el modelo tiene componente lag
if(mejor_modelo_nombre %in% c("SAR (Lag)", "SDM", "Manski", "SARAR/SAC")) {
  
  # Convertir a matriz sparse para eficiencia
  W_sparse <- as(W_listw, "CsparseMatrix")
  trMC <- trW(W_sparse, type = "MC")
  
  # Calcular impactos
  impactos <- impacts(modelo_final, tr = trMC, R = 500)
  cat("\n=== Impactos del Modelo", mejor_modelo_nombre, "===\n")
  summary(impactos, zstats = TRUE)
} else {
  cat("El modelo", mejor_modelo_nombre, "no requiere cálculo de impactos.\n")
  cat("Los coeficientes se interpretan directamente.\n")
}

=== Impactos del Modelo Manski ===
Impact measures (sacmixed, trace):
               Direct    Indirect      Total
RD90 dy/dx  4.0609667  1.00579076  5.0667574
PS90 dy/dx  1.4205221  1.26803887  2.6885609
UE90 dy/dx -0.1107483 -0.79617423 -0.9069226
DV90 dy/dx  0.5630771 -0.06406599  0.4990111
MA90 dy/dx  0.0508859 -0.27494845 -0.2240626
========================================================
Simulation results ( variance matrix):
Direct:

Iterations = 1:500
Thinning interval = 1 
Number of chains = 1 
Sample size per chain = 500 

1. Empirical mean and standard deviation for each variable,
   plus standard error of the mean:

               Mean      SD Naive SE Time-series SE
RD90 dy/dx  4.04666 0.28011 0.012527       0.012527
PS90 dy/dx  1.43222 0.24685 0.011040       0.010308
UE90 dy/dx -0.10439 0.09390 0.004199       0.004199
DV90 dy/dx  0.56863 0.13608 0.006086       0.006086
MA90 dy/dx  0.05347 0.05794 0.002591       0.002591

2. Quantiles for each variable:

               2.5%      25%      50%      75%   97.5%
RD90 dy/dx  3.53083  3.85166  4.06395  4.23878 4.57554
PS90 dy/dx  0.95298  1.27400  1.42034  1.59647 1.91223
UE90 dy/dx -0.28907 -0.16118 -0.10711 -0.03799 0.06303
DV90 dy/dx  0.28150  0.48007  0.57197  0.66203 0.83132
MA90 dy/dx -0.06462  0.01634  0.05188  0.09324 0.16772

========================================================
Indirect:

Iterations = 1:500
Thinning interval = 1 
Number of chains = 1 
Sample size per chain = 500 

1. Empirical mean and standard deviation for each variable,
   plus standard error of the mean:

               Mean     SD Naive SE Time-series SE
RD90 dy/dx  1.01883 0.5316 0.023774       0.023774
PS90 dy/dx  1.27091 0.5264 0.023541       0.023541
UE90 dy/dx -0.80129 0.1745 0.007805       0.007630
DV90 dy/dx -0.06774 0.2952 0.013201       0.015531
MA90 dy/dx -0.27396 0.1286 0.005751       0.005751

2. Quantiles for each variable:

               2.5%     25%      50%     75%    97.5%
RD90 dy/dx -0.03883  0.6844  1.03870  1.3761  2.07276
PS90 dy/dx  0.25169  0.8971  1.28151  1.6075  2.31191
UE90 dy/dx -1.14019 -0.9160 -0.80291 -0.6824 -0.45498
DV90 dy/dx -0.60243 -0.2644 -0.05409  0.1320  0.50515
MA90 dy/dx -0.54488 -0.3517 -0.26830 -0.1871 -0.02899

========================================================
Total:

Iterations = 1:500
Thinning interval = 1 
Number of chains = 1 
Sample size per chain = 500 

1. Empirical mean and standard deviation for each variable,
   plus standard error of the mean:

              Mean     SD Naive SE Time-series SE
RD90 dy/dx  5.0655 0.4766 0.021315       0.023352
PS90 dy/dx  2.7031 0.4821 0.021561       0.021561
UE90 dy/dx -0.9057 0.1524 0.006814       0.007011
DV90 dy/dx  0.5009 0.2740 0.012254       0.012254
MA90 dy/dx -0.2205 0.1232 0.005512       0.005512

2. Quantiles for each variable:

               2.5%     25%     50%     75%     97.5%
RD90 dy/dx  4.17527  4.7478  5.0818  5.3538  6.051625
PS90 dy/dx  1.74332  2.4161  2.7018  3.0068  3.622176
UE90 dy/dx -1.21752 -1.0046 -0.9032 -0.7966 -0.612866
DV90 dy/dx -0.01995  0.3361  0.4996  0.6822  1.021506
MA90 dy/dx -0.49042 -0.2926 -0.2098 -0.1439 -0.002744

========================================================
Simulated standard errors
               Direct  Indirect     Total
RD90 dy/dx 0.28011363 0.5316067 0.4766153
PS90 dy/dx 0.24685313 0.5263991 0.4821297
UE90 dy/dx 0.09389998 0.1745282 0.1523573
DV90 dy/dx 0.13607788 0.2951883 0.2740006
MA90 dy/dx 0.05793667 0.1286035 0.1232489

Simulated z-values:
               Direct   Indirect     Total
RD90 dy/dx 14.4465077  1.9165045 10.628048
PS90 dy/dx  5.8018971  2.4143443  5.606635
UE90 dy/dx -1.1117443 -4.5912043 -5.944495
DV90 dy/dx  4.1786875 -0.2294938  1.828036
MA90 dy/dx  0.9228776 -2.1302357 -1.788960

Simulated p-values:
           Direct                 Indirect    Total                 
RD90 dy/dx < 0.000000000000000222 0.055301    < 0.000000000000000222
PS90 dy/dx 0.0000000065569        0.015764    0.0000000206298       
UE90 dy/dx 0.26625                0.000004407 0.0000000027731       
DV90 dy/dx 0.0000293196211        0.818485    0.067544              
MA90 dy/dx 0.35607                0.033152    0.073621              

11. Modelos Hurdle Espaciales con Filtrado de Eigenvectores de Moran

Esta sección implementa la metodología propuesta por Montilla Velásquez, Bohorquez Castañeda y Rentería Ramos (2023) para datos de conteo con exceso de ceros, sobredispersión y autocorrelación espacial.

11.1 Preparación de Datos de Conteo

Para los modelos Hurdle necesitamos trabajar con conteos (número de homicidios) en lugar de tasas. Los valores deben ser enteros no negativos.

# Verificar si existe la variable de conteo HC90
if("HC90" %in% names(south_sf)) {
  south_90$HC90 <- as.integer(round(south_sf$HC90))
  cat("Variable HC90 (conteo de homicidios) encontrada\n")
} else {
  # Si no existe, aproximar desde la tasa y población
  if(all(c("HR90", "PO90") %in% names(south_sf))) {
    # Calcular conteo y asegurar que sea entero
    hc_calc <- south_sf$HR90 * south_sf$PO90 / 100000
    south_90$HC90 <- as.integer(round(hc_calc))
    south_90$PO90 <- south_sf$PO90
    cat("Variable HC90 calculada desde HR90 y PO90\n")
  } else {
    # Alternativa: usar la tasa directamente discretizada
    # Crear conteo aproximado basado en cuantiles
    cat("Advertencia: No se encontró PO90. Creando conteo aproximado.\n")
    south_90$HC90 <- as.integer(round(south_90$HR90))
  }
}
Variable HC90 (conteo de homicidios) encontrada
# Limpiar valores NA y negativos
south_90$HC90[is.na(south_90$HC90)] <- 0L
south_90$HC90[south_90$HC90 < 0] <- 0L

# Verificar que sea entero
south_90$HC90 <- as.integer(south_90$HC90)

# Resumen de la variable de conteo
cat("\n=== Resumen de HC90 (Conteo de Homicidios) ===\n")

=== Resumen de HC90 (Conteo de Homicidios) ===
cat("Clase:", class(south_90$HC90), "\n")
Clase: integer 
cat("Rango:", range(south_90$HC90, na.rm = TRUE), "\n")
Rango: 0 664 
print(summary(south_90$HC90))
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  0.000   1.000   2.000   7.572   5.000 664.000 
cat("\nNúmero de ceros:", sum(south_90$HC90 == 0, na.rm = TRUE), "\n")

Número de ceros: 220 
cat("Porcentaje de ceros:", round(100 * mean(south_90$HC90 == 0, na.rm = TRUE), 2), "%\n")
Porcentaje de ceros: 15.58 %
cat("Valores únicos (primeros 20):", head(sort(unique(south_90$HC90)), 20), "\n")
Valores únicos (primeros 20): 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 

11.2 Diagnóstico: Exceso de Ceros y Sobredispersión

Siguiendo a Cameron y Trivedi (2013), evaluamos si hay exceso de ceros y sobredispersión.

# Instalar pscl si no está disponible (para modelos Hurdle)
if(!requireNamespace("pscl", quietly = TRUE)) {
  install.packages("pscl", repos = "https://cloud.r-project.org")
}
library(pscl)

if("HC90" %in% names(south_90)) {
  
  # Preparar datos sin geometría
  datos_diag <- st_drop_geometry(south_90)
  datos_diag$HC90 <- as.integer(datos_diag$HC90)
  
  # Eliminar NAs
  vars_diag <- c("HC90", "RD90", "PS90", "UE90", "DV90", "MA90")
  datos_diag <- datos_diag[complete.cases(datos_diag[, vars_diag]), ]
  
  # Modelo Poisson base
  modelo_poisson <- glm(HC90 ~ RD90 + PS90 + UE90 + DV90 + MA90, 
                        data = datos_diag, family = poisson)
  
  # Test de sobredispersión (Cameron & Trivedi)
  # H0: No hay sobredispersión (dispersión = 1)
  dispersion <- sum(residuals(modelo_poisson, type = "pearson")^2) / modelo_poisson$df.residual
  cat("=== Test de Sobredispersión ===\n")
  cat("Parámetro de dispersión estimado:", round(dispersion, 4), "\n")
  if(dispersion > 1) {
    cat("CONCLUSIÓN: Hay sobredispersión (dispersión > 1)\n\n")
  } else {
    cat("No hay evidencia de sobredispersión\n\n")
  }
  
  # Comparar ceros observados vs esperados
  zeros_obs <- sum(datos_diag$HC90 == 0, na.rm = TRUE)
  zeros_esp <- sum(dpois(0, fitted(modelo_poisson)))
  
  cat("=== Análisis de Exceso de Ceros ===\n")
  cat("Ceros observados:", zeros_obs, "\n")
  cat("Ceros esperados (Poisson):", round(zeros_esp), "\n")
  cat("Ratio obs/esp:", round(zeros_obs/zeros_esp, 2), "\n")
  
  if(zeros_obs > zeros_esp * 1.5) {
    cat("CONCLUSIÓN: Hay exceso de ceros - Se recomienda modelo Hurdle o ZIP\n")
  } else {
    cat("No hay evidencia clara de exceso de ceros\n")
  }
}
=== Test de Sobredispersión ===
Parámetro de dispersión estimado: 2.4251 
CONCLUSIÓN: Hay sobredispersión (dispersión > 1)

=== Análisis de Exceso de Ceros ===
Ceros observados: 220 
Ceros esperados (Poisson): 326 
Ratio obs/esp: 0.67 
No hay evidencia clara de exceso de ceros
if("HC90" %in% names(south_90)) {
  
  datos_hist <- st_drop_geometry(south_90)
  datos_hist$HC90 <- as.integer(datos_hist$HC90)
  datos_hist <- datos_hist[!is.na(datos_hist$HC90), ]
  
  p1 <- ggplot(datos_hist, aes(x = HC90)) +
    geom_histogram(fill = "steelblue", color = "white", bins = 50) +
    geom_vline(xintercept = 0, color = "red", linetype = "dashed", linewidth = 1) +
    labs(title = "Distribución del Conteo de Homicidios (HC90)",
         subtitle = paste("Ceros:", sum(datos_hist$HC90 == 0)),
         x = "Número de Homicidios", y = "Frecuencia") +
    theme_minimal()
  
  # Comparar observado vs Poisson esperado (primeros 30 valores)
  max_val <- min(30, max(datos_hist$HC90, na.rm = TRUE))
  lambda_hat <- mean(datos_hist$HC90, na.rm = TRUE)
  x_vals <- 0:max_val
  
  df_comp <- data.frame(
    x = x_vals,
    Observado = sapply(x_vals, function(k) sum(datos_hist$HC90 == k)),
    Poisson = dpois(x_vals, lambda_hat) * nrow(datos_hist)
  )
  
  p2 <- ggplot(df_comp, aes(x = x)) +
    geom_bar(aes(y = Observado), stat = "identity", fill = "steelblue", alpha = 0.7) +
    geom_line(aes(y = Poisson), color = "red", linewidth = 1) +
    geom_point(aes(y = Poisson), color = "red", size = 2) +
    labs(title = "Observado vs Esperado (Poisson)",
         subtitle = "Línea roja = distribución Poisson esperada",
         x = "Conteo", y = "Frecuencia") +
    theme_minimal()
  
  grid.arrange(p1, p2, ncol = 2)
}

11.3 Modelo Poisson-Hurdle

El modelo Hurdle separa el proceso en dos partes:

  1. Modelo de ceros (Binomial): \(P(Y = 0) = p\)
  2. Modelo de conteo (Poisson truncado): \(P(Y = k | Y > 0)\)

\[\ln\left(\frac{p_i}{1-p_i}\right) = X'_i\beta \quad \text{y} \quad \ln(\mu_i) = X'_i\alpha\]

if("HC90" %in% names(south_90)) {
  
  # Preparar datos sin geometría
  datos_modelo <- st_drop_geometry(south_90)
  
  # Verificar que HC90 es entero
  datos_modelo$HC90 <- as.integer(datos_modelo$HC90)
  
  # Eliminar filas con NA en las variables del modelo
  vars_modelo <- c("HC90", "RD90", "PS90", "UE90", "DV90", "MA90")
  datos_modelo <- datos_modelo[complete.cases(datos_modelo[, vars_modelo]), ]
  
  cat("Observaciones para el modelo:", nrow(datos_modelo), "\n")
  cat("Ceros en HC90:", sum(datos_modelo$HC90 == 0), "\n\n")
  
  # Modelo Hurdle Poisson
  modelo_hurdle <- tryCatch({
    hurdle(HC90 ~ RD90 + PS90 + UE90 + DV90 + MA90,
           data = datos_modelo,
           dist = "poisson",
           zero.dist = "binomial")
  }, error = function(e) {
    cat("Error en modelo Hurdle completo:", conditionMessage(e), "\n")
    cat("Intentando modelo simplificado...\n")
    # Modelo más simple si falla
    hurdle(HC90 ~ RD90 + PS90 + UE90,
           data = datos_modelo,
           dist = "poisson",
           zero.dist = "binomial")
  })
  
  cat("=== MODELO HURDLE POISSON ===\n\n")
  print(summary(modelo_hurdle))
}
Observaciones para el modelo: 1412 
Ceros en HC90: 220 

=== MODELO HURDLE POISSON ===


Call:
hurdle(formula = HC90 ~ RD90 + PS90 + UE90 + DV90 + MA90, data = datos_modelo, 
    dist = "poisson", zero.dist = "binomial")

Pearson residuals:
     Min       1Q   Median       3Q      Max 
-12.5860  -0.7645  -0.1379   0.6802  16.5502 

Count model coefficients (truncated poisson with log link):
             Estimate Std. Error z value             Pr(>|z|)    
(Intercept)  0.613830   0.135594   4.527           0.00000598 ***
RD90         0.227004   0.014264  15.914 < 0.0000000000000002 ***
PS90         1.726723   0.011811 146.200 < 0.0000000000000002 ***
UE90         0.026197   0.006794   3.856             0.000115 ***
DV90         0.106650   0.008436  12.642 < 0.0000000000000002 ***
MA90        -0.035365   0.003688  -9.588 < 0.0000000000000002 ***
Zero hurdle model coefficients (binomial with logit link):
            Estimate Std. Error z value             Pr(>|z|)    
(Intercept)  4.84308    1.08446   4.466       0.000007973567 ***
RD90         0.99477    0.15425   6.449       0.000000000113 ***
PS90         2.84239    0.20572  13.817 < 0.0000000000000002 ***
UE90        -0.11734    0.03989  -2.941              0.00327 ** 
DV90         0.08077    0.06881   1.174              0.24048    
MA90        -0.07526    0.02802  -2.686              0.00724 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

Number of iterations in BFGS optimization: 11 
Log-likelihood: -3455 on 12 Df

11.4 Comparación: Hurdle Poisson vs Binomial Negativa

La Poisson truncada en cero no tiene un parámetro de sobredispersión. Si los conteos positivos presentan sobredispersión, la Binomial Negativa truncada es preferible porque añade el parámetro \(\theta\):

\[Var[Y] = \mu + \frac{\mu^2}{\theta}\]

if("HC90" %in% names(south_90) && exists("datos_modelo")) {
  
  # Modelo Hurdle Poisson (ya ajustado)
  modelo_hurdle_poisson <- modelo_hurdle
  
  # Modelo Hurdle Binomial Negativa
  modelo_hurdle_nb <- tryCatch({
    hurdle(HC90 ~ RD90 + PS90 + UE90 + DV90 + MA90,
           data = datos_modelo,
           dist = "negbin",
           zero.dist = "binomial")
  }, error = function(e) {
    cat("Error en modelo Hurdle NB:", conditionMessage(e), "\n")
    NULL
  })
  
  if(!is.null(modelo_hurdle_nb)) {
    
    cat("=== MODELO HURDLE BINOMIAL NEGATIVA ===\n\n")
    print(summary(modelo_hurdle_nb))
    
    # Comparación de modelos
    cat("\n=== COMPARACIÓN DE MODELOS ===\n\n")
    
    comp_table <- data.frame(
      Modelo = c("Hurdle Poisson", "Hurdle Neg. Binomial"),
      LogLik = c(logLik(modelo_hurdle_poisson), logLik(modelo_hurdle_nb)),
      Df = c(attr(logLik(modelo_hurdle_poisson), "df"), 
             attr(logLik(modelo_hurdle_nb), "df")),
      AIC = c(AIC(modelo_hurdle_poisson), AIC(modelo_hurdle_nb)),
      BIC = c(BIC(modelo_hurdle_poisson), BIC(modelo_hurdle_nb))
    )
    
    # Determinar el mejor modelo
    mejor_idx <- which.min(comp_table$AIC)
    
    comp_table %>%
      kable(digits = 2, row.names = FALSE,
            caption = "Comparación Hurdle Poisson vs Binomial Negativa") %>%
      kable_styling(bootstrap_options = c("striped", "hover")) %>%
      row_spec(mejor_idx, bold = TRUE, color = "darkgreen")
    
  }
}
=== MODELO HURDLE BINOMIAL NEGATIVA ===


Call:
hurdle(formula = HC90 ~ RD90 + PS90 + UE90 + DV90 + MA90, data = datos_modelo, 
    dist = "negbin", zero.dist = "binomial")

Pearson residuals:
    Min      1Q  Median      3Q     Max 
-1.9726 -0.6137 -0.1130  0.5038  7.0991 

Count model coefficients (truncated negbin with log link):
             Estimate Std. Error z value             Pr(>|z|)    
(Intercept)  0.177336   0.259543   0.683             0.494440    
RD90         0.479048   0.031766  15.080 < 0.0000000000000002 ***
PS90         1.794318   0.032379  55.417 < 0.0000000000000002 ***
UE90        -0.034374   0.011528  -2.982             0.002866 ** 
DV90         0.055649   0.016852   3.302             0.000959 ***
MA90        -0.003709   0.007124  -0.521             0.602595    
Log(theta)   1.714007   0.099029  17.308 < 0.0000000000000002 ***
Zero hurdle model coefficients (binomial with logit link):
            Estimate Std. Error z value             Pr(>|z|)    
(Intercept)  4.84308    1.08446   4.466       0.000007973567 ***
RD90         0.99477    0.15425   6.449       0.000000000113 ***
PS90         2.84239    0.20572  13.817 < 0.0000000000000002 ***
UE90        -0.11734    0.03989  -2.941              0.00327 ** 
DV90         0.08077    0.06881   1.174              0.24048    
MA90        -0.07526    0.02802  -2.686              0.00724 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

Theta: count = 5.5512
Number of iterations in BFGS optimization: 25 
Log-likelihood: -2614 on 13 Df

=== COMPARACIÓN DE MODELOS ===
Comparación Hurdle Poisson vs Binomial Negativa
Modelo LogLik Df AIC BIC
Hurdle Poisson -3455.14 12 6934.28 6997.32
Hurdle Neg. Binomial -2613.99 13 5253.98 5322.27
=== Test de Razón de Verosimilitud ===
H0: Poisson es adecuado (θ → ∞)
H1: Binomial Negativa es necesaria (θ finito)

Estadístico LR: 1682.3 
P-valor: < 0.00000000000000022 

CONCLUSIÓN: Se rechaza H0. La Binomial Negativa captura
sobredispersión significativa en los conteos positivos.

→ Se usará Hurdle Binomial Negativa para el modelo espacial.
if(exists("modelo_hurdle_poisson") && exists("modelo_hurdle_nb") && !is.null(modelo_hurdle_nb)) {
  
  # Parámetros
  n_obs <- nrow(datos_modelo)
  max_count <- min(30, max(datos_modelo$HC90, na.rm = TRUE))
  
  # Frecuencias observadas
  obs_freq <- sapply(0:max_count, function(k) sum(datos_modelo$HC90 == k))
  
  # --- Frecuencias esperadas: Hurdle Poisson ---
  prob_zero_pois <- predict(modelo_hurdle_poisson, type = "prob")[, 1]
  lambda_pois <- predict(modelo_hurdle_poisson, type = "count")
  
  hurdle_pois_freq <- sapply(0:max_count, function(k) {
    if(k == 0) {
      sum(prob_zero_pois)
    } else {
      prob_positive <- 1 - prob_zero_pois
      prob_k_given_positive <- dpois(k, lambda_pois) / (1 - dpois(0, lambda_pois))
      sum(prob_positive * prob_k_given_positive)
    }
  })
  
  # --- Frecuencias esperadas: Hurdle Binomial Negativa ---
  prob_zero_nb <- predict(modelo_hurdle_nb, type = "prob")[, 1]
  mu_nb <- predict(modelo_hurdle_nb, type = "count")
  theta_nb <- modelo_hurdle_nb$theta["count"]
  
  hurdle_nb_freq <- sapply(0:max_count, function(k) {
    if(k == 0) {
      sum(prob_zero_nb)
    } else {
      prob_positive <- 1 - prob_zero_nb
      # Binomial negativa truncada
      prob_k_nb <- dnbinom(k, size = theta_nb, mu = mu_nb)
      prob_0_nb <- dnbinom(0, size = theta_nb, mu = mu_nb)
      prob_k_given_positive <- prob_k_nb / (1 - prob_0_nb)
      sum(prob_positive * prob_k_given_positive)
    }
  })
  
  # DataFrame para gráficos
  df_comp_dist <- data.frame(
    Conteo = 0:max_count,
    Observado = obs_freq,
    Hurdle_Poisson = hurdle_pois_freq,
    Hurdle_NB = hurdle_nb_freq
  )
  
  # Gráfico 1: Observado vs Hurdle Poisson
  p1 <- ggplot(df_comp_dist, aes(x = Conteo)) +
    geom_bar(aes(y = Observado), stat = "identity", fill = "steelblue", alpha = 0.7) +
    geom_line(aes(y = Hurdle_Poisson), color = "red", linewidth = 1) +
    geom_point(aes(y = Hurdle_Poisson), color = "red", size = 2) +
    labs(title = "Observado vs Hurdle Poisson",
         subtitle = "Línea roja = Poisson truncada",
         x = "Conteo", y = "Frecuencia") +
    theme_minimal()
  
  # Gráfico 2: Observado vs Hurdle Binomial Negativa
  p2 <- ggplot(df_comp_dist, aes(x = Conteo)) +
    geom_bar(aes(y = Observado), stat = "identity", fill = "steelblue", alpha = 0.7) +
    geom_line(aes(y = Hurdle_NB), color = "purple", linewidth = 1) +
    geom_point(aes(y = Hurdle_NB), color = "purple", size = 2) +
    labs(title = "Observado vs Hurdle Neg. Binomial",
         subtitle = paste0("Línea púrpura = NB truncada (θ = ", round(theta_nb, 2), ")"),
         x = "Conteo", y = "Frecuencia") +
    theme_minimal()
  
  grid.arrange(p1, p2, ncol = 2)
}

11.5 Moran Eigenvector Spatial Filtering (MESF)

El método MESF permite incorporar la autocorrelación espacial como variables predictoras derivadas de la descomposición espectral de la matriz de pesos espaciales.

# Calcular la matriz modificada para eigenvectores de Moran
# Θ = (I - 11'/N) W (I - 11'/N)

if("HC90" %in% names(south_90)) {
  
  # Obtener matriz W (manejar islas/regiones sin vecinos)
  W_mat_full <- nb2mat(vecinos, style = "W", zero.policy = TRUE)
  n <- nrow(W_mat_full)
  
  # Matriz de centrado
  I <- diag(n)
  ones <- matrix(1, n, 1)
  M <- I - (ones %*% t(ones)) / n
  
  # Matriz Theta para eigenvectores de Moran
  Theta <- M %*% W_mat_full %*% M
  
  # Descomposición espectral
  eigen_decomp <- eigen(Theta, symmetric = TRUE)
  
  # Los eigenvalores están relacionados con MC por: MC = (N/S0) * lambda
  S0 <- sum(W_mat_full)
  MC_eigen <- (n / S0) * eigen_decomp$values
  
  cat("=== Eigenvectores de Moran ===\n")
  cat("Número de eigenvectores:", length(MC_eigen), "\n")
  cat("Primeros 10 eigenvalores (relacionados con MC):\n")
  print(round(MC_eigen[1:min(10, length(MC_eigen))], 4))
  
  # ═══════════════════════════════════════════════════════════════════════
  # CORRECCIÓN: Selección de eigenvectores con criterio justificado
  # ═══════════════════════════════════════════════════════════════════════
  
  # Umbral basado en significancia estadística del Moran's I
  # Para N grande, el umbral de MC significativo es aproximadamente:
  E_I <- -1 / (n - 1)  # Valor esperado bajo H0
  umbral_mc <- max(0.25, abs(E_I) * 5)  # Umbral conservador
  
  cat("\nUmbral de selección MC >", round(umbral_mc, 3), "\n")
  cat("(Basado en N =", n, "observaciones)\n\n")
  
  # Seleccionar eigenvectores con MC > umbral (autocorrelación positiva significativa)
  idx_pos <- which(MC_eigen > umbral_mc)
  
  if(length(idx_pos) > 0) {
    eigenvec_pos <- eigen_decomp$vectors[, idx_pos, drop = FALSE]
    cat("Número de eigenvectores seleccionados (MC >", round(umbral_mc, 3), "):", 
        ncol(eigenvec_pos), "\n")
  } else {
    # Fallback: usar los primeros 3 eigenvectores positivos
    cat("Nota: No hay eigenvectores con MC >", round(umbral_mc, 3), "\n")
    cat("Usando los primeros 3 eigenvectores con MC positivo.\n")
    idx_pos <- which(MC_eigen > 0)[1:min(3, sum(MC_eigen > 0))]
    eigenvec_pos <- eigen_decomp$vectors[, idx_pos, drop = FALSE]
  }
  
  n_eigen <- ncol(eigenvec_pos)
}
=== Eigenvectores de Moran ===
Número de eigenvectores: 1412 
Primeros 10 eigenvalores (relacionados con MC):
 [1] 1.4981 1.4646 1.1989 1.1971 1.1815 1.1726 1.1698 1.1373 1.1345 1.1275

Umbral de selección MC > 0.25 
(Basado en N = 1412 observaciones)

Número de eigenvectores seleccionados (MC > 0.25 ): 387 
if(exists("eigenvec_pos") && exists("n_eigen") && n_eigen > 0) {
  
  # Añadir los primeros eigenvectores como predictores
  # ═══════════════════════════════════════════════════════════════════════
  # CORRECCIÓN CRÍTICA: Escalar eigenvectores para estabilidad numérica
  # ═══════════════════════════════════════════════════════════════════════
  n_usar <- min(5, n_eigen)  # Limitar a 5 para evitar sobreajuste
  
  for(i in 1:n_usar) {
    # ESCALAR para evitar coeficientes extremos
    south_90[[paste0("MEM", i)]] <- as.numeric(scale(eigenvec_pos[, i]))
  }
  
  cat("Eigenvectores añadidos al dataset (ESCALADOS): MEM1 a MEM", n_usar, "\n")
  cat("Nota: Los eigenvectores fueron estandarizados (media=0, sd=1)\n")
  cat("      para mejorar la estabilidad numérica del modelo.\n\n")
  
  # Visualizar los primeros 4 eigenvectores (Moran Eigenvector Maps)
  if(n_usar >= 4) {
    p_list <- list()
    for(i in 1:4) {
      p_list[[i]] <- ggplot(south_90) +
        geom_sf(aes(fill = .data[[paste0("MEM", i)]])) +
        scale_fill_viridis_c(option = "plasma", name = paste0("MEM", i)) +
        labs(title = paste("MEM", i), 
             subtitle = paste("MC ≈", round(MC_eigen[idx_pos[i]], 3))) +
        theme_minimal() +
        theme(legend.position = "bottom",
              axis.text = element_blank())
    }
    
    grid.arrange(grobs = p_list, ncol = 2,
                 top = "Moran Eigenvector Maps (MEMs)")
  }
}
Eigenvectores añadidos al dataset (ESCALADOS): MEM1 a MEM 5 
Nota: Los eigenvectores fueron estandarizados (media=0, sd=1)
      para mejorar la estabilidad numérica del modelo.

11.6 Modelo Hurdle Espacial con MESF

Incorporamos los eigenvectores de Moran como predictores para capturar la dependencia espacial.

if("HC90" %in% names(south_90) && exists("n_usar")) {
  
  # Preparar datos sin geometría y asegurar enteros
  datos_espacial <- st_drop_geometry(south_90)
  datos_espacial$HC90 <- as.integer(datos_espacial$HC90)
  
  # Variables MEM disponibles (usar solo las escaladas)
  mem_vars <- paste0("MEM", 1:min(5, n_usar))
  vars_modelo <- c("HC90", "RD90", "PS90", "UE90", "DV90", "MA90", mem_vars)
  
  # Filtrar casos completos
  datos_espacial <- datos_espacial[complete.cases(datos_espacial[, vars_modelo]), ]
  
  cat("Observaciones para modelo espacial:", nrow(datos_espacial), "\n\n")
  
  # Fórmula con MEMs
  formula_count <- as.formula(paste("HC90 ~", 
                                    paste(c("RD90", "PS90", "UE90", "DV90", "MA90", mem_vars), 
                                          collapse = " + ")))
  
  cat("Fórmula:", deparse(formula_count), "\n\n")
  
  # Modelo Hurdle con eigenvectores de Moran
  modelo_hurdle_espacial <- tryCatch({
    hurdle(formula_count, 
           data = datos_espacial,
           dist = "poisson",
           zero.dist = "binomial")
  }, error = function(e) {
    cat("Error:", conditionMessage(e), "\n")
    cat("Intentando modelo con menos MEMs...\n")
    hurdle(HC90 ~ RD90 + PS90 + UE90 + DV90 + MA90 + MEM1 + MEM2,
           data = datos_espacial,
           dist = "poisson",
           zero.dist = "binomial")
  })
  
  cat("=== MODELO HURDLE ESPACIAL (con MESF) ===\n\n")
  print(summary(modelo_hurdle_espacial))
}
Observaciones para modelo espacial: 1412 

Fórmula: HC90 ~ RD90 + PS90 + UE90 + DV90 + MA90 + MEM1 + MEM2 + MEM3 +      MEM4 + MEM5 

=== MODELO HURDLE ESPACIAL (con MESF) ===


Call:
hurdle(formula = formula_count, data = datos_espacial, dist = "poisson", 
    zero.dist = "binomial")

Pearson residuals:
     Min       1Q   Median       3Q      Max 
-11.5845  -0.7301  -0.1329   0.6579  15.2309 

Count model coefficients (truncated poisson with log link):
             Estimate Std. Error z value             Pr(>|z|)    
(Intercept)  0.457506   0.141356   3.237             0.001210 ** 
RD90         0.272410   0.015063  18.085 < 0.0000000000000002 ***
PS90         1.725322   0.011801 146.201 < 0.0000000000000002 ***
UE90        -0.001600   0.007449  -0.215             0.829921    
DV90         0.092081   0.008571  10.743 < 0.0000000000000002 ***
MA90        -0.025726   0.003685  -6.982     0.00000000000292 ***
MEM1        -1.110978   0.212209  -5.235     0.00000016471176 ***
MEM2        -0.892437   0.248422  -3.592             0.000328 ***
MEM3         0.009176   0.011121   0.825             0.409298    
MEM4        -0.043034   0.014656  -2.936             0.003323 ** 
MEM5         0.195337   0.015806  12.359 < 0.0000000000000002 ***
Zero hurdle model coefficients (binomial with logit link):
            Estimate Std. Error z value             Pr(>|z|)    
(Intercept)  4.74532    1.09005   4.353       0.000013410843 ***
RD90         0.97074    0.15570   6.235       0.000000000452 ***
PS90         2.86580    0.21326  13.438 < 0.0000000000000002 ***
UE90        -0.11232    0.04031  -2.786              0.00533 ** 
DV90         0.07052    0.07011   1.006              0.31452    
MA90        -0.07027    0.02831  -2.482              0.01305 *  
MEM1        -0.05173    0.18805  -0.275              0.78326    
MEM2        -0.18438    0.16173  -1.140              0.25428    
MEM3        -0.01063    0.25857  -0.041              0.96720    
MEM4        -0.14207    0.35078  -0.405              0.68547    
MEM5         0.02530    0.11355   0.223              0.82371    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

Number of iterations in BFGS optimization: 35 
Log-likelihood: -3272 on 22 Df

11.7 Comparación de Modelos Hurdle

if(exists("modelo_hurdle") && exists("modelo_hurdle_espacial")) {
  
  # Comparar AIC y BIC
  comp_hurdle <- data.frame(
    Modelo = c("Hurdle Poisson", "Hurdle Espacial (MESF)"),
    AIC = c(AIC(modelo_hurdle), AIC(modelo_hurdle_espacial)),
    BIC = c(BIC(modelo_hurdle), BIC(modelo_hurdle_espacial)),
    LogLik = c(logLik(modelo_hurdle), logLik(modelo_hurdle_espacial))
  )
  
  # Calcular mejora porcentual por AIC
  mejora_aic <- (comp_hurdle$AIC[1] - comp_hurdle$AIC[2]) / comp_hurdle$AIC[1] * 100
  
  comp_hurdle %>%
    arrange(AIC) %>%
    kable(digits = 2, 
          caption = "Comparación de Modelos Hurdle") %>%
    kable_styling(bootstrap_options = c("striped", "hover")) %>%
    row_spec(1, bold = TRUE, background = "#d4edda")
  
  cat("\n=== Mejora del Modelo Espacial ===\n")
  cat("ΔAIC:", round(comp_hurdle$AIC[1] - comp_hurdle$AIC[2], 2), "unidades\n")
  cat("Mejora relativa:", round(mejora_aic, 1), "%\n")
}

=== Mejora del Modelo Espacial ===
ΔAIC: -1334.51 unidades
Mejora relativa: -25.4 %

11.8 Verificación: Autocorrelación en Residuales

if(exists("modelo_hurdle_espacial") && exists("datos_espacial")) {
  
  # Extraer residuales del modelo Hurdle espacial
  res_hurdle <- residuals(modelo_hurdle_espacial, type = "pearson")
  
  cat("Número de residuales:", length(res_hurdle), "\n")
  cat("Tamaño de la matriz de pesos:", length(W_listw$neighbours), "\n\n")
  
  # Verificar si las dimensiones coinciden
  if(length(res_hurdle) == length(W_listw$neighbours)) {
    # Test de Moran en residuales
    moran_res_hurdle <- moran.test(res_hurdle, W_listw, zero.policy = TRUE)
    
    cat("=== Test de Moran en Residuales del Modelo Hurdle Espacial ===\n")
    print(moran_res_hurdle)
    
    if(moran_res_hurdle$p.value > 0.05) {
      cat("\n✓ Los residuales NO muestran autocorrelación espacial significativa\n")
      cat("  El modelo ha capturado adecuadamente la dependencia espacial\n")
    } else {
      cat("\n✗ Aún hay autocorrelación espacial residual\n")
      cat("  Considerar incluir más eigenvectores de Moran\n")
    }
  } else {
    cat("Nota: No se puede realizar test de Moran - dimensiones no coinciden\n")
    cat("El modelo se ajustó sobre un subconjunto de datos sin NA.\n")
    cat("\nResumen de residuales de Pearson:\n")
    print(summary(res_hurdle))
  }
}
Número de residuales: 1412 
Tamaño de la matriz de pesos: 1412 

=== Test de Moran en Residuales del Modelo Hurdle Espacial ===

    Moran I test under randomisation

data:  res_hurdle  
weights: W_listw    

Moran I statistic standard deviate = 10.196, p-value <
0.00000000000000022
alternative hypothesis: greater
sample estimates:
Moran I statistic       Expectation          Variance 
     0.1636243381     -0.0007087172      0.0002597534 


✗ Aún hay autocorrelación espacial residual
  Considerar incluir más eigenvectores de Moran

11.9 Interpretación de Resultados (Rate Ratios)

Nota Metodológica Importante

Los coeficientes de los eigenvectores de Moran (MEMs) no deben interpretarse como efectos sustantivos. Los MEMs son controles espaciales que absorben la autocorrelación residual. Sus Rate Ratios pueden ser extremos o inestables debido a:

  1. Multicolinealidad entre eigenvectores adyacentes
  2. La naturaleza abstracta de los patrones espaciales que capturan
  3. Posible sobreajuste si se incluyen demasiados MEMs

Por esta razón, la tabla de Rate Ratios se presenta solo para las variables sustantivas.

if(exists("modelo_hurdle_espacial")) {
  
  # Extraer coeficientes del modelo de conteo
  coefs_count <- coef(modelo_hurdle_espacial, model = "count")
  se_count <- sqrt(diag(vcov(modelo_hurdle_espacial, model = "count")))
  
  # Calcular Rate Ratios e intervalos de confianza
  rr_table <- data.frame(
    Variable = names(coefs_count),
    Coef = coefs_count,
    SE = se_count,
    SE_Coef_Ratio = abs(se_count / coefs_count),  # Diagnóstico de estabilidad
    RR = exp(coefs_count),
    CI_lower = exp(coefs_count - 1.96 * se_count),
    CI_upper = exp(coefs_count + 1.96 * se_count),
    P_valor = 2 * pnorm(-abs(coefs_count / se_count)),
    stringsAsFactors = FALSE
  )
  
  # ═══════════════════════════════════════════════════════════════════════
  # CORRECCIÓN: Separar variables sustantivas de MEMs
  # ═══════════════════════════════════════════════════════════════════════
  
  # Identificar MEMs
  es_mem <- grepl("^MEM", rr_table$Variable)
  
  # Tabla de variables sustantivas (interpretables)
  rr_sustantivas <- rr_table[!es_mem, c("Variable", "Coef", "SE", "RR", 
                                         "CI_lower", "CI_upper", "P_valor")]
  
  cat("=== RATE RATIOS: Variables Sustantivas ===\n\n")
  
  rr_sustantivas %>%
    kable(digits = 4, row.names = FALSE,
          caption = "Rate Ratios del Modelo Hurdle Espacial (Variables Interpretables)",
          col.names = c("Variable", "Coef", "SE", "RR", "IC 95% Inf", "IC 95% Sup", "P-valor")) %>%
    kable_styling(bootstrap_options = c("striped", "hover")) %>%
    row_spec(which(rr_sustantivas$P_valor < 0.05), bold = TRUE, color = "red")
  
  # Diagnóstico de MEMs (solo informativo)
  if(any(es_mem)) {
    cat("\n=== Diagnóstico de Eigenvectores de Moran (MEMs) ===\n")
    cat("Nota: Los MEMs son controles espaciales, no variables sustantivas.\n\n")
    
    rr_mems <- rr_table[es_mem, c("Variable", "Coef", "SE", "SE_Coef_Ratio", "P_valor")]
    
    for(i in 1:nrow(rr_mems)) {
      estable <- ifelse(rr_mems$SE_Coef_Ratio[i] < 0.5, "✓ Estable", "⚠ Inestable")
      sig <- ifelse(rr_mems$P_valor[i] < 0.05, "Significativo", "No significativo")
      cat(sprintf("  %s: Coef=%.2f, SE=%.2f, %s, %s\n", 
                  rr_mems$Variable[i], rr_mems$Coef[i], rr_mems$SE[i], estable, sig))
    }
  }
}
=== RATE RATIOS: Variables Sustantivas ===


=== Diagnóstico de Eigenvectores de Moran (MEMs) ===
Nota: Los MEMs son controles espaciales, no variables sustantivas.

  MEM1: Coef=-1.11, SE=0.21, ✓ Estable, Significativo
  MEM2: Coef=-0.89, SE=0.25, ✓ Estable, Significativo
  MEM3: Coef=0.01, SE=0.01, ⚠ Inestable, No significativo
  MEM4: Coef=-0.04, SE=0.01, ✓ Estable, Significativo
  MEM5: Coef=0.20, SE=0.02, ✓ Estable, Significativo

11.10 Mapa de Valores Ajustados (Modelo Hurdle)

if(exists("modelo_hurdle_espacial") && exists("datos_modelo")) {
  
  # Los valores ajustados corresponden al subconjunto de datos usado
  fitted_vals <- predict(modelo_hurdle_espacial, type = "response")
  resid_vals <- residuals(modelo_hurdle_espacial, type = "pearson")
  
  cat("Valores ajustados calculados para", length(fitted_vals), "observaciones\n")
  
  # Crear dataframe para mapas (usar datos originales con NAs donde no hay modelo)
  datos_mapa <- st_drop_geometry(south_90)
  datos_mapa$fitted_hurdle <- NA
  datos_mapa$residuales_hurdle <- NA
  
  # Identificar filas con datos completos (mismas que usó el modelo)
  vars_modelo_check <- c("HC90", "RD90", "PS90", "UE90", "DV90", "MA90")
  idx_completos <- which(complete.cases(datos_mapa[, vars_modelo_check]))
  
  if(length(idx_completos) == length(fitted_vals)) {
    datos_mapa$fitted_hurdle[idx_completos] <- fitted_vals
    datos_mapa$residuales_hurdle[idx_completos] <- resid_vals
    south_90$fitted_hurdle <- datos_mapa$fitted_hurdle
    south_90$residuales_hurdle <- datos_mapa$residuales_hurdle
    
    p1 <- ggplot(south_90) +
      geom_sf(aes(fill = fitted_hurdle)) +
      scale_fill_viridis_c(option = "plasma", name = "Ajustado", na.value = "grey80") +
      labs(title = "Valores Ajustados",
           subtitle = "Modelo Hurdle Espacial") +
      theme_minimal()
    
    p2 <- ggplot(south_90) +
      geom_sf(aes(fill = residuales_hurdle)) +
      scale_fill_gradient2(low = "blue", mid = "white", high = "red",
                           midpoint = 0, name = "Residuales", na.value = "grey80") +
      labs(title = "Residuales de Pearson",
           subtitle = "Modelo Hurdle Espacial") +
      theme_minimal()
    
    grid.arrange(p1, p2, ncol = 2)
  } else {
    cat("Nota: No se pueden crear mapas - dimensiones no coinciden\n")
  }
}
Valores ajustados calculados para 1412 observaciones

11.11 Comparación: Frecuencias Observadas vs Ajustadas

Comparamos el ajuste del modelo Poisson simple contra el modelo Hurdle Espacial para evaluar la mejora en la captura del exceso de ceros y la distribución general.

if(exists("modelo_hurdle_espacial") && "HC90" %in% names(south_90) && exists("datos_modelo")) {
  
  # Parámetros
  n_obs <- nrow(datos_modelo)
  max_count <- min(30, max(datos_modelo$HC90, na.rm = TRUE))
  
  # Frecuencias observadas
  obs_freq <- sapply(0:max_count, function(k) sum(datos_modelo$HC90 == k))
  
  # --- Frecuencias esperadas del modelo Hurdle Espacial ---
  # Probabilidad de cero (del componente binomial)
  prob_zero <- predict(modelo_hurdle_espacial, type = "prob")[, 1]
  
  # Detectar si el modelo usa Poisson o Binomial Negativa
  es_negbin <- !is.null(modelo_hurdle_espacial$theta)
  
  if(es_negbin) {
    # Binomial Negativa truncada
    mu_count <- predict(modelo_hurdle_espacial, type = "count")
    theta_count <- modelo_hurdle_espacial$theta["count"]
    
    hurdle_freq <- sapply(0:max_count, function(k) {
      if(k == 0) {
        sum(prob_zero)
      } else {
        prob_positive <- 1 - prob_zero
        prob_k_nb <- dnbinom(k, size = theta_count, mu = mu_count)
        prob_0_nb <- dnbinom(0, size = theta_count, mu = mu_count)
        prob_k_given_positive <- prob_k_nb / (1 - prob_0_nb)
        sum(prob_positive * prob_k_given_positive)
      }
    })
    dist_label <- paste0("Hurdle NB Espacial (θ = ", round(theta_count, 2), ")")
  } else {
    # Poisson truncada
    lambda_count <- predict(modelo_hurdle_espacial, type = "count")
    
    hurdle_freq <- sapply(0:max_count, function(k) {
      if(k == 0) {
        sum(prob_zero)
      } else {
        prob_positive <- 1 - prob_zero
        prob_k_given_positive <- dpois(k, lambda_count) / (1 - dpois(0, lambda_count))
        sum(prob_positive * prob_k_given_positive)
      }
    })
    dist_label <- "Hurdle Poisson Espacial"
  }
  
  # --- Frecuencias esperadas Poisson simple (para comparación) ---
  lambda_poisson <- mean(datos_modelo$HC90)
  poisson_freq <- dpois(0:max_count, lambda_poisson) * n_obs
  
  # DataFrame para gráfico
  df_comp <- data.frame(
    Conteo = 0:max_count,
    Observado = obs_freq,
    Hurdle = hurdle_freq,
    Poisson = poisson_freq
  )
  
  # Gráfico 1: Observado vs Poisson Simple
  p1 <- ggplot(df_comp, aes(x = Conteo)) +
    geom_bar(aes(y = Observado), stat = "identity", fill = "steelblue", alpha = 0.7) +
    geom_line(aes(y = Poisson), color = "red", linewidth = 1) +
    geom_point(aes(y = Poisson), color = "red", size = 2) +
    labs(title = "Observado vs Poisson Simple",
         subtitle = "Línea roja = distribución Poisson (solo media)",
         x = "Conteo", y = "Frecuencia") +
    theme_minimal()
  
  # Gráfico 2: Observado vs Hurdle Espacial
  p2 <- ggplot(df_comp, aes(x = Conteo)) +
    geom_bar(aes(y = Observado), stat = "identity", fill = "steelblue", alpha = 0.7) +
    geom_line(aes(y = Hurdle), color = "darkgreen", linewidth = 1) +
    geom_point(aes(y = Hurdle), color = "darkgreen", size = 2) +
    labs(title = paste("Observado vs", dist_label),
         subtitle = "Línea verde = distribución Hurdle + MESF",
         x = "Conteo", y = "Frecuencia") +
    theme_minimal()
  
  grid.arrange(p1, p2, ncol = 2)
}

═══════════════════════════════════════════════════════════════
     MÉTRICAS DE BONDAD DE AJUSTE (CORREGIDAS)                 
═══════════════════════════════════════════════════════════════


Nota: La mejora se calcula sobre el AIC, que es la métrica apropiada
para comparar modelos estadísticos. El RMSE y MAE son métricas
de ajuste a las frecuencias observadas vs esperadas.

12. Resumen de Metodologías Aplicadas

Resumen de Metodologías Implementadas
Sección Metodología Referencia Objetivo
6-7 Análisis Exploratorio Espacial Maczokni et al. - Crime Mapping in R Detectar patrones espaciales
7 Tests de Autocorrelación (Moran I, Local G) Anselin (2007) Cuantificar dependencia espacial
8-10 Modelos de Regresión Espacial (OLS, SLX, SAR, SEM, SDM, SDEM, Manski, SARAR) Bohorquez - SpatFD Tutorial Modelar dependencia espacial en errores o variable dependiente
11 Modelos Hurdle Espaciales con MESF Montilla et al. (2023) - Frontiers in Applied Mathematics Manejar exceso de ceros + autocorrelación espacial

13. Conclusiones

Hallazgos Principales

  1. Autocorrelación Espacial Inicial: El test de Moran’s I mostró autocorrelación significativa (I = 0.257), justificando el uso de modelos espaciales.

  2. Modelo de Regresión Espacial Óptimo: El modelo Manski obtuvo el menor AIC (8924.38), mejorando en 84.36 unidades respecto al OLS.

  3. Autocorrelación Residual: Tras ajustar el modelo espacial, el índice de Moran en residuales es -0.0032 (p = 0.5614).

  4. Modelos Hurdle con MESF:

    • El modelo Hurdle Espacial mejora el ajuste respecto al Hurdle simple
    • Mejora por AIC: -1334.51 unidades (-25.4%)
    • Los eigenvectores de Moran capturan patrones espaciales residuales
Nota Metodológica

Los coeficientes de los eigenvectores de Moran (MEMs) no deben interpretarse como efectos sustantivos. Son controles espaciales que absorben la autocorrelación residual. Sus Rate Ratios pueden presentar valores extremos debido a la naturaleza abstracta de los patrones espaciales que capturan y posible multicolinealidad entre eigenvectores adyacentes.

  1. Variables Significativas: Las variables de privación de recursos (RD90), estructura poblacional (PS90) y tasa de divorcios (DV90) son predictores consistentemente significativos de la tasa/conteo de homicidios.

Comparación de Enfoques

Enfoque Ventajas Limitaciones
OLS Simple, fácil interpretación Ignora dependencia espacial
SAR/SEM Modela dependencia espacial Asume distribución normal
Hurdle Maneja exceso de ceros No considera espacialidad directamente
Hurdle + MESF Combina todas las ventajas Mayor complejidad computacional

Implicaciones Metodológicas

  • La metodología de Montilla et al. (2023) es especialmente útil cuando:

    • Los datos presentan exceso de ceros (muchas áreas sin eventos)
    • Hay sobredispersión (varianza > media)
    • Existe autocorrelación espacial
  • Los eigenvectores de Moran actúan como proxies de variables espaciales no observadas, capturando patrones que no pueden medirse directamente.

  • Recomendaciones para futuros análisis:

    • Usar selección stepwise de MEMs basada en AIC
    • Escalar los eigenvectores para estabilidad numérica
    • No interpretar los coeficientes de MEMs como efectos causales
    • Considerar validación cruzada para evaluar generalización

Referencias

  • Baller, R., L. Anselin, S. Messner, G. Deane y D. Hawkins (2001). “Structural Covariates of US County Homicide Rates: Incorporating Spatial Effects.” Criminology 39, 561-590.
  • Anselin, L. (2007). Spatial Regression Analysis in R: A Workbook. GeoDa Center.
  • Ward, M. y K. Gleditsch (2008). Spatial Regression Models. Sage Publications.
  • Bohorquez, M.P. SpatFD - Functional Geostatistics. https://mpbohorquezc.github.io/SpatFD-Functional-Geostatistics/
  • Montilla Velásquez, M.P., Bohorquez Castañeda, M.P., y Rentería Ramos, R. (2023). “An implementation of Hurdle models for spatial count data.” Frontiers in Applied Mathematics and Statistics 9:1150735. https://doi.org/10.3389/fams.2023.1150735
  • Cameron, A.C. y Trivedi, P.K. (2013). Regression Analysis of Count Data. Cambridge University Press.
  • Griffith, D.A. y Chun, Y. (2019). “Implementing Moran eigenvector spatial filtering for massively large georeferenced datasets.” International Journal of Geographical Information Science 33, 1703-1717.

Documento generado con Quarto el 2025-12-13