Choosing Adaptive vs Fixed Bandwidth for GWR

A fixed-distance bandwidth applies the same kernel radius to every unit, which starves data-sparse rural counties of neighbors while over-smoothing dense urban ones; an adaptive nearest-neighbor bandwidth adjusts the kernel support to local density instead. This guide, part of Geographically Weighted Regression for Disease-Rate Modeling, shows how to choose between the two by measuring density heterogeneity, comparing AICc, and checking what each does to local sample sizes and coefficient stability.

Problem Context & Constraints

Bandwidth is the parameter that makes or breaks a GWR fit, and it comes in two flavors that behave very differently over a population surface with uneven density. A fixed bandwidth is a distance — say 40 km — and every local regression pulls in whatever units fall inside that radius. An adaptive bandwidth is a count — say the 30 nearest units — and the radius silently expands or contracts so that each local fit always sees the same number of neighbors.

Epidemiological study areas are almost never uniform in density. A state contains a handful of dense metropolitan counties packed into a small footprint and a broad scattering of large, sparse rural counties. A fixed 40 km radius is two contradictory things on that surface at once: in the metro it encloses dozens of small counties and over-smooths the coefficient surface into near-global mush; in the frontier it may enclose only one or two counties, so the local regression is fit on almost no data and its coefficient is dominated by noise — sometimes it cannot be estimated at all because the local sample is smaller than the number of covariates. The adaptive bandwidth sidesteps this by holding the neighbor count constant: its radius shrinks in the metro and stretches across the frontier, keeping every local fit on comparable statistical footing.

Three constraints frame the choice:

  • Density heterogeneity is the deciding variable. The more the number of units per unit area varies across the study region, the more a fixed bandwidth mistreats one end of that range.
  • A degenerate local fit is worse than a smooth one. A local regression with fewer observations than parameters is singular; the pipeline must detect that, not silently emit a garbage coefficient.
  • The chosen criterion must be auditable. Whether the bandwidth is fixed or adaptive, and the value it resolved to, has to be logged so the surface can be reproduced.

The mechanics of the two kernels over the same uneven density are the whole argument in one picture:

Fixed-Distance vs Adaptive Nearest-Neighbor Kernels Two panels over the same uneven point density, a sparse cluster of units at the top and a dense cluster at the bottom. The left panel, fixed-distance bandwidth, draws the same-radius kernel around a unit in each panel: in the sparse region the fixed radius encloses only a few units, in the dense region the same radius encloses many. The right panel, adaptive nearest-neighbor bandwidth, draws a large kernel in the sparse region and a small kernel in the dense region so that each encloses the same fixed neighbor count k. An arrow between the panels marks that the adaptive radius adjusts to local density. Fixed-distance bandwidth same radius r everywhere Adaptive kNN bandwidth radius adjusts to hold k neighbors sparse: catches 3 dense: catches 8 — over-smoothed sparse: wide radius, k=6 dense: tight radius, k=6 Adaptive support keeps every local fit on the same neighbor count k despite uneven density

Prerequisites

Pin the stack so the two bandwidth searches are reproducible:

  • python 3.11
  • geopandas 0.14.4, libpysal 4.12.1
  • mgwr 2.2.1 — provides both fixed=True and fixed=False search modes
  • numpy 1.26.4

Input state assumed here:

Step-by-Step Solution

First, quantify how uneven the density actually is — this is the number that tips the decision. A simple, defensible measure is the coefficient of variation of nearest-neighbor distances: near zero means near-uniform density (a fixed bandwidth is fine), while a large value means a fixed radius will mistreat one end of the range.

# python 3.11
# geopandas==0.14.4  libpysal==4.12.1  numpy==1.26.4
import logging
import numpy as np
from scipy.spatial import cKDTree

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("gwr.bandwidth")

def density_heterogeneity(coords):
    """CV of 1st-nearest-neighbour distance. ~0 uniform; >0.6 strongly uneven."""
    tree = cKDTree(coords)
    d, _ = tree.query(coords, k=2)     # k=1 is the point itself
    nn = d[:, 1]
    cv = float(nn.std() / nn.mean())
    log.info("nn-distance CV=%.3f (uniform~0, uneven>0.6)", cv)
    return cv

Next, run Sel_BW both ways on identical inputs and compare AICc. The fixed search returns a distance in CRS units; the adaptive search returns a neighbor count.

# mgwr==2.2.1
import numpy as np
from mgwr.sel_bw import Sel_BW
from mgwr.gwr import GWR

def compare_bandwidths(coords, y, X, kernel="bisquare"):
    """Search fixed and adaptive bandwidths; return both with their AICc."""
    bw_fixed = Sel_BW(coords, y, X, kernel=kernel, fixed=True).search(criterion="AICc")
    bw_adapt = Sel_BW(coords, y, X, kernel=kernel, fixed=False).search(criterion="AICc")

    aicc_fixed = GWR(coords, y, X, bw=bw_fixed, kernel=kernel, fixed=True).fit().aicc
    aicc_adapt = GWR(coords, y, X, bw=bw_adapt, kernel=kernel, fixed=False).fit().aicc

    log.info("fixed:  bw=%.0f (map units)  AICc=%.2f", bw_fixed, aicc_fixed)
    log.info("adapt:  bw=%d (neighbours)   AICc=%.2f", int(bw_adapt), aicc_adapt)
    chosen = "adaptive" if aicc_adapt < aicc_fixed else "fixed"
    log.info("lower AICc => choose %s bandwidth", chosen)
    return {"bw_fixed": float(bw_fixed), "aicc_fixed": float(aicc_fixed),
            "bw_adaptive": int(bw_adapt), "aicc_adaptive": float(aicc_adapt),
            "chosen": chosen}

AICc is the right adjudicator because it penalizes the effective number of parameters, so it will not reward a fixed bandwidth that fits the dense core beautifully at the cost of degenerate rural fits. In practice the decision rule is a two-part check: if density_heterogeneity is high, favor adaptive on principle; then confirm with AICc. When the CV is near zero and the two AICc values are within a point or two, a fixed bandwidth is the simpler, equally defensible choice.

Finally, make the consequence concrete by counting how many units each fixed radius actually reaches, because an average conceals the starved tail:

# scipy==1.13.1
from scipy.spatial import cKDTree
import numpy as np

def local_sample_sizes(coords, radius):
    """How many units fall inside a fixed radius around each unit."""
    tree = cKDTree(coords)
    counts = np.array([len(tree.query_ball_point(c, r=radius)) - 1 for c in coords])
    log.info("fixed radius=%.0f: neighbours min=%d median=%d max=%d",
             radius, counts.min(), int(np.median(counts)), counts.max())
    return counts

A min near zero is the alarm: those units have essentially no local data under the fixed radius, and their coefficients are noise. The adaptive kernel, by construction, gives every unit the same neighbor count, so this failure mode cannot occur — which is exactly why adaptive is the safer default whenever density is uneven.

Validation & Edge Cases

1. The bandwidth search does not converge. Sel_BW uses a golden-section search over the bandwidth; a flat or multimodal AICc curve can leave it at a boundary value. Detect a fixed bandwidth that lands at the maximum inter-point distance (it has collapsed to global OLS) or an adaptive count equal to nn:

n = len(coords)
if int(res_bw_adapt) >= n - 1:
    log.warning('{"event":"bw_degenerate","mode":"adaptive","bw":%d,"n":%d}'
                % (int(res_bw_adapt), n))

A degenerate maximum bandwidth is not a bug to paper over — it is GWR telling you the data show no non-stationarity at all, and a global model is the honest report.

2. A degenerate (near-singular) local kernel. If an adaptive neighbor count is barely above the covariate count, the local design matrix is nearly singular and coefficients explode. Guard the minimum neighbor count against the number of covariates:

k_cov = X.shape[1]
min_neighbours = max(int(res_bw_adapt), 0)
if min_neighbours < 2 * k_cov:
    log.warning('{"event":"thin_local_fit","bw":%d,"covariates":%d}'
                % (min_neighbours, k_cov))
WARNING {"event":"thin_local_fit","bw":9,"covariates":5}

Raise the floor on the adaptive count (or drop a covariate) so each local fit keeps a comfortable ratio of observations to parameters.

3. Coefficient instability across the density gradient. Even when a fixed bandwidth “works,” its local coefficients are far noisier in the sparse tail than the adaptive equivalent. Compare the variance of a coefficient surface between the two fits; a fixed bandwidth whose sparse-region coefficients swing wildly relative to adaptive is over-fitting the frontier. This residual instability is also what you would catch by testing the surface for leftover spatial structure with Global & Local Moran’s I Implementation — patchy, high-variance coefficients often leave autocorrelated residuals behind.

Compliance Notes

Whichever bandwidth wins, the audit record must state the criterion as well as the value: the fixed flag (distance vs neighbor count), the resolved bandwidth, the selection criterion (AICc), the kernel, and the measured density-heterogeneity CV that justified the choice. Logging only the number is not enough — a bandwidth of “30” is a distance in one mode and a neighbor count in the other, and a reviewer cannot reproduce the surface without knowing which. Bind that record to the output layer with the same SHA-256 configuration hash used for the coefficient surface, so the bandwidth decision is reconstructable alongside the result it produced.