4.6. Geometry Validation and Quality Control#
4.6.1. Learning objectives#
By the end of this section, you should be able to:
run a repeatable inspection routine immediately after reading a layer;
distinguish invalid, empty, and missing geometries and report each separately;
diagnose and repair invalid geometry, and record what the repair changed;
turn the invariants of an operation into assertions;
explain the difference between
set_crsandto_crs; andrecord the metadata needed to interpret a result responsibly.
Section goal
Build the checks that make assumptions visible before they become maps, statistics, or decisions. A toy parks layer with one planted defect of each kind shows what every check finds.
A spatial operation can return a result without raising an error and still answer the wrong question. Quality control makes assumptions visible before they become maps, statistics, or decisions.
4.6.2. A repeatable inspection routine#
Run a small set of checks immediately after reading each layer. Inspection is cheap, and it finds the problems that every later stage inherits. Table 4.21 lists each check and what it reveals.
Check |
What it reveals |
How |
|---|---|---|
Row count and identifiers |
Duplicate or missing records |
|
CRS |
A missing or wrong reference system |
|
Geometry types |
Mixed or unexpected multipart types |
|
Missing, empty, invalid geometry |
Rows that break later operations |
|
Bounds |
Swapped or wrong-CRS coordinates |
|
Attribute nulls, units, dates |
Incomplete or stale records |
|
A plotted sample |
Problems no summary statistic shows |
|
A clean layer would show nothing, so the toy parks layer below plants one defect of each kind: a self-crossing bow-tie, an empty polygon, a missing geometry, a duplicated identifier, and a missing name. Its coordinates are in metres in a UTM CRS.
asset_cd site_name
0 P-001 Central Green
1 P-002 Riverside
2 P-003 Bow-tie Park
3 P-004 Empty Lot
4 P-005 NaN
5 P-002 Riverside East
{'rows': 6, 'columns': ['asset_cd', 'site_name', 'geometry'], 'crs': 'EPSG:32611', 'geometry_types': {'Polygon': 5, 'missing': 1}, 'missing_geometry': 1, 'empty_geometry': 1, 'invalid_geometry': 1, 'null_attributes': {'site_name': 1}, 'duplicate_ids': 1}
bounds: [0.0, 0.0, 700.0, 100.0]
The report finds each planted defect: one missing geometry, one empty geometry, one
invalid geometry, one duplicated identifier, and one null name. It excludes missing
geometries from the invalid count on purpose, because is_valid returns False for a
missing geometry, so counting it would report an invalid geometry that does not exist.
Run the report before and after every repair or filter, and keep the output with the
analysis.
Also inspect total_bounds (which ignores missing and empty geometries), units, dates,
and a plotted sample. Verify that the CRS is defined rather than assuming that a missing
CRS means WGS 84.
4.6.3. Invalid, empty, and missing geometries#
Common invalidity causes include self-intersections, unclosed rings, duplicate vertices, and incorrect holes (S01 lists the rules [Herring, 2011]). Empty and missing geometries are different:
an empty geometry is present but has no coordinates;
a missing geometry is null; and
an invalid geometry has coordinates that violate the rules for its type.
Table 4.22 shows why the three must be counted separately: each one fails differently.
Case |
|
|
Area |
Effect in later operations |
|---|---|---|---|---|
Missing ( |
|
|
|
Nulls propagate; must be counted and handled |
Empty |
|
|
0 |
Valid but matches nothing and adds no area |
Invalid |
|
|
Unreliable, and can be 0 |
Overlays fail or return wrong areas |
asset_cd missing empty valid area_m2
P-001 False False True 10000.0
P-002 False False True 10000.0
P-003 False False False 0.0
P-004 False True True 0.0
P-005 True False False NaN
P-002 False False True 10000.0
The missing row has a NaN area and the empty row has an area of 0, but the bow-tie also
reports 0 even though it encloses two triangles. That is how invalid geometry produces a
plausible but wrong number.
Repair only after understanding the source problem. make_valid [GEOS: Geometry Engine - Open Source, 2024, Gillies et al., 2026] rebuilds the geometry from the same coordinates, splitting it at each self-intersection, so a polygon can come
back as a MultiPolygon or a GeometryCollection. With Shapely versions that provide
make_valid, a repair can be recorded explicitly: keep the original layer, safely filter
out both missing values and structurally empty geometries to avoid modern library warnings,
repair only rows that have a geometry, and log what changed.
P-003 -> Self-intersection[450 50]
type before type after area before area after
2 Polygon MultiPolygon 0.0 5000.0
The bow-tie becomes a two-part MultiPolygon with the area it should have always had.
Selecting only rows with a geometry matters, because make_valid raises an error on a
missing value.
The often-used buffer(0) workaround may repair some polygon defects, but it can
change geometry type or remove small parts. The comparison below shows how much it can
remove. Treat buffer(0) as a documented fallback, not a universal validator. Re-run
the checks after repair and retain the original layer.
make_valid MultiPolygon area=5000 m²
buffer(0) Polygon area=2500 m²
{'rows': 6, 'columns': ['asset_cd', 'site_name', 'geometry'], 'crs': 'EPSG:32611', 'geometry_types': {'Polygon': 4, 'MultiPolygon': 1, 'missing': 1}, 'missing_geometry': 1, 'empty_geometry': 1, 'invalid_geometry': 0, 'null_attributes': {'site_name': 1}, 'duplicate_ids': 1}
make_valid keeps both lobes (5,000 m²), whereas buffer(0) silently keeps only one
(2,500 m²) and raises no error. After the repair the invalid count is zero, and
everything else in the report is unchanged: repair fixes geometry only, so the missing
geometry, the empty polygon, and the duplicated identifier still need a decision.
4.6.4. Invariants for common operations#
Use expectations that reflect the operation. Table 4.23 pairs each operation with the invariant it must satisfy and an assertion that tests it. Assertions turn those expectations into early failures:
Operation |
Invariant |
Assertion |
|---|---|---|
|
Cannot create more rows than the input |
|
Left attribute join, unique right key |
Retains the left row count |
|
|
Adds no area outside A |
|
Overlay |
Pieces satisfy the area identity from S04 |
|
Metric calculation |
Lengths and areas are non-negative |
|
Any output |
Identifiers trace back to the source |
|
all invariants hold: 4 of 6 rows kept
The filter keeps four rows: P-001, both P-002 rows, and the repaired P-003. A passing geometry check says nothing about the duplicated identifier, which still appears twice among them, so the identifier needs its own decision.
An assertion earns its place when it fails. The unrepaired layer breaks the validity expectation, and the failure carries a message that says why. Use tolerances for floating-point comparisons [Goldberg, 1991] and explain exceptions instead of disabling checks.
AssertionError: the unrepaired layer contains invalid geometry
False True
The assertion fails on exactly the layer known to be faulty, and its message names the
cause. The second line shows why tolerances matter: 0.1 + 0.2 == 0.3 is False while
math.isclose returns True, so equality tests on computed areas or distances should
use a tolerance.
4.6.5. CRS and units#
Every operation involving two layers should begin with a CRS comparison, and every measurement needs its units. Table 4.24 separates the two methods that are most often confused.
Method |
What it does |
Changes the coordinates? |
Use when |
|---|---|---|---|
|
Labels coordinates that already use a known CRS |
No |
The CRS is missing and you know what it truly is |
|
Transforms coordinates between two known CRSs |
Yes |
Layers must share a CRS, or you need metres |
set_crs [Jordahl et al., 2020] refuses to replace an existing CRS unless allow_override=True, which is a
guard against relabelling by accident. The cell below compares the CRSs of a layer pair
and aligns them, and reports the units that each CRS uses.
right before: EPSG:4326 - degree
right after: EPSG:32611 - metre
Reprojection does not fix a wrongly assigned CRS. set_crs labels coordinates that
already use a known CRS; to_crs transforms coordinates between known CRSs. Confusing
the two can move data by hundreds or thousands of kilometers. The example below labels
longitude and latitude values as if they were UTM metres.
to_crs : POINT (706264.8946586304 5659061.020824244)
set_crs : POINT (-114.0573 51.046)
mislabelled point lands 5,701 km from the true location
to_crs changed the numbers, giving an easting of 706,265 m and a northing of 5,659,061
m, whereas set_crs left the degree values untouched and merely called them metres. The
mislabelled point lands 5,701 km from Calgary, and nothing raises an error, because
every number is still a legal coordinate.
4.6.6. Responsible interpretation#
Record the source, retrieval date, spatial extent, CRS, predicate or overlay, thresholds, and unit of analysis [Wilkinson et al., 2016]. Table 4.25 explains why each item matters.
Item |
Why it matters |
Example |
|---|---|---|
Source and retrieval date |
Portals update, and results must be reproducible |
Open-data portal name and download date |
Spatial extent |
Defines where absence is meaningful |
City boundary or study area |
CRS |
Gives the coordinates their meaning |
Stored in EPSG:4326, analysed in a UTM zone |
Predicate or overlay |
Determines how boundary contact is treated |
|
Thresholds |
Findings change with them |
A 500 m service radius |
Unit of analysis |
States what one row counts |
Community district |
Absence of a feature usually means “not observed,” not “does not exist,” unless the source guarantees complete coverage. Check sensitivity to buffers, boundary predicates, and missing values before presenting a conclusion.
Network-specific connectivity checks are introduced separately in Chapter 5.
4.6.7. Summary#
Validation is part of the analysis, not a final cosmetic step. Inspect geometry, attributes, CRS, identifiers, and units; keep missing, empty, and invalid geometries separate; repair cautiously and log what each repair changed; encode invariants as assertions; and report limitations with every derived result.