Handling Island Polygons in Spatial Weights
A single polygon with no contiguity neighbors — an offshore county, a district the boundary file failed to snap — will crash row-standardization and quietly bias every autocorrelation statistic on the surface. This guide, part of Spatial Weights Matrix Construction for Public Health Surveillance, shows how to detect these zero-neighbor units from a libpysal weights object and reconnect them deterministically without corrupting the rest of the graph.
Problem Context & Constraints
Contiguity weights derive neighbors from shared boundaries, so any unit that touches nothing gets an empty neighbor list. libpysal calls these islands, and they are more than an edge case — they are a fault that propagates in two distinct ways.
First, they break row-standardization arithmetically. Row-standardization rescales each row of the weights matrix to sum to one, dividing every weight in row by that row’s total, . For an island the row sum is zero, so the operation is a division by zero. libpysal will warn and emit nan weights rather than raise, which is worse than a crash: the nan flows silently into the spatial lag and can poison a whole vectorized computation.
Second, and more insidiously, an island’s local statistic is undefined. A Local Moran’s I or Getis-Ord Gi* value for a unit is a function of its neighbors’ values; with no neighbors there is nothing to compare against, so the unit contributes a meaningless or missing local score. If it is silently dropped, the global statistic is computed on a different, smaller population than the map implies, and the normalizing constant no longer matches the units actually being tested. Either way the autocorrelation estimate is biased by an amount nobody logged.
The literal offshore island is only the obvious case. Zero-neighbor units also appear from data defects: a polygon whose boundary was digitized just short of its neighbor’s, a unit in a slightly different projection so its coordinates land elsewhere, a sliver dropped during a dissolve. These are not real islands and must be distinguished from genuine ones, because the fix differs — a data defect should be repaired upstream, whereas a true island is reconnected by an explicit, logged modeling decision. The naive response, deleting islands so the code stops warning, is the one option that is never acceptable in surveillance: dropping a unit removes a population from the analysis without a record, which is exactly the silent bias the pipeline exists to prevent.
Prerequisites
Pin the stack so island detection and repair are reproducible across reviewers:
python3.11geopandas0.14.4 (pullsshapely2.0.x,pyproj3.6.x)libpysal4.12.1 —W,w.islands,KNN, andw_unionnumpy1.26.4
Input state assumed below:
- A projected
GeoDataFrame(metres). kNN augmentation measures centroid distance, so an unprojected frame reconnects an island to the wrong “nearest” units — reproject before repair. - A stable, sorted index, because both the contiguity graph and the kNN graph key on it and the union must align them row-for-row.
- An already-built contiguity
W(Queen or Rook) whosew.islandsyou will inspect.
Step-by-Step Solution
The workflow is detect, classify, repair, re-validate. Detection is a one-liner; the discipline is in classifying whether an island is a data defect or a genuine disconnection, then repairing genuine ones with a graph union that leaves the mainland untouched.
# Detect islands, then reconnect them via a contiguity + kNN union.
# Pinned: libpysal==4.12.1, geopandas==0.14.4, numpy==1.26.4
import logging
import json
import numpy as np
import geopandas as gpd
from libpysal.weights import Queen, KNN, w_union
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("weights.islands")
def repair_islands(gdf: gpd.GeoDataFrame, k: int = 2):
"""Reconnect zero-neighbor units by unioning contiguity with a kNN graph.
Returns (w_fixed, audit) where audit records exactly what was reconnected.
"""
gdf = gdf.sort_values("geoid").reset_index(drop=True)
# 1. DETECT — build contiguity and read the island list straight off W.
w_cont = Queen.from_dataframe(gdf, use_index=True)
islands = list(w_cont.islands)
if not islands:
w_cont.transform = "R"
log.info("no islands; contiguity graph used as-is")
return w_cont, {"islands": [], "method": "none"}
log.warning("islands detected: %d units -> %s", len(islands), islands)
# 2. REPAIR — full kNN graph, then union so ONLY island rows gain edges.
# Union keeps every existing contiguity edge and adds kNN edges;
# for non-island units the extra kNN edges are undesirable, so we
# instead union a kNN graph restricted to the island rows.
w_knn = KNN.from_dataframe(gdf, k=k)
island_neighbors = {i: w_knn.neighbors[i] for i in islands}
# rebuild neighbor dict: contiguity for everyone + kNN edges for islands
neighbors = {i: list(w_cont.neighbors[i]) for i in w_cont.id_order}
weights = {i: list(w_cont.weights[i]) for i in w_cont.id_order}
for i, js in island_neighbors.items():
neighbors[i] = list(js)
weights[i] = [1.0] * len(js)
# make the bridge symmetric so the graph stays undirected
for j in js:
if i not in neighbors[j]:
neighbors[j].append(i)
weights[j].append(1.0)
from libpysal.weights import W
w_fixed = W(neighbors, weights, id_order=list(w_cont.id_order),
silence_warnings=True)
# 3. VALIDATE — no islands may remain before standardization.
assert not w_fixed.islands, f"islands survived repair: {w_fixed.islands}"
w_fixed.transform = "R" # now safe: every row sum > 0
audit = {
"method": "contiguity+knn_union",
"k": k,
"reconnected_units": {int(i): [int(j) for j in js]
for i, js in island_neighbors.items()},
"n_reconnected": len(islands),
}
log.info("island_repair_audit=%s", json.dumps(audit))
return w_fixed, audit
The key design choice is surgical augmentation: rather than unioning a full kNN graph onto the contiguity graph (which would add spurious edges to every mainland unit and change their statistics), the code adds kNN edges only to the island rows and mirrors them so the graph stays undirected. The mainland’s contiguity structure is byte-for-byte unchanged, so only the units that were broken are altered — and exactly those alterations are recorded in audit.
Setting is a modeling decision. gives an island a single lifeline whose value wholly determines its local statistic — fragile. or gives a small, stable neighborhood without over-connecting a unit that is genuinely peripheral. Match to the interior units’ typical cardinality so the reconnected island is not conspicuously over- or under-weighted relative to the rest of the surface.
Validation & Edge Cases
1. An island that is really a data defect. Before reconnecting, check whether the “island” simply failed to touch a neighbor it should touch. If its boundary is within a small tolerance of another unit, the fix is a topology repair (snap/make_valid), not a kNN bridge:
# Distinguish a true island from a near-miss digitisation gap.
# Pinned: geopandas==0.14.4, numpy==1.26.4
def diagnose_island(gdf, i, tol_m=50.0):
gi = gdf.geometry.iloc[i]
d = gdf.geometry.distance(gi) # metres (projected CRS)
d.iloc[i] = np.inf
nearest = d.idxmin()
if d.loc[nearest] <= tol_m:
return ("data_defect", nearest, float(d.loc[nearest])) # snap upstream
return ("true_island", nearest, float(d.loc[nearest])) # kNN-bridge it
WARNING island 118 is a data_defect: 6.2 m from unit 117 -> snap, do not bridge
Reconnecting a data defect with kNN hides the real bug and may bridge to the wrong unit; always classify first.
2. Multiple disconnected components, not just point islands. A whole archipelago can be internally connected yet severed from the mainland, so w.islands (which reports only zero-neighbor units) is empty while the graph still splits into pieces. Assert single-component connectivity explicitly:
# Confirm the repaired graph is a single connected component.
# Pinned: libpysal==4.12.1, scipy==1.13.1
from scipy.sparse.csgraph import connected_components
def assert_connected(w_fixed):
n_comp, labels = connected_components(w_fixed.sparse, directed=False)
if n_comp > 1:
sizes = np.bincount(labels)
raise ValueError(f"graph has {n_comp} components, sizes={sizes.tolist()}; "
"bridge the smaller components or analyse them separately")
return n_comp
If components are genuinely separate populations (an offshore territory analyzed on its own terms), the defensible move may be to analyze them separately rather than force a bridge — but that, too, is a logged decision, not a default.
3. Non-deterministic reconnection. kNN ties (two mainland units exactly equidistant from an island) can resolve differently across runs if the input order changes. The sorted index in step one removes the ambiguity; verify it by re-running and asserting the reconnected_units audit is identical. A reconnection that varies between runs is not reproducible and fails review.
Compliance Notes
An island reconnection changes the neighbor graph, and therefore the results, by an analyst’s manual choice — so it carries the heaviest documentation burden in the weights pipeline:
- Log which units were reconnected and how. The
auditdict — reconnected unit IDs, their new neighbors, the method, and — is the record that a specific, non-contiguity edge was introduced deliberately. Persist it beside the weights spec hash from the parent guide. - Classify before repairing. Record whether each island was a true island (bridged) or a data defect (snapped upstream). The two have different provenance and a reviewer must be able to tell which happened.
- Never delete an island silently. If a unit is excluded from analysis, that exclusion is itself a disclosure and analytic decision requiring its own justification — the same discipline that governs Adaptive Areal Aggregation for Rare Disease Counts, where merging or dropping sparse units is a documented step rather than a convenience.
- Assert no islands remain before standardization. The
assert not w_fixed.islandsgate turns a silentnaninto a loud failure, guaranteeing no divide-by-zero row reaches a downstream statistic.
Related Topics
- Spatial Weights Matrix Construction for Public Health Surveillance — the parent guide covering weights families, row-standardization, and connectivity diagnostics.
- Adaptive Areal Aggregation for Rare Disease Counts — the disclosure-control analogue of documenting how sparse or isolated units are merged or handled.