Spatial Econometrics (R)¶
spatialreg: lag, error, LM tests, and impacts¶
The R counterpart to spatecon_python.ipynb. Where the Python notebook builds Bayesian SAR/SEM from scratch, this notebook uses the field-standard spatialreg: lagsarlm for the spatial-lag model, errorsarlm for the spatial-error model, impacts for the direct/indirect/total effects, and the Lagrange-multiplier tests (spdep) that formally guide the lag-vs-error choice. Same Columbus crime data, with sf/ggplot2 maps.
options(repr.plot.width=8.2, repr.plot.height=3.4)
.libPaths(c("C:/Users/user/R/win-library/4.6", .libPaths()))
suppressMessages({library(sf); library(spdep); library(spatialreg); library(spData); library(ggplot2); library(patchwork)})
col <- st_read(system.file("shapes/columbus.gpkg", package="spData"), quiet=TRUE)
st_crs(col) <- NA # Columbus coords are arbitrary planar units; drop CRS so ggplot maps don't attempt a PROJ transformation
nb <- poly2nb(col); lw <- nb2listw(nb, style="W")
cat(nrow(col), "neighbourhoods; crime ~ income (INC) + housing value (HOVAL)\n")
Warning message: "package 'spatialreg' was built under R version 4.6.1"
49 neighbourhoods; crime ~ income (INC) + housing value (HOVAL)
1. OLS, residual autocorrelation, and the lag-vs-error tests¶
Fit OLS, map crime and the residuals, and run the diagnostics: lm.morantest for residual autocorrelation, and the Lagrange-multiplier tests whose robust versions (RLMerr, RLMlag) point to whether a lag or an error specification is the better fix.
ols <- lm(CRIME ~ INC + HOVAL, data=col); col$resid <- resid(ols)
cat("OLS coefficients:\n"); print(round(coef(ols),2))
cat(sprintf("\nMoran's I on OLS residuals: p = %.4f (spatial autocorrelation present)\n", lm.morantest(ols, lw)$p.value))
lmt <- tryCatch(lm.RStests(ols, lw, test="all"), error=function(e) lm.LMtests(ols, lw, test="all"))
cat("\nLagrange-multiplier tests (small p = that alternative is needed):\n")
print(sapply(lmt, function(t) round(t$p.value,4)))
p1<-ggplot(col)+geom_sf(aes(fill=CRIME),color="white",linewidth=0.2)+scale_fill_distiller(palette="OrRd",direction=1)+labs(title="Crime",fill="")+theme_void()
p2<-ggplot(col)+geom_sf(aes(fill=resid),color="white",linewidth=0.2)+scale_fill_distiller(palette="RdBu")+labs(title="OLS residuals (clustered)",fill="")+theme_void()
print(p1 + p2)
cat("\nThe residuals are spatially autocorrelated, so OLS is mis-specified; the robust LM tests indicate which\n")
cat("spatial specification (lag or error) to prefer.\n")
OLS coefficients:
(Intercept) INC HOVAL
68.62 -1.60 -0.27
Moran's I on OLS residuals: p = 0.0023 (spatial autocorrelation present)
Lagrange-multiplier tests (small p = that alternative is needed):
RSerr RSlag adjRSerr adjRSlag SARMA 0.0225 0.0029 0.8340 0.0533 0.0114
The residuals are spatially autocorrelated, so OLS is mis-specified; the robust LM tests indicate which
spatial specification (lag or error) to prefer.
2. Spatial lag (SAR) and its impacts¶
lagsarlm fits $y=\rho Wy+X\beta+\varepsilon$ by maximum likelihood, and impacts turns the coefficients into the direct / indirect / total effects via the spatial multiplier — the numbers to report.
m_lag <- lagsarlm(CRIME ~ INC + HOVAL, data=col, listw=lw)
cat(sprintf("spatial-lag rho = %.2f (p = %.4f)\n", m_lag$rho, summary(m_lag)$LR1$p.value))
imp <- impacts(m_lag, listw=lw, R=2000)
cat("\ndirect / indirect / total effects:\n"); print(round(imp$res$direct,2));
eff <- data.frame(direct=imp$res$direct, indirect=imp$res$indirect, total=imp$res$total, row.names=c("INC","HOVAL"))
print(round(eff,2))
cat("\nAs in the Python notebook, income's TOTAL effect on crime exceeds its coefficient once the spillover\n")
cat("(indirect) part is included -- the effect, not the coefficient, is the policy-relevant quantity.\n")
spatial-lag rho = 0.42 (p = 0.0022)
direct / indirect / total effects:
[,1] [1,] -1.10 [2,] -0.28
direct indirect total INC -1.10 -0.72 -1.82 HOVAL -0.28 -0.18 -0.46
As in the Python notebook, income's TOTAL effect on crime exceeds its coefficient once the spillover
(indirect) part is included -- the effect, not the coefficient, is the policy-relevant quantity.
3. Spatial error (SEM), and the comparison¶
errorsarlm fits the spatial-error model. Comparing coefficients across OLS, SAR and SEM shows the substantive difference: under SEM the covariate effects stay near OLS (the spatial term only cleans the errors), while under SAR they shift (the spatial term absorbs a spillover).
m_err <- errorsarlm(CRIME ~ INC + HOVAL, data=col, listw=lw)
cat(sprintf("spatial-error lambda = %.2f (p = %.4f)\n\n", m_err$lambda, summary(m_err)$LR1$p.value))
comp <- rbind(OLS=coef(ols)[c("INC","HOVAL")],
SAR=coef(m_lag)[c("INC","HOVAL")],
SEM=coef(m_err)[c("INC","HOVAL")])
print(round(comp,2))
cat(sprintf("\nAIC: OLS %.0f, SAR %.0f, SEM %.0f (lower = better)\n", AIC(ols), AIC(m_lag), AIC(m_err)))
cat(sprintf("Both improve on OLS, and both criteria point the same way. The ROBUST LM tests are the ones to\n"))
cat(sprintf("read: adjRSerr p = %.2f says no error term is needed once a lag is allowed, while adjRSlag p = %.3f\n",
lmt[["adjRSerr"]]$p.value, lmt[["adjRSlag"]]$p.value))
cat(sprintf("says a lag is still needed once an error term is allowed. AIC agrees: SAR %.0f < SEM %.0f < OLS %.0f.\n",
AIC(m_lag), AIC(m_err), AIC(ols)))
cat("So on Columbus the evidence favours the LAG specification -- neighbours' crime behaving as a genuine\n")
cat("spillover rather than a correlated omitted factor. That is a statistical verdict, not a causal one:\n")
cat("the tests choose between two error structures, and only theory can license the causal reading.\n")
spatial-error lambda = 0.55 (p = 0.0071)
INC HOVAL OLS -1.60 -0.27 SAR -1.05 -0.27 SEM -0.96 -0.30
AIC: OLS 383, SAR 375, SEM 377 (lower = better)
Both improve on OLS, and both criteria point the same way. The ROBUST LM tests are the ones to
read: adjRSerr p = 0.83 says no error term is needed once a lag is allowed, while adjRSlag p = 0.053
says a lag is still needed once an error term is allowed. AIC agrees: SAR 375 < SEM 377 < OLS 383.
So on Columbus the evidence favours the LAG specification -- neighbours' crime behaving as a genuine
spillover rather than a correlated omitted factor. That is a statistical verdict, not a causal one:
the tests choose between two error structures, and only theory can license the causal reading.
4. Summary¶
spatialreg is the standard toolkit and reproduces the from-scratch Bayesian results. lm.morantest and the Lagrange-multiplier tests diagnose the spatial mis-specification and point to lag vs error; lagsarlm and errorsarlm fit the two models; and crucially impacts delivers the direct / indirect / total effects, confirming that in the lag model the covariate's total impact — spillovers included — exceeds its coefficient. The lag-vs-error contrast (spillover to interpret vs nuisance to correct) is the identification decision at the heart of spatial econometrics.
spatialreg/spdep (with sf/ggplot2 maps) are the package mirror of the Bayesian SAR/SEM sampler in spatecon_python.ipynb, and PySAL's spreg is the Python-package equivalent. This reuses the same neighbour graph as the areal CAR models but in a simultaneous-autoregressive form — the deliberate distinction of this arc. Next: the spatial Durbin model (spillovers in the covariates too); then the arc turns to geostatistics/kriging.