Skill Tracer
Skills › Scientific Agent Skills › geopandas › geometric-operations.md

geometric-operations.md

267 lines · skills/geopandas/references/geometric-operations.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Geometric operations, validity, and precision
2
3GeoPandas delegates geometry work to Shapely/GEOS. Operations are planar,
4two-dimensional, and expressed in CRS coordinate units. Z/M ordinates may be
5carried but are not part of topology.
6

Geometric operations, validity, and precision

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.

Raw lines 1 to 6
7## Preflight state
8
9Count missing, empty, invalid, and mixed geometries separately before any
10constructive or set operation:
12```python
13geometry = gdf.geometry
14missing = geometry.isna()
15empty = geometry.is_empty
16invalid = (~missing) & (~empty) & (~geometry.is_valid)
18state = {
19 "rows": len(gdf),
20 "missing": int(missing.sum()),
21 "empty": int(empty.sum()),
22 "invalid": int(invalid.sum()),
23 "types": geometry.geom_type.value_counts(dropna=False).to_dict(),
25```
27`is_valid` concerns polygon/ring topology; points and lines are generally valid
28unless malformed. `is_simple`, `is_ring`, and `minimum_clearance` answer
29different questions.

Preflight state

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.

Raw lines 7 to 30
31### Redacted validity diagnostics
33`is_valid_reason()` can include the coordinate of a defect, for example a
34self-intersection. Precise coordinates can be sensitive. Aggregate only the
35reason category before `[` and retain detailed diagnostics in a protected local
36artifact:
38```python
39reason_category = (
40 geometry[invalid]
41 .is_valid_reason()
42 .str.split("[", n=1)
43 .str[0]
44 .value_counts()
46```

Redacted validity diagnostics

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()
)
Raw lines 31 to 47
48## Repair is a model change
50GeoPandas 1.1 exposes Shapely 2.1 repair controls:
52```python
53repaired = geometry.make_valid(
54 method="structure",
55 keep_collapsed=True,
57```
59Methods:
61- `linework` preserves every edge/vertex, nodes all rings, and reconstructs
62 areas with even/odd parity. It can produce complex `GeometryCollection`
63 output and requires `keep_collapsed=True`.
64- `structure` repairs rings, merges shells, and subtracts holes. It assumes
65 shell/hole categorization is meaningful, requires GEOS >=3.10, and can drop
66 collapsed parts when `keep_collapsed=False`.
68Repair can turn a polygon into a multipolygon, line, point, collection, or empty
69geometry. Never replace source data in place. Create a new artifact and compare:
711. valid/invalid/null/empty counts;
722. geometry-type and dimensionality transitions;
733. component counts and collapsed outputs;
744. area/length changes in appropriate units;
755. stable feature IDs and row count;
766. downstream predicate/coverage behavior.
78`overlay(make_valid=True)` also repairs invalid inputs, but that convenience can
79hide type changes. Audit and repair explicitly for traceable work.

Repair is a model change

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:

  1. valid/invalid/null/empty counts;
  2. geometry-type and dimensionality transitions;
  3. component counts and collapsed outputs;
  4. area/length changes in appropriate units;
  5. stable feature IDs and row count;
  6. downstream predicate/coverage behavior.

overlay(make_valid=True) also repairs invalid inputs, but that convenience can hide type changes. Audit and repair explicitly for traceable work.

Raw lines 48 to 80
81## Precision models
83`set_precision(grid_size, mode=...)` rounds x/y to a grid in **CRS units**:
85```python
86snapped = geometry.set_precision(
87 grid_size=0.01,
88 mode="valid_output",
90```
92Shapely 2.1 modes:
94- `valid_output` removes collapsed polygonal/linear elements and duplicate
95 vertices while producing valid output;
96- `pointwise` rounds independently, retains duplicate vertices, and may produce
97 invalid output;
98- `keep_collapsed` preserves collapsed linear elements but removes collapsed
99 polygonal elements.
101Consequences:
103- features narrower/shorter than the grid may become empty;
104- spikes and narrow sections can disappear or split polygons;
105- duplicate vertices are normally removed;
106- Z is not rounded;
107- vertex/ring/order is canonicalized and must not be used as identity;
108- inputs should be valid first;
109- later operations use the higher precision (smaller grid size) of inputs.
111Choose the grid from documented source resolution and error—not decimal
112aesthetics. `0.001` degrees is not a universal metric tolerance.
114For one union/dissolve, `grid_size=` can apply fixed precision without first
115attaching a precision model:
117```python
118merged = geometry.union_all(method="unary", grid_size=0.01)
119```
121GeoPandas 1.2 also accepts `grid_size=` on row-wise `intersection`, `union`,
122`difference`, and `symmetric_difference`. This does not add a `grid_size`
123argument to `overlay`; snap valid inputs explicitly for that workflow.
124`buffer(resolution=...)` is deprecated in 1.2; use `quad_segs=`.
126Record whether precision was attached to inputs or applied only to an operation.

Precision models

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:

  • features narrower/shorter than the grid may become empty;
  • spikes and narrow sections can disappear or split polygons;
  • duplicate vertices are normally removed;
  • Z is not rounded;
  • vertex/ring/order is canonicalized and must not be used as identity;
  • inputs should be valid first;
  • later operations use the higher precision (smaller grid size) of inputs.

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)

GeoPandas 1.2 also accepts grid_size= on row-wise intersection, union, difference, and symmetric_difference. This does not add a grid_size argument to overlay; snap valid inputs explicitly for that workflow. buffer(resolution=...) is deprecated in 1.2; use quad_segs=.

Record whether precision was attached to inputs or applied only to an operation.

Raw lines 81 to 127
128## `union_all` algorithms
130GeoPandas 1.2.0 signature:
132```python
133geometry.union_all(method="unary", grid_size=None)
134```
136- `unary`: robust general-purpose algorithm; the only method supporting
137 `grid_size`.
138- `coverage`: optimized for non-overlapping edge-matched polygon coverages; it
139 can return invalid geometry if polygons overlap.
140- `disjoint_subset`: optimized when input can be divided into non-intersecting
141 subsets; requires Shapely >=2.1 and may be slower when there is one subset.
143Do not use `coverage` based on visual inspection:
145```python
146if not geometry.is_valid_coverage(gap_width=0.0):
147 edges = geometry.invalid_coverage_edges(gap_width=0.0)
148 raise ValueError("Not an edge-matched non-overlapping coverage")
150merged = geometry.union_all(method="coverage")
151```
153`is_valid_coverage()` ignores non-polygon geometry and requires Shapely >=2.1.
154If narrow gaps matter, select `gap_width` in justified projected units.
155`simplify_coverage()` preserves shared boundaries for a valid coverage; ordinary
156element-wise `simplify()` does not.
158The old `unary_union` attribute is deprecated. Use `union_all()`.

union_all algorithms

GeoPandas 1.2.0 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().

Raw lines 128 to 159
160## Constructive operations
162All distance/tolerance arguments are CRS units:
164```python
165buffered = geometry.buffer(50, quad_segs=16)
166simplified = geometry.simplify(5, preserve_topology=True)
167densified = geometry.segmentize(max_segment_length=10)
168centroids = geometry.centroid
169inside_points = geometry.representative_point()
170```
172Correctness notes:
174- Buffering geographic degrees does not create a fixed-metre buffer.
175- Negative polygon buffers can collapse to empty.
176- `centroid` may fall outside a concave polygon; `representative_point()` is
177 guaranteed within the geometry but is not a centroid.
178- `preserve_topology=True` protects each geometry's validity, not shared
179 boundaries between adjacent features.
180- `segmentize()` inserts vertices along planar segments; it does not create
181 geodesic densification.
182- Affine rotate/scale/translate/skew operations are coordinate-space transforms,
183 not CRS transformations.

Constructive operations

All distance/tolerance arguments are CRS units:

buffered = geometry.buffer(50, quad_segs=16)
simplified = geometry.simplify(5, preserve_topology=True)
densified = geometry.segmentize(max_segment_length=10)
centroids = geometry.centroid
inside_points = geometry.representative_point()

Correctness notes:

  • Buffering geographic degrees does not create a fixed-metre buffer.
  • Negative polygon buffers can collapse to empty.
  • 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.
  • Affine rotate/scale/translate/skew operations are coordinate-space transforms, not CRS transformations.
Raw lines 160 to 184
185## Binary predicates
187Predicates implement DE-9IM relationships and are directional:
189| Predicate | Practical meaning |
190|---|---|
191| `intersects` | Boundaries or interiors share any point |
192| `disjoint` | Share no point |
193| `within` | Left geometry lies in right interior/boundary under DE-9IM |
194| `contains` | Inverse direction of `within`; boundary-only point is not contained |
195| `covers` | No point of right lies outside left; includes boundary cases |
196| `covered_by` | Inverse of `covers` |
197| `contains_properly` | Contains with no common boundary points |
198| `touches` | Interiors do not meet, boundaries do |
199| `crosses` | Interiors meet with lower-dimensional result |
200| `overlaps` | Same-dimensional partial overlap, neither contains the other |
201| `dwithin` | Planar distance is within the supplied CRS-unit threshold |
203Do not describe `contains`, `covers`, and `intersects` as interchangeable.
204Boundary-point tests are an important synthetic fixture.
206Binary GeoSeries calls are one-to-one and index-aligned by default:
208```python
209matched = left.intersects(right, align=True)
210```
212They do not answer whether each left geometry intersects *any* right geometry.
213Use `sjoin` or the spatial index for all-pairs matching.

Binary predicates

Predicates implement DE-9IM relationships and are directional:

PredicatePractical meaning
intersectsBoundaries or interiors share any point
disjointShare no point
withinLeft geometry lies in right interior/boundary under DE-9IM
containsInverse direction of within; boundary-only point is not contained
coversNo point of right lies outside left; includes boundary cases
covered_byInverse of covers
contains_properlyContains with no common boundary points
touchesInteriors do not meet, boundaries do
crossesInteriors meet with lower-dimensional result
overlapsSame-dimensional partial overlap, neither contains the other
dwithinPlanar 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.

Raw lines 185 to 214
215## Overlay robustness and slivers
217Overlay and intersection can create tiny slivers from precision mismatch,
218near-coincident edges, or distinct source accuracy:
2201. validate inputs and CRS;
2212. quantify source precision/accuracy;
2223. select a justified grid if snapping is appropriate;
2234. run overlay with explicit `keep_geom_type`;
2245. validate output and count type changes;
2256. summarize area distribution and very small parts in projected units;
2267. compare area conservation appropriate to the selected overlay mode.
228Do not delete polygons below an arbitrary area threshold. A small polygon can be
229legitimate, and thresholding can bias boundaries. Record any sliver rule and
230retain pre-cleaning output.

Overlay robustness and slivers

Overlay and intersection can create tiny slivers from precision mismatch, near-coincident edges, or distinct source accuracy:

  1. validate inputs and CRS;
  2. quantify source precision/accuracy;
  3. select a justified grid if snapping is appropriate;
  4. run overlay with explicit keep_geom_type;
  5. validate output and count type changes;
  6. summarize area distribution and very small parts in projected units;
  7. compare area conservation appropriate to the selected overlay mode.

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.

Raw lines 215 to 231
232## Equality and identity
234- `geom_equals` is topological equality; coordinate order may differ.
235- `geom_equals_exact(tolerance=...)` checks structural coordinate equality
236 within tolerance.
237- `geom_equals_identical` exposes Shapely's identical comparison in GeoPandas
238 1.1 and includes coordinate/order details.
239- `normalize()` can canonicalize ordering for reproducible comparisons, but a
240 normalized WKB hash is still geometry identity, not stable feature identity.

Equality and identity

  • 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.
Raw lines 232 to 241
242## Post-operation validation
244For every geometry-changing operation, record:
246- operation and all parameters;
247- source/target CRS and units;
248- package and GEOS versions;
249- null/empty/invalid/type/component counts before and after;
250- row expansion/contraction and stable-ID mapping;
251- precision model, repair method, collapsed-part policy;
252- area/length conservation checks where meaningful;
253- a new output path and source/output hashes.
255The bundled `scripts/geometry_validity_report.py` provides bounded dry-run
256counts and optional new-file repair without emitting geometries or coordinates.

Post-operation validation

For every geometry-changing operation, record:

  • operation and all parameters;
  • source/target CRS and units;
  • package and GEOS versions;
  • null/empty/invalid/type/component counts before and after;
  • row expansion/contraction and stable-ID mapping;
  • precision model, repair method, collapsed-part policy;
  • area/length conservation checks where meaningful;
  • a new output path and source/output hashes.

The bundled scripts/geometry_validity_report.py provides bounded dry-run counts and optional new-file repair without emitting geometries or coordinates.

Raw lines 242 to 257
Runs code scripts/geometry_validity_report.py :255
258## Sources (verified 2026-10-01)
260- [GeoPandas geometric manipulations](https://geopandas.org/en/stable/docs/user_guide/geometric_manipulations.html).
261- [GeoSeries.make_valid](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoSeries.make_valid.html).
262- [GeoSeries.union_all](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoSeries.union_all.html).
263- [GeoSeries.is_valid_coverage](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoSeries.is_valid_coverage.html).
264- [Shapely 2.1.2 make_valid](https://shapely.readthedocs.io/en/2.1.2/reference/shapely.make_valid.html).
265- [Shapely 2.1.2 set_precision](https://shapely.readthedocs.io/en/2.1.2/reference/shapely.set_precision.html).
266- [Shapely 2.1.2 union_all](https://shapely.readthedocs.io/en/2.1.2/reference/shapely.union_all.html).
267- [GeoPandas 1.1.0 release](https://github.com/geopandas/geopandas/releases/tag/v1.1.0) — released 2025-06-01.