skills/geopandas/references/geometric-operations.md
GeoPandas delegates geometry work to Shapely/GEOS. Operations are planar, two-dimensional, and expressed in CRS coordinate units. Z/M ordinates may be carried but are not part of topology.
Count missing, empty, invalid, and mixed geometries separately before any constructive or set operation:
geometry = gdf.geometry
missing = geometry.isna()
empty = geometry.is_empty
invalid = (~missing) & (~empty) & (~geometry.is_valid)
state = {
"rows": len(gdf),
"missing": int(missing.sum()),
"empty": int(empty.sum()),
"invalid": int(invalid.sum()),
"types": geometry.geom_type.value_counts(dropna=False).to_dict(),
}
is_valid concerns polygon/ring topology; points and lines are generally valid
unless malformed. is_simple, is_ring, and minimum_clearance answer
different questions.
is_valid_reason() can include the coordinate of a defect, for example a
self-intersection. Precise coordinates can be sensitive. Aggregate only the
reason category before [ and retain detailed diagnostics in a protected local
artifact:
reason_category = (
geometry[invalid]
.is_valid_reason()
.str.split("[", n=1)
.str[0]
.value_counts()
)
GeoPandas 1.1 exposes Shapely 2.1 repair controls:
repaired = geometry.make_valid(
method="structure",
keep_collapsed=True,
)
Methods:
linework preserves every edge/vertex, nodes all rings, and reconstructs
areas with even/odd parity. It can produce complex GeometryCollection
output and requires keep_collapsed=True.structure repairs rings, merges shells, and subtracts holes. It assumes
shell/hole categorization is meaningful, requires GEOS >=3.10, and can drop
collapsed parts when keep_collapsed=False.Repair can turn a polygon into a multipolygon, line, point, collection, or empty geometry. Never replace source data in place. Create a new artifact and compare:
overlay(make_valid=True) also repairs invalid inputs, but that convenience can
hide type changes. Audit and repair explicitly for traceable work.
set_precision(grid_size, mode=...) rounds x/y to a grid in CRS units:
snapped = geometry.set_precision(
grid_size=0.01,
mode="valid_output",
)
Shapely 2.1 modes:
valid_output removes collapsed polygonal/linear elements and duplicate
vertices while producing valid output;pointwise rounds independently, retains duplicate vertices, and may produce
invalid output;keep_collapsed preserves collapsed linear elements but removes collapsed
polygonal elements.Consequences:
Choose the grid from documented source resolution and error—not decimal
aesthetics. 0.001 degrees is not a universal metric tolerance.
For one union/dissolve, grid_size= can apply fixed precision without first
attaching a precision model:
merged = geometry.union_all(method="unary", grid_size=0.01)
Record whether precision was attached to inputs or applied only to an operation.
union_all algorithmsGeoPandas 1.1.4 signature:
geometry.union_all(method="unary", grid_size=None)
unary: robust general-purpose algorithm; the only method supporting
grid_size.coverage: optimized for non-overlapping edge-matched polygon coverages; it
can return invalid geometry if polygons overlap.disjoint_subset: optimized when input can be divided into non-intersecting
subsets; requires Shapely >=2.1 and may be slower when there is one subset.Do not use coverage based on visual inspection:
if not geometry.is_valid_coverage(gap_width=0.0):
edges = geometry.invalid_coverage_edges(gap_width=0.0)
raise ValueError("Not an edge-matched non-overlapping coverage")
merged = geometry.union_all(method="coverage")
is_valid_coverage() ignores non-polygon geometry and requires Shapely >=2.1.
If narrow gaps matter, select gap_width in justified projected units.
simplify_coverage() preserves shared boundaries for a valid coverage; ordinary
element-wise simplify() does not.
The old unary_union attribute is deprecated. Use union_all().
All distance/tolerance arguments are CRS units:
buffered = geometry.buffer(50)
simplified = geometry.simplify(5, preserve_topology=True)
densified = geometry.segmentize(max_segment_length=10)
centroids = geometry.centroid
inside_points = geometry.representative_point()
Correctness notes:
centroid may fall outside a concave polygon; representative_point() is
guaranteed within the geometry but is not a centroid.preserve_topology=True protects each geometry's validity, not shared
boundaries between adjacent features.segmentize() inserts vertices along planar segments; it does not create
geodesic densification.Predicates implement DE-9IM relationships and are directional:
| Predicate | Practical meaning |
|---|---|
intersects | Boundaries or interiors share any point |
disjoint | Share no point |
within | Left geometry lies in right interior/boundary under DE-9IM |
contains | Inverse direction of within; boundary-only point is not contained |
covers | No point of right lies outside left; includes boundary cases |
covered_by | Inverse of covers |
contains_properly | Contains with no common boundary points |
touches | Interiors do not meet, boundaries do |
crosses | Interiors meet with lower-dimensional result |
overlaps | Same-dimensional partial overlap, neither contains the other |
dwithin | Planar distance is within the supplied CRS-unit threshold |
Do not describe contains, covers, and intersects as interchangeable.
Boundary-point tests are an important synthetic fixture.
Binary GeoSeries calls are one-to-one and index-aligned by default:
matched = left.intersects(right, align=True)
They do not answer whether each left geometry intersects any right geometry.
Use sjoin or the spatial index for all-pairs matching.
Overlay and intersection can create tiny slivers from precision mismatch, near-coincident edges, or distinct source accuracy:
keep_geom_type;Do not delete polygons below an arbitrary area threshold. A small polygon can be legitimate, and thresholding can bias boundaries. Record any sliver rule and retain pre-cleaning output.
geom_equals is topological equality; coordinate order may differ.geom_equals_exact(tolerance=...) checks structural coordinate equality
within tolerance.geom_equals_identical exposes Shapely's identical comparison in GeoPandas
1.1 and includes coordinate/order details.normalize() can canonicalize ordering for reproducible comparisons, but a
normalized WKB hash is still geometry identity, not stable feature identity.For every geometry-changing operation, record:
The bundled scripts/geometry_validity_report.py provides bounded dry-run
counts and optional new-file repair without emitting geometries or coordinates.