Spatial Clustering & Regionalization
Regionalization is the art of drawing regions that are both internally homogeneous and geographically contiguous. Statistical agencies use it to build reporting zones with a minimum population; ecologists use it to delineate habitat regions; utilities use it to carve service territories from customer data. The defining constraint is contiguity: unlike ordinary clustering, which can scatter members of a group across the map, regionalization requires every region to be a single connected block of adjacent units. That constraint is enforced through the spatial weight matrix, which tells the algorithm which units are allowed to merge. This page develops the objective functions, the leading methods (Max-P-Regions, SKATER, and Ward with connectivity), and a runnable spopt implementation, all within the broader context of Core Concepts of Spatial Statistics & Geostatistics.
Prerequisites
- Python 3.9+
-
numpy>=1.22,geopandas>=0.13,libpysal>=4.8,spopt>=0.5,scikit-learn>=1.2 - A
GeoDataFrameof areal units in a projected (metric) CRS with a reset integer index - One or more numeric clustering variables, standardised to zero mean and unit variance
- A contiguity weight matrix with a single connected component — islands cannot join a region
Mathematical Core
Regionalization partitions areal units into regions that minimise within-region heterogeneity subject to a contiguity constraint. Let be the standardised attribute vector for unit . A general objective is the total within-region sum of squared deviations from each region’s centroid:
subject to the constraint that every region induces a connected subgraph of the contiguity graph defined by the weight matrix . It is this constraint that separates regionalization from ordinary k-means, which optimises the same objective with no spatial term.
SKATER: Minimum Spanning Tree Partitioning
SKATER (Spatial 'K’luster Analysis by Tree Edge Removal) first builds a minimum spanning tree (MST) over the contiguity graph, where each edge carries a cost equal to the attribute dissimilarity between adjacent units:
The MST reduces the full connectivity graph to edges while preserving connectivity. SKATER then removes edges to split the tree into connected subtrees, choosing at each step the edge whose removal yields the greatest reduction in within-region variance. Because every subtree is by construction a connected component of the original graph, each region is guaranteed contiguous.
Max-P-Regions: Maximising Regions Under a Floor
Max-P-Regions inverts the question. Rather than fixing , it fixes a minimum-size threshold and finds the maximum number of regions such that each region satisfies a floor constraint on a spatially extensive variable (population, area, count):
where is the threshold variable for unit . Among all partitions satisfying the floor, Max-P selects the one that also minimises within-region attribute heterogeneity. This is the right formulation when the design requirement is “every region must contain at least 50,000 people” rather than “give me exactly eight regions”. Max-P is NP-hard and is solved with a heuristic (construction followed by local search).
Ward with Connectivity
Agglomerative Ward clustering can be made spatial by supplying a connectivity matrix derived from . At each merge step Ward joins the two adjacent clusters whose union produces the smallest increase in within-cluster variance, and the connectivity constraint restricts merges to spatially adjacent clusters. It produces a full hierarchy (a dendrogram) rather than a single partition, which is useful when you want to explore regionalizations at multiple scales.
| Method | You specify | Contiguity | Best when |
|---|---|---|---|
| SKATER | Number of regions | MST subtrees | You know how many regions you need |
| Max-P-Regions | Minimum region size (floor) | Graph merge | You have a size/population requirement |
| Ward + connectivity | Cut level of hierarchy | Adjacency-restricted merges | You want a multi-scale hierarchy |
| k-means (no constraint) | Number of clusters | None | Geography is irrelevant to the grouping |
Annotated Implementation
The spopt.region module implements SKATER and Max-P-Regions against a libpysal weights object. The pattern is: standardise attributes, build contiguity weights, fit the solver, extract labels.
1. Attribute Standardisation and Contiguity Weights
import geopandas as gpd
import numpy as np
import libpysal
from sklearn.preprocessing import StandardScaler
# Load areal units in a projected CRS and reset the index
gdf = gpd.read_file("counties.gpkg").to_crs("EPSG:5070")
gdf = gdf.reset_index(drop=True)
# Clustering variables — standardise so no feature dominates the metric
attrs = ["median_income", "pct_college", "unemployment"]
gdf[attrs] = StandardScaler().fit_transform(gdf[attrs].values)
# Queen contiguity defines which units may merge into a shared region
w = libpysal.weights.Queen.from_dataframe(gdf)
assert w.n_components == 1, "Weight matrix must be a single connected component"
The n_components == 1 assertion is critical: regionalization cannot bridge disconnected components, so islands must be resolved (via KNN or manual bridging) before fitting.
2. SKATER for a Fixed Number of Regions
from spopt.region import Skater
skater = Skater(
gdf,
w,
attrs_name=attrs,
n_clusters=6, # target number of contiguous regions
floor=5, # minimum units per region
trace=False,
)
skater.solve()
gdf["skater_region"] = skater.labels_
print(gdf["skater_region"].value_counts())
Skater takes the GeoDataFrame, the weights, the attribute names, and n_clusters. After solve(), labels_ holds the region index for each unit.
3. Max-P-Regions Under a Size Floor
from spopt.region import MaxPHeuristic
# A spatially extensive threshold variable — e.g. population per unit
gdf["pop"] = gdf["population"].astype(float)
maxp = MaxPHeuristic(
gdf,
w,
attrs_name=attrs, # homogeneity variables
threshold_name="pop", # the floor is applied to this column
threshold=50000, # every region must contain >= 50,000 people
top_n=2,
)
maxp.solve()
gdf["maxp_region"] = maxp.labels_
print(f"Max-P found {maxp.p} regions")
MaxPHeuristic discovers the number of regions rather than taking it as input. maxp.p reports how many regions satisfied the floor.
Output Interpretation
Evaluate a regionalization on two axes: contiguity (are regions connected?) and homogeneity (are units within a region similar?).
def within_region_variance(gdf, label_col, attrs):
"""Mean within-region variance across clustering variables (lower = tighter)."""
total = 0.0
for _, grp in gdf.groupby(label_col):
total += grp[attrs].var(ddof=0).sum() * len(grp)
return total / len(gdf)
wrv = within_region_variance(gdf, "skater_region", attrs)
print(f"Mean within-region variance: {wrv:.3f}")
Good signs: each region label maps to a solid connected block, and within-region variance is well below the global variance of the standardised attributes (which is , the number of variables, since each has unit variance). Warning signs: a region consisting of a single unit (the floor was too low, or an outlier could not merge), or within-region variance close to the global value, which means the contiguity constraint has overwhelmed attribute similarity — often a sign that the attributes carry little spatial structure to begin with. Whether that structure exists is exactly what the spatial autocorrelation metrics quantify: variables with near-zero Moran’s I have no coherent regions to find.
Production Considerations
Complexity. Max-P-Regions is NP-hard; the heuristic’s runtime grows with the number of units and the tightness of the floor. For in the tens of thousands, expect minutes and set top_n modestly. SKATER is cheaper because the MST construction is near-linear in the number of edges and edge removal is .
Determinism. Both heuristics involve randomised initialisation. Pass a fixed random seed where the API exposes one, and record it, so a reported partition can be reproduced.
Islands. Resolve disconnected components before fitting. A single island forces the solver either to fail or to emit a singleton region; neither is usually acceptable. Repair geometry, switch to KNN, or bridge the island manually — the same remedies covered in the spatial weight matrices guide.
Scaling variables. Never skip standardisation. Because the objective is a variance across features, an unstandardised high-magnitude variable will silently dominate every region boundary.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Solver raises a connectivity error | Weight matrix has more than one component | assert w.n_components == 1; resolve islands before fitting |
| Regions form along one variable only | Attributes not standardised | Apply StandardScaler to the clustering columns |
| Max-P returns very few regions | Floor set too high for the threshold variable | Lower threshold or check units of threshold_name |
| A region contains a single unit | Floor too low, or an unmergeable outlier | Raise the floor; inspect the outlier’s attributes |
| SKATER ignores my target count | n_clusters incompatible with floor |
Ensure n_clusters * floor <= n |
| Runtime is very slow | Large with a tight Max-P floor | Reduce top_n; pre-aggregate units; prefer SKATER |
| Non-contiguous region in output | Island slipped through, or wrong weights | Rebuild contiguity weights; verify w.n_components |
Next Steps
For a focused, copy-pasteable walkthrough of the Max-P heuristic — passing a libpysal weights object, setting the threshold variable and floor, extracting labels, and mapping the regions — follow Spatially Constrained Clustering with Max-P Regions. Because every method here depends on the connectivity graph, revisit spatial weight matrices to get the contiguity structure right, and use the spatial autocorrelation metrics to confirm your variables have the spatial structure worth regionalizing in the first place.
Frequently Asked Questions
What is the difference between clustering and regionalization?
Attribute-space clustering such as k-means groups observations by feature similarity alone and ignores geography, so a resulting cluster can be scattered across the map. Regionalization adds a contiguity constraint through a spatial weight matrix, requiring every region to form a single connected block of adjacent units. Regionalization trades some attribute homogeneity for spatial contiguity, which is what makes the output usable as administrative or planning regions.
When should I use Max-P-Regions instead of SKATER?
Use SKATER when you know how many regions you want; it partitions a minimum spanning tree into a fixed number of contiguous groups. Use Max-P-Regions when you instead have a minimum-size requirement, such as a minimum population per region, and want the algorithm to find the maximum number of regions that all satisfy that floor while remaining internally homogeneous.
Why must clustering variables be standardised?
Regionalization objectives are built on a distance or variance metric across the clustering variables. If one variable is measured in the thousands and another in fractions, the large-magnitude variable dominates the metric and the regions form almost entirely along that single dimension. Standardising each variable to zero mean and unit variance gives every feature comparable influence on the partition.
Does regionalization guarantee contiguous regions?
Yes, provided the spatial weight matrix is a single connected component. SKATER and Max-P-Regions operate on the connectivity graph and can only merge units that are adjacent, so every output region is contiguous. If the weight matrix contains islands or disconnected components, those units cannot join a region and the solver will fail or isolate them.
Related
- Spatially Constrained Clustering with Max-P Regions — the step-by-step spopt MaxPHeuristic workflow
- Spatial Weight Matrices — the contiguity graph that enforces the region constraint
- Spatial Autocorrelation Metrics — confirming variables carry the spatial structure worth regionalizing
- Hot Spot Analysis with Getis-Ord Statistics — locating intense clusters as a complement to partitioning
← Back to Core Concepts of Spatial Statistics & Geostatistics