
6.1 Introduction and Motivation¶
Part: 6 — Geographically Weighted Models (Chapter 6.1 of 14)
Dataset: diabetes_atlantic.shp (666 counties)
Focus: Why a single global relationship (Part 3) is not always enough, made concrete with real evidence
Library: spatialstats.gwmodel (PySpatialStats)
1. Overview¶
Every model in Part 3 — OLS, the spatial lag model, the spatial error model — fits one coefficient per covariate for the entire study region. Part 3, Chapter 3.6 already tested that assumption directly on this same data (a North/South split, a Chow test, F=2.85, p=0.004) and found it fails: the relationship between obesity and diabetes prevalence is not the same everywhere. Geographically weighted models generalise that finding into a full toolkit — instead of one region, or two arbitrary halves, fit a continuously varying coefficient surface, one local regression centred at every location.
| Step | What we do |
|---|---|
| Stationarity | State precisely what Part 3's global models assume, and why "stationary" is the technical name for it |
| Evidence | Reuse Part 3, Chapter 3.6's own finding as the concrete motivating evidence, rather than asserting non-stationarity in the abstract |
| Local regression | The core idea: a kernel-weighted regression re-fit at every location |
| Bandwidth | Why bandwidth is best understood as a spatial scale parameter, not just a tuning knob |
Chapter 6.2 prepares every dataset this Part uses; Chapter 6.3 builds the kernel and bandwidth machinery in full; Chapter 6.5 fits the first real GWR model.
2. Python Setup¶
import sys, 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.regression import OLS
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_atlantic.shp")
print(f"spatialstats : version {spatialstats.__version__}")
print(f"counties : {counties.shape[0]}")
spatialstats : version 0.2.0 counties : 666
3. The Stationarity Assumption of Global Models¶
A model is spatially stationary if the relationship it estimates — the coefficient $\beta_k$ linking covariate $x_k$ to the outcome — is assumed constant across the entire study region. OLS, the spatial lag model, and the spatial error model (Part 3) are all stationary in exactly this sense: $\rho$ and $\lambda$ let the residual structure vary spatially, but $\beta$ itself is one fixed number, everywhere, by construction. Stationarity is not a flaw in these models — it is a deliberate simplification, appropriate whenever the underlying relationship really is (approximately) uniform. The question this Part exists to answer is: when is it not?
4. Evidence of Non-stationarity¶
Rather than assert that relationships vary spatially, reuse Part 3, Chapter 3.6's own direct test on this exact dataset: splitting the 666 Atlantic counties into a northern and a southern half and refitting the same OLS specification separately in each.
covariates = ["Obesity", "PhysInact", "Acc_Exer", "PCP_rate", "FoodEnvIdx", "SVI"]
X = counties[covariates].copy()
X["Urban"] = (counties["UrbRural"] == "Urban").astype(int)
y = counties["Dia_pct"]
cy = counties.geometry.centroid.y
north = counties[cy >= cy.median()].reset_index(drop=True)
south = counties[cy < cy.median()].reset_index(drop=True)
def build_Xy(sub):
Xs = sub[covariates].copy()
Xs["Urban"] = (sub["UrbRural"] == "Urban").astype(int)
return Xs, sub["Dia_pct"]
X_n, y_n = build_Xy(north)
X_s, y_s = build_Xy(south)
ols_pooled = OLS(y, X)
ols_n = OLS(y_n, X_n)
ols_s = OLS(y_s, X_s)
coef_compare = pd.DataFrame({
"Pooled": ols_pooled.beta, "North": ols_n.beta, "South": ols_s.beta,
}, index=ols_pooled.names).drop(index="const")
coef_compare.round(4)
| Pooled | North | South | |
|---|---|---|---|
| Obesity | 0.1462 | 0.1446 | 0.1776 |
| PhysInact | 0.1520 | 0.1536 | 0.1425 |
| Acc_Exer | -0.0060 | -0.0043 | -0.0065 |
| PCP_rate | -0.0003 | 0.0005 | -0.0015 |
| FoodEnvIdx | -0.2002 | -0.1673 | -0.2149 |
| SVI | 1.0327 | 0.6427 | 0.8865 |
| Urban | -0.0144 | 0.0048 | -0.1355 |
Obesity's coefficient is 23% larger in the South than the North; SVI's is 38% larger; Urban even changes
sign. Part 3's formal Chow test already confirmed this difference is not sampling noise (F=2.85, p=0.004) — a
global model, fit once for all 666 counties, necessarily reports a single compromise value that fits
neither region's true relationship particularly well. A two-region split is a blunt instrument, though: it
assumes the "right" partition is exactly two contiguous blocks split at one arbitrary latitude. Real geographic
variation is rarely a clean step function.
5. Local Regression and Bandwidth as a Spatial Scale¶
Geographically weighted regression (GWR) generalises the North/South split to its natural conclusion: instead of one region, or two, fit a separate weighted regression centred at every observation, using a kernel to down-weight distant observations smoothly rather than including or excluding them with a hard boundary.
$$ \hat\beta(\mathbf{u}_i) = \left(X^\top W(\mathbf{u}_i) X\right)^{-1} X^\top W(\mathbf{u}_i) y, $$
where $W(\mathbf{u}_i) = \text{diag}(w_{i1}, \dots, w_{in})$ and $w_{ij}$ is a kernel function of the distance between location $i$ and every other observation $j$. Every location $i$ gets its own $\hat\beta(\mathbf{u}_i)$ — a full coefficient surface, not a single number or two regional numbers.
The bandwidth controls how quickly $w_{ij}$ decays with distance, and is best understood as a spatial scale parameter rather than an arbitrary tuning knob:
- A very large bandwidth makes every $w_{ij} \approx 1$ everywhere — GWR degenerates exactly to global OLS, recovering Part 3's single pooled coefficient.
- A very small bandwidth makes $w_{ij}$ nearly zero for all but the closest few neighbours — each local regression is fit on a tiny, highly local subset, extremely responsive to local variation but also to noise.
- The useful range in between is where GWR earns its keep: local enough to capture real regional differences like the North/South split above, but not so local that each fit is dominated by a handful of noisy observations.
Choosing the bandwidth well — not too large, not too small — is the single most consequential decision in any GWR analysis, and it is the entire subject of Chapter 6.3.
6. Summary and Conclusions¶
This chapter motivated Part 6 with concrete evidence, not an abstract claim.
What we did¶
- Defined spatial stationarity precisely: one fixed $\beta$ for the whole region, shared by every model in Part 3.
- Reused Part 3, Chapter 3.6's own North/South coefficient comparison and Chow test as direct evidence that this assumption fails on the very data used throughout this Part.
- Introduced local (kernel-weighted) regression as the natural generalisation of an arbitrary two-region split into a continuously varying coefficient surface.
- Explained bandwidth as a spatial-scale parameter, with global OLS as its large-bandwidth limiting case.
Practical takeaways¶
- GWR is not a replacement for Part 3's models — it is the tool for exactly the situation Part 3, Chapter 3.6 flagged: when a single global coefficient hides real, testable spatial variation.
- Every GWR result is conditional on its bandwidth in the same way every Part 2/3 result was conditional on its weights definition — treat bandwidth choice with the same scrutiny.
Next steps¶
Chapter 6.2 (Data) prepares every dataset this Part uses: the real Atlantic-county health data, a space-time panel for later spatiotemporal chapters, and the library's bundled simulation datasets used only for API demonstrations.
7. Resources¶
Textbooks¶
| Reference | Notes |
|---|---|
| Fotheringham, A. S., Brunsdon, C. & Charlton, M. (2002). Geographically Weighted Regression: The Analysis of Spatially Varying Relationships. Wiley. | The foundational GWR text, and this Part's primary reference throughout |
| Fotheringham, A. S., Yang, W. & Kang, W. (2017). Multiscale geographically weighted regression (MGWR). Annals of the American Association of Geographers, 107, 1247–1265. | Extended to Chapter 6.6 |
Key papers¶
| Paper | Contribution |
|---|---|
| Brunsdon, C., Fotheringham, A. S. & Charlton, M. (1996). Geographically weighted regression: a method for exploring spatial nonstationarity. Geographical Analysis, 28, 281–298. | Original GWR paper |
| Fotheringham, A. S., Brunsdon, C. & Charlton, M. (1998). Geographically weighted regression: a natural evolution of the expansion method for spatial data analysis. Environment and Planning A, 30, 1905–1927. | Frames GWR as a natural generalisation of exactly this chapter's regional-split argument |
In this library¶
| Object | Role |
|---|---|
spatialstats.regression.OLS |
Reused from Part 3 for the motivating evidence in Section 4 |
spatialstats.gwmodel.linear.GWR |
The core model this Part builds toward — first full treatment in Chapter 6.5 |
| Chapter 6.3 | Kernels, bandwidths, and how bandwidth is actually selected |