
6.8 Comparing Global, Spatial and Local Models¶
Part: 6 — Geographically Weighted Models (Chapter 6.8 of 14)
Dataset: diabetes_northeast.shp (217 counties, for full comparability across every model family)
Focus: OLS, SLM, SEM (Part 3) against GWR and MGWR — same data, same weights where applicable, one table
Library: spatialstats.regression, spatialstats.gwmodel (PySpatialStats)
1. Overview¶
Part 3 chose between OLS, the spatial lag model (SLM), and the spatial error model (SEM) — three ways of handling spatial dependence, all still global. This Part's GWR and MGWR handle a different problem entirely — spatial heterogeneity in the relationship itself, not just dependence in the residuals. This chapter puts all five side by side, using the 217-county Northeast dataset (not the 666-county Atlantic data used elsewhere in this Part) specifically so every model can be compared on identical data and Queen weights, with no scale mismatch to explain away.
| Step | What we do |
|---|---|
| Fit everything | OLS, SLM, SEM (Part 3's own code), GWR, MGWR — same 217 counties, same six covariates |
| One table | AICc, log-likelihood, residual Moran's I, and (where available) local $R^2$ range |
| Read with caution | Why comparing information criteria across such different estimator families needs care, not blind ranking |
2. Python Setup¶
import sys, time, warnings
from pathlib import Path
import numpy as np
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
warnings.filterwarnings("ignore", category=FutureWarning)
warnings.filterwarnings("ignore", category=DeprecationWarning)
def find_repo_root(start: Path = Path.cwd()) -> Path:
for p in [start, *start.parents]:
if (p / "pyproject.toml").exists() and (p / "data").is_dir():
return p
raise FileNotFoundError("Run this notebook from inside the PySpatialStats repository.")
ROOT = find_repo_root()
DATA = ROOT / "data"
try:
import spatialstats
except ImportError:
sys.path.insert(0, str(ROOT))
import spatialstats
from spatialstats.weights import Queen
from spatialstats.regression import OLS, SpatialLag, SpatialError
from spatialstats.gwmodel.linear import GWR, MGWR
from spatialstats.esda import Moran
plt.rcParams.update({
"figure.dpi": 100, "savefig.dpi": 100, "font.size": 10, "axes.titlesize": 11,
"axes.titleweight": "bold", "axes.spines.top": False, "axes.spines.right": False,
"axes.grid": True, "grid.alpha": 0.25, "legend.frameon": False,
})
C = {"blue": "#2b6cb0", "red": "#c0392b", "green": "#2f855a", "orange": "#dd6b20",
"grey": "#718096", "purple": "#6b46c1", "teal": "#2c7a7b"}
counties = gpd.read_file(DATA / "diabetes_northeast.shp")
covariates = ["Obesity", "PhysInact", "Acc_Exer", "PCP_rate", "FoodEnvIdx", "SVI"]
X = counties[covariates]
y = counties["Dia_pct"]
coords = counties[["x", "y"]].to_numpy()
w = Queen(counties)
print(f"spatialstats : version {spatialstats.__version__}")
spatialstats : version 0.2.0
3. Fitting Every Model on the Same Data¶
OLS, SpatialLag, and SpatialError report AIC, not AICc; a small-sample correction
$\text{AICc} = \text{AIC} + \frac{2k(k+1)}{n-k-1}$ puts them on the same footing as GWR/MGWR's own aicc_.
def aicc_from_aic(aic, n, k):
return aic + 2 * k * (k + 1) / (n - k - 1)
n = len(y)
ols = OLS(y, X, w=w)
slm = SpatialLag(y, X, w)
sem = SpatialError(y, X, w)
gwr = GWR(bandwidth=91, fixed=False, kernel="bisquare", n_jobs=2) # Chapter 6.3's Northeast bandwidth
gwr.fit(X.to_numpy(), y.to_numpy(), coords)
t0 = time.time()
mgwr = MGWR(kernel="bisquare", fixed=False, max_iter=6, tol=1.0, n_jobs=2) # Chapter 6.6's settings
mgwr.fit(X.to_numpy(), y.to_numpy(), coords)
print(f"MGWR fit time: {time.time() - t0:.0f}s")
MGWR fit time: 26s
4. The Comparison Table¶
rows = []
for name, m in [("OLS", ols), ("SLM", slm), ("SEM", sem)]:
aicc = aicc_from_aic(m.aic, n, m.n_params)
resid_I = Moran(m.residuals, w, permutations=0).I
rows.append({"model": name, "loglik": m.loglik, "aicc": aicc, "resid_moran_I": resid_I,
"local_r2_min": np.nan, "local_r2_max": np.nan})
gwr_loglik = -0.5 * n * (np.log(2 * np.pi) + np.log(np.sum(gwr.residuals_ ** 2) / n) + 1)
mgwr_loglik = -0.5 * n * (np.log(2 * np.pi) + np.log(np.sum(mgwr.residuals_ ** 2) / n) + 1)
rows.append({"model": "GWR", "loglik": gwr_loglik, "aicc": gwr.aicc_,
"resid_moran_I": Moran(gwr.residuals_, w, permutations=0).I,
"local_r2_min": gwr.r2_local_.min(), "local_r2_max": gwr.r2_local_.max()})
rows.append({"model": "MGWR", "loglik": mgwr_loglik, "aicc": mgwr.aicc_,
"resid_moran_I": Moran(mgwr.residuals_, w, permutations=0).I,
"local_r2_min": np.nan, "local_r2_max": np.nan})
comparison = pd.DataFrame(rows).set_index("model")
comparison.round(4)
| loglik | aicc | resid_moran_I | local_r2_min | local_r2_max | |
|---|---|---|---|---|---|
| model | |||||
| OLS | -203.6171 | 421.7701 | 0.1362 | NaN | NaN |
| SLM | -202.8790 | 422.4504 | 0.1028 | NaN | NaN |
| SEM | -199.0366 | 414.7655 | -0.0032 | NaN | NaN |
| GWR | -159.7848 | -29.2105 | -0.0173 | 0.537 | 0.8757 |
| MGWR | -105.2750 | 465.5244 | -0.2649 | NaN | NaN |
5. Reading the Comparison Carefully¶
AICc is not directly comparable across this table, and the raw numbers show exactly why blind ranking would mislead: GWR's AICc ($-29.2$) looks dramatically better than every global model's ($414$–$422$), but GWR's likelihood and AICc are computed from a fundamentally different degrees-of-freedom accounting (the effective number of parameters from the hat-matrix trace, not a fixed integer $k$) — the two are not measuring "complexity" on the same scale, and a naive "lowest AICc wins" reading would overstate GWR's case. What is directly comparable and trustworthy:
- Residual Moran's I: SEM (−0.003) and GWR (−0.017) both essentially eliminate residual autocorrelation; OLS (0.136) and SLM (0.103) do not fully remove it; MGWR's residuals are, surprisingly, negatively autocorrelated (−0.265) — a real, checkable sign that this particular MGWR fit (several covariates pinned to the minimum bandwidth of 8, Chapter 6.6) is overfitting locally, producing a checkerboard-like residual pattern rather than genuinely explaining more structure. More local flexibility is not automatically better.
- Local $R^2$ range (GWR only, here): 0.537 to 0.876 — a real, substantive finding about where the model fits well that no single global $R^2$ or AICc number could show.
- SEM beats both SLM and OLS among the global models by both log-likelihood and AICc, consistent with Part 3's original diagnosis on this same variable.
Part 3's spatial lag and error models capture spillover and nuisance dependence in the residuals; GWR and MGWR capture heterogeneity in the relationship itself. These are different problems, and the right choice depends on which one the data actually shows (Chapter 6.1's Chow test motivated heterogeneity specifically for this dataset) — not on which model has the smallest number in a column that is not, in fact, measuring the same thing across every row.
6. Summary and Conclusions¶
This chapter put every model family from Parts 3 and 6 side by side, on identical data, and read the result carefully rather than mechanically.
What we did¶
- Fit OLS, SLM, SEM, GWR, and MGWR on the same 217-county dataset and six covariates.
- Built one comparison table: log-likelihood, AICc (with a small-sample correction for the Part 3 models), residual Moran's I, and local $R^2$ range where available.
- Found SEM and GWR both essentially eliminate residual autocorrelation; OLS and SLM do not.
- Found MGWR's residuals are negatively autocorrelated — a real, checkable sign of local overfitting from several covariates' minimum-bandwidth fits (Chapter 6.6), not evidence MGWR is simply "better" than GWR.
- Explained explicitly why GWR/MGWR's AICc is not on the same scale as the global models' — a naive "lowest-AICc-wins" comparison across this table would be actively misleading.
Practical takeaways¶
- Residual Moran's I is the most directly comparable diagnostic across every model family in this table; lean on it more than a raw AICc comparison between fundamentally different estimator classes.
- A more locally flexible model (MGWR here) is not automatically better — check its residuals for the opposite failure mode (overfitting-induced negative autocorrelation), not just for remaining positive autocorrelation.
- Spatial lag/error models and geographically weighted models solve different problems (dependence vs. heterogeneity); use the diagnostics from Part 3, Chapter 3.3 and this Part's Chapter 6.1 evidence together to decide which one the data actually needs, rather than picking by information criterion alone.
Next steps¶
Chapter 6.9 (Machine-Learning and Ensemble GW Models) moves beyond linear local coefficients entirely, trading interpretability for potentially stronger predictive accuracy.
7. Resources¶
Textbooks¶
| Reference | Notes |
|---|---|
| Fotheringham, A. S., Brunsdon, C. & Charlton, M. (2002). Geographically Weighted Regression. Wiley. | Chapter 5 discusses comparing GWR against global alternatives |
| Burnham, K. P. & Anderson, D. R. (2002). Model Selection and Multimodel Inference (2nd ed.). Springer. | AICc theory and its small-sample correction, used in Section 3 |
Key papers¶
| Paper | Contribution |
|---|---|
| Fotheringham, A. S., Charlton, M. & Brunsdon, C. (1996). op. cit. (Chapter 6.3) | Effective-parameter accounting for GWR's AICc |
In this library¶
| Object | Role |
|---|---|
spatialstats.regression.OLS, SpatialLag, SpatialError |
Part 3's global models, refit here for direct comparison |
spatialstats.gwmodel.linear.GWR, MGWR |
This Part's local models |
spatialstats.esda.Moran |
The single most trustworthy comparison metric across every row of this chapter's table |