Banner

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)


Table of Contents¶

  1. Overview

  2. Python Setup

  3. MGWR: a Bandwidth per Covariate

  4. MixedGWR: Some Coefficients Global, Some Local

  5. GWGLM: Beyond Gaussian Outcomes

    • 5.1 A real limitation: no offset support
  6. Summary and Conclusions

  7. Resources


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¶

In [1]:
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.

In [2]:
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
Out[2]:
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.

In [3]:
# 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.

In [4]:
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.

In [5]:
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.

In [6]:
# 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¶

  1. Fit MGWR on the Northeast data: PhysInact and Acc_Exer converge 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.
  2. Fit MixedGWR, fixing those two large-bandwidth covariates globally while the rest vary locally.
  3. Confirmed GWGLM(family="gaussian") reproduces GWR's coefficients exactly.
  4. 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}$) — GWGLM cannot currently model exposure-adjusted rates, and the correct workaround is modelling the rate itself with family="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's fixed_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