
6.6 Advanced Variants¶
Part: 6 — Geographically Weighted Models (Chapter 6.6 of 14)
Dataset: diabetes_northeast.shp (217 counties, for MGWR); diabetes_atlantic.shp (666 counties, for MixedGWR and GWGLM)
Focus: MGWR (a bandwidth per covariate), MixedGWR (global + local coefficients), and GWGLM — including a real, dramatic limitation
Library: spatialstats.gwmodel (PySpatialStats)
1. Overview¶
Chapter 6.5's GWR gives every covariate the same bandwidth — implicitly assuming every relationship varies at the same spatial scale. This chapter relaxes that, and two further assumptions: that every coefficient must vary locally at all, and that the outcome must be (conditionally) Gaussian.
| Step | What we do |
|---|---|
| MGWR | Back-fit a separate bandwidth per covariate; a large bandwidth means "nearly stationary", a small one means "sharply local" |
| MixedGWR | Fix some coefficients globally, let others vary — a direct, explicit middle ground |
| GWGLM | Poisson and binomial families for count and binary outcomes — and a genuine, dramatic limitation this library has |
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.gwmodel.linear import GWR, MGWR, MixedGWR, GWGLM
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"}
covariates = ["Obesity", "PhysInact", "Acc_Exer", "PCP_rate", "FoodEnvIdx", "SVI"]
atlantic = gpd.read_file(DATA / "diabetes_atlantic.shp")
X_atl = atlantic[covariates].to_numpy()
y_atl = atlantic["Dia_pct"].to_numpy()
coords_atl = atlantic[["x", "y"]].to_numpy()
BANDWIDTH = 131
print(f"spatialstats : version {spatialstats.__version__}")
spatialstats : version 0.2.0
3. MGWR: a Bandwidth per Covariate¶
Multiscale GWR relaxes GWR's single shared bandwidth: each covariate gets its own bandwidth, estimated
by back-fitting (repeatedly re-fitting one covariate's local surface holding the others fixed, cycling until
the coefficients stop changing). Interpretation is direct: a large bandwidth means the process is nearly
global (the covariate's true effect barely varies over space, so borrowing from distant observations does not
hurt); a small bandwidth means the process is genuinely local (its effect changes sharply over short
distances). MGWR's back-fitting is expensive at 666 observations — this section uses the 217-county Northeast
data to keep the fit practical, at a reduced max_iter that still shows clear convergence behaviour.
northeast = gpd.read_file(DATA / "diabetes_northeast.shp")
X_ne = northeast[covariates].to_numpy()
y_ne = northeast["Dia_pct"].to_numpy()
coords_ne = northeast[["x", "y"]].to_numpy()
t0 = time.time()
mgwr = MGWR(kernel="bisquare", fixed=False, max_iter=6, tol=1.0, n_jobs=2)
mgwr.fit(X_ne, y_ne, coords_ne)
print(f"fit time: {time.time() - t0:.1f}s")
names = ["Intercept"] + covariates
bw_table = pd.DataFrame({"covariate": names, "bandwidth": mgwr.bandwidths_}).sort_values("bandwidth")
bw_table
fit time: 25.1s
| covariate | bandwidth | |
|---|---|---|
| 0 | Intercept | 8.0 |
| 1 | Obesity | 8.0 |
| 5 | FoodEnvIdx | 8.0 |
| 4 | PCP_rate | 8.0 |
| 6 | SVI | 8.0 |
| 2 | PhysInact | 108.0 |
| 3 | Acc_Exer | 109.0 |
The back-fitting converges to two clearly different scales: PhysInact and Acc_Exer settle at large
bandwidths (108–109 neighbours, close to GWR's own single shared bandwidth of 91–131 found in Chapters 6.3 and
6.5) — their effects are close to spatially stationary. Every other covariate, including the intercept and
PCP_rate, settles at the minimum allowed bandwidth (8) — their local effects vary sharply, changing
meaningfully from one small cluster of neighbouring counties to the next. A single-bandwidth GWR (Chapter 6.5)
necessarily forces a compromise between these two genuinely different spatial scales; MGWR is the tool that
separates them.
4. MixedGWR: Some Coefficients Global, Some Local¶
Sometimes the choice is not "how local" but "local at all" — theory or a prior GWR/MGWR fit (Section 3's large-
bandwidth covariates) may suggest a specific covariate really is stationary. MixedGWR fixes it globally while
letting the rest vary, an explicit, interpretable middle ground between OLS and full GWR.
# indices into covariates: PhysInact=1, Acc_Exer=2 (both large-bandwidth / near-stationary in Section 3)
# fixed globally; the rest vary locally
fixed_idx = [1, 2]
varying_idx = [0, 3, 4, 5]
t0 = time.time()
mixed = MixedGWR(fixed_vars=fixed_idx, varying_vars=varying_idx, bandwidth=BANDWIDTH, fixed=False)
mixed.fit(X_atl, y_atl, coords_atl)
print(f"fit time: {time.time() - t0:.1f}s")
print("global (fixed) coefficients:")
for idx, val in zip(fixed_idx, mixed.global_coef_):
print(f" {covariates[idx]:12s}: {val:.4f}")
print(f"\nlocal coefficients shape: {mixed.coef_.shape} (intercept + {len(varying_idx)} varying covariates)")
fit time: 1.1s global (fixed) coefficients: PhysInact : 0.0236 Acc_Exer : -0.0071 local coefficients shape: (666, 5) (intercept + 4 varying covariates)
Acc_Exer's single global coefficient can be compared directly against its earlier local range from full GWR
— MixedGWR's advantage is exactly this kind of explicit, testable simplification, rather than defaulting to
"everything varies" without checking whether it needs to.
5. GWGLM: Beyond Gaussian Outcomes¶
GWGLM(family=...) extends the same local-fitting machinery to Poisson (counts) and binomial (proportions/
binary) outcomes via local IRLS. family="gaussian" should recover ordinary GWR exactly.
gwglm_gaussian = GWGLM(family="gaussian", bandwidth=BANDWIDTH, fixed=False)
gwglm_gaussian.fit(X_atl, y_atl, coords_atl)
gwr_ref = GWR(bandwidth=BANDWIDTH, fixed=False, kernel="bisquare", n_jobs=-1)
gwr_ref.fit(X_atl, y_atl, coords_atl)
print(f"GWGLM(gaussian) R2 : {gwglm_gaussian.score(X_atl, y_atl, coords_atl):.6f}")
print(f"GWR R2 : {gwr_ref.score(X_atl, y_atl, coords_atl):.6f} (should match exactly)")
print(f"coefficients match : {np.allclose(gwglm_gaussian.coef_, gwr_ref.coef_, atol=1e-6)}")
GWGLM(gaussian) R2 : 0.825360 GWR R2 : 0.825360 (should match exactly) coefficients match : True
Confirmed: family="gaussian" reproduces GWR's coefficients exactly, as documented.
5.1 A real limitation: no offset support¶
Every count variable in this Part's data (Dia_count) is a raw case count that scales with each county's
population — modelling it properly needs an offset ($\log E_i$, Part 5's expected-count machinery) so the
model targets the rate, not the raw count. GWGLM has no offset argument at all. Fitting it on the raw
counts directly, ignoring this, is not merely suboptimal — it breaks outright.
y_counts = atlantic["Dia_count"].to_numpy()
gwglm_poisson = GWGLM(family="poisson", bandwidth=BANDWIDTH, fixed=False)
gwglm_poisson.fit(X_atl, y_counts, coords_atl)
obesity_coef = gwglm_poisson.coef_[:, 1]
print(f"Poisson GWGLM on raw Dia_count (no offset), local Obesity coefficient:")
print(f" min={obesity_coef.min():.3e}, max={obesity_coef.max():.3e}")
print(f" (a sensible coefficient here is on the order of 0.01-0.1, as in the rate model above)")
Poisson GWGLM on raw Dia_count (no offset), local Obesity coefficient: min=-2.283e+26, max=2.810e+17 (a sensible coefficient here is on the order of 0.01-0.1, as in the rate model above)
The local coefficients are numerical nonsense — magnitudes around $10^{17}$–$10^{26}$, meaningless on any
substantive scale. The mechanism is direct: without an offset, the Poisson model tries to explain raw case
counts (which range from a few hundred to hundreds of thousands, driven overwhelmingly by each county's
population) using covariates that carry no population information at all. Local IRLS, fit on a kernel
neighbourhood of counties with wildly different population scales and no offset to account for it, diverges.
This is not a quirk of this particular bandwidth or kernel — it is a structural consequence of the missing
offset, and it is why GWGLM cannot currently replace Part 5's expected-count-adjusted models for rate
data. The only safe uses of family="poisson" in this library are on outcomes that are genuinely counts
without a meaningful exposure/population denominator, or where every observation's exposure is already equal.
# The correct alternative for rate data: model the RATE directly with the gaussian family, as done throughout
# this chapter and Chapter 6.5 — exactly the workaround this limitation requires.
print("Recommended workaround for exposure-adjusted counts: model the rate (Dia_pct) with "
"family='gaussian', not the raw count with family='poisson'.")
print(f"(Confirmed above: GWGLM(gaussian) on Dia_pct gives R2={gwglm_gaussian.score(X_atl, y_atl, coords_atl):.4f}, "
"a sensible, well-behaved fit.)")
Recommended workaround for exposure-adjusted counts: model the rate (Dia_pct) with family='gaussian', not the raw count with family='poisson'. (Confirmed above: GWGLM(gaussian) on Dia_pct gives R2=0.8254, a sensible, well-behaved fit.)
6. Summary and Conclusions¶
This chapter extended GWR along three axes, and found a genuine, dramatic implementation gap along the way.
What we did¶
- Fit MGWR on the Northeast data:
PhysInactandAcc_Exerconverge to large (near-stationary) bandwidths, every other covariate to the minimum allowed (sharply local) — two genuinely different spatial scales a single-bandwidth GWR must compromise between. - Fit MixedGWR, fixing those two large-bandwidth covariates globally while the rest vary locally.
- Confirmed
GWGLM(family="gaussian")reproduces GWR's coefficients exactly. - Demonstrated a real, severe limitation directly: fitting
GWGLM(family="poisson")on raw case counts without an offset produces numerically nonsensical coefficients (order $10^{17}$–$10^{26}$) —GWGLMcannot currently model exposure-adjusted rates, and the correct workaround is modelling the rate itself withfamily="gaussian".
Practical takeaways¶
- Read MGWR's per-covariate bandwidths as a direct statement about spatial scale: large = stationary, small =
sharply local — use them to decide which covariates are candidates for
MixedGWR'sfixed_vars. - Never fit
GWGLM(family="poisson")on a raw count that depends on a varying exposure/population without first converting it to a rate — this chapter showed directly what happens, not just what the documentation warns about. - When a package genuinely cannot do something (no offset support here), the correct response is a documented workaround (model the rate with a different family), not a silent, broken fit.
Next steps¶
Chapter 6.7 (Diagnostics and Model Checking) builds the full validation toolkit for the models fit across this chapter and Chapter 6.5: residual autocorrelation, non-stationarity tests, and local collinearity.
7. Resources¶
Textbooks¶
| Reference | Notes |
|---|---|
| Fotheringham, A. S., Yang, W. & Kang, W. (2017). Multiscale geographically weighted regression (MGWR). Annals of the American Association of Geographers, 107, 1247–1265. | Origin of MGWR, Section 3 |
| Nakaya, T., Fotheringham, A. S., Brunsdon, C. & Charlton, M. (2005). Geographically weighted Poisson regression for disease association mapping. Statistics in Medicine, 24, 2695–2717. | GWGLM's Poisson family, and the offset issue Section 5.1 surfaces |
Key papers¶
| Paper | Contribution |
|---|---|
| Mei, C.-L., Wang, N. & Zhang, W.-X. (2006). Testing the importance of the explanatory variables in a mixed geographically weighted regression model. Environment and Planning A, 38, 587–598. | Statistical foundations for MixedGWR |
In this library¶
| Object | Role |
|---|---|
spatialstats.gwmodel.linear.MGWR |
Per-covariate bandwidth back-fitting |
spatialstats.gwmodel.linear.MixedGWR |
fixed_vars / varying_vars |
spatialstats.gwmodel.linear.GWGLM |
Non-Gaussian families — see Section 5.1 for the offset limitation |
| Part 5 | The expected-count / offset machinery GWGLM cannot currently use |