Validating and Repairing Invalid Polygon Geometry
This guide, part of Spatial Data Types & Formats in Production Epidemiology Pipelines, is about the repair itself: what each class of invalid polygon looks like, which repair method suits it, and how to confirm the repaired shape still describes the area the data belong to.
Problem Context & Constraints
Invalid polygons enter health data from ordinary processes. A service area traced by hand crosses its own boundary where two tracing passes met. A format conversion drops the closing vertex of a ring. A simplification step collapses a narrow peninsula into a bow-tie. A hole digitised as a separate polygon ends up outside its shell after a coordinate transformation.
Most spatial libraries will refuse to overlay an invalid polygon, which is the good case. The bad case is an operation that accepts it and returns something plausible and wrong: an area computed as the signed sum of two lobes that partially cancel, a point-in-polygon test that flips at a self-intersection, a union that silently drops a lobe.
The standard repair tools do not all behave the same way. make_valid restructures the geometry to a valid one that covers the same point set, which for a bow-tie means a multipolygon of two triangles — correct, but now a different geometry type. The older buffer(0) trick also produces a valid result but chooses which lobes to keep by winding order, and can delete half a bow-tie outright. Manual repair — re-digitising — is the only method that recovers the intended shape, and is only practical for a few features.
The constraint that ties this together is that a repair must be verified, not assumed: compare the repaired area to the original, and review any feature whose area changed by more than a small tolerance.
Prerequisites
python3.11shapely2.0.4,geopandas0.14.4,pandas2.2.2- The layer in a projected metric coordinate reference system, so area comparisons are meaningful
- A tolerance for acceptable area change, declared in advance
- Access to the original source for features whose automatic repair fails review
Step-by-Step Solution
Classify each invalid feature by its validity reason, apply the repair suited to that class, keep only the geometry type the layer expects, and compare areas before and after.
# Classify, repair and verify invalid polygons.
# Pinned: shapely==2.0.4, geopandas==0.14.4, pandas==2.2.2
import logging
import re
import pandas as pd
import geopandas as gpd
import shapely
from shapely.validation import explain_validity, make_valid
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("geometry.repair")
AREA_TOLERANCE = 0.005 # repairs changing area by more than 0.5% need review
def classify(gdf: gpd.GeoDataFrame, id_col: str = "geoid") -> pd.DataFrame:
bad = gdf[~gdf.geometry.is_valid]
reasons = bad.geometry.map(explain_validity)
cls = reasons.str.extract(r"^([A-Za-z ]+)")[0].str.strip()
out = pd.DataFrame({id_col: bad[id_col], "reason": reasons, "class": cls})
log.info("invalid features by class: %s", out["class"].value_counts().to_dict())
return out
def _areal_only(geom):
"""make_valid may return a collection with lines or points; keep areal parts."""
parts = [g for g in getattr(geom, "geoms", [geom])
if g.geom_type in ("Polygon", "MultiPolygon") and not g.is_empty]
return shapely.union_all(parts) if parts else shapely.Polygon()
def repair(gdf: gpd.GeoDataFrame, id_col: str = "geoid") -> tuple[gpd.GeoDataFrame, pd.DataFrame]:
g = gdf.copy()
before = g.geometry.area
mask = ~g.geometry.is_valid
# Repeated vertices and spikes first: cheap and shape-preserving.
g.loc[mask, "geometry"] = shapely.remove_repeated_points(g.loc[mask, "geometry"].values)
still = ~g.geometry.is_valid
g.loc[still, "geometry"] = g.loc[still, "geometry"].map(make_valid).map(_areal_only)
after = g.geometry.area
change = ((after - before).abs() / before.where(before > 0)).fillna(0.0)
report = pd.DataFrame({id_col: g[id_col], "repaired": mask.values,
"area_before": before, "area_after": after,
"area_change_frac": change})
report = report[report["repaired"]]
review = report[report["area_change_frac"] > AREA_TOLERANCE]
log.info("repaired %d features; %d changed area by more than %.1f%% and need review",
len(report), len(review), 100 * AREA_TOLERANCE)
still_bad = int((~g.geometry.is_valid).sum())
if still_bad:
log.error("%d features remain invalid after repair", still_bad)
return g, report
Removing repeated points before calling make_valid is a small ordering choice with a large effect. Many invalidities are spikes and duplicate vertices introduced by snapping, and removing them fixes the geometry without restructuring it. Calling make_valid first would also produce a valid result, but sometimes by splitting the polygon at the spike, which changes the topology for no reason.
Keeping only the areal parts after make_valid matters because the function is allowed to return a geometry collection containing lines or points where a polygon collapsed to zero width. A layer of tract polygons should contain polygons; a stray line segment in the geometry column will break an overlay later in a confusing way.
Validation & Edge Cases
A repaired geometry of a different type. A polygon that make_valid returns as a multipolygon changes the layer’s geometry type. Most downstream code handles multipolygons, but a format or database column typed as single polygons will reject them. Promote the whole column to multipolygon rather than forcing a single part.
Repairs that pass area review but move boundaries. A spike removal can leave area nearly unchanged while moving a shared boundary, opening a sliver against the neighbouring polygon. After repairing an areal layer that is meant to tile the study area, re-run the tessellation check from Writing Geometry Validity Assertions for a Pipeline.
Repair order that depends on input order. Running make_valid over features in whatever order a
file delivers them is deterministic per feature, but a later dissolve or union across repaired
features is not always order-independent at the vertex level. Sort on a stable identifier before
repairing and before any operation that combines features, so a re-run produces byte-identical
geometry.
Multipart features split by repair. A bow-tie tract repaired into two triangles is still one tract with one identifier and one population. Keep it as a single multipolygon row; exploding it into two rows duplicates the identifier and doubles the denominator in any join that follows.
Topology across the whole layer. Repairing polygons one at a time can leave shared edges no longer shared, because each feature’s repair is computed independently. For a tessellating layer, a coverage-aware cleaning step after repair restores the shared edges and should be the last geometry operation before the layer is published to the pipeline.
Features that remain invalid. Some pathological geometries survive every automatic repair. Those must be re-sourced or re-digitised; excluding them silently drops their cases from every aggregation.
Repairing the published file instead of the source. A repair applied downstream is lost when the source is refreshed. Report persistent invalidity to the producer and repair at ingestion with a recorded rule, so the same repair recurs deterministically on every refresh.
Invalid geometry that is intentional. Very occasionally a layer encodes something as a degenerate shape deliberately — a zero-area placeholder for an area with no mapped extent. Check the documentation before repairing, and exclude such features explicitly rather than turning them into something they were not meant to be.
INFO geometry.repair invalid features by class: {'Self-intersection': 11, 'Ring Self-intersection': 4, 'Too few points in geometry component': 2}
INFO geometry.repair repaired 17 features; 3 changed area by more than 0.5% and need review
The three features flagged for review are exactly the ones worth a human look: most often bow-ties where one lobe is a real part of the area and the other is a digitising error, and only the person reviewing can tell which. Their review outcome — accept, manually correct, or re-source — belongs in the repair record alongside the automatic results.
Compliance Notes
Record for each repaired feature its identifier, the invalidity reason, the repair method, the area before and after, and — for features over tolerance — the review outcome and reviewer role. Record the tolerance and the repair order as configuration.
Where repaired geometry is used to publish counts or rates, state in the product metadata that the source boundaries were repaired, how many features were affected, and by what method. A partner comparing your figures with their own will otherwise see small discrepancies at exactly the repaired features and have no way to explain them.
Related Topics
- Spatial Data Types & Formats in Production Epidemiology Pipelines — the parent guide on formats and geometry types.
- Writing Geometry Validity Assertions for a Pipeline — detecting invalidity automatically on every run.
- Handling Island Polygons in Spatial Weights — where a bow-tie’s false island does its damage.