Back to Claude Scientific Skills

GeoPandas data structures

skills/geopandas/references/data-structures.md

2.55.07.4 KB
Original Source

GeoPandas data structures

GeoPandas 1.1.4 extends pandas with a geometry extension dtype backed by Shapely 2. A GeoSeries is one geometry-valued pandas Series; a GeoDataFrame is a DataFrame with one active geometry column and may contain additional geometry columns.

Construction

Always assign CRS at construction when it is known from authoritative metadata. Do not infer it from coordinate ranges.

python
import geopandas as gpd
import pandas as pd
from shapely import Point, box

points = gpd.GeoSeries(
    [Point(0, 0), Point(1, 1), None],
    index=["feature-a", "feature-b", "feature-c"],
    crs="EPSG:3857",
    name="location",
)

gdf = gpd.GeoDataFrame(
    {
        "feature_id": ["feature-a", "feature-b"],
        "value": [10, 20],
        "geometry": [Point(0, 0), Point(1, 1)],
    },
    geometry="geometry",
    crs="EPSG:3857",
)

table = pd.DataFrame({"x": [0.0, 1.0], "y": [0.0, 1.0]})
from_xy = gpd.GeoDataFrame(
    table,
    geometry=gpd.points_from_xy(table["x"], table["y"]),
    crs="EPSG:3857",
)

points_from_xy interprets arguments as x then y. For geographic data, that is normally longitude then latitude in the coordinate array, even though the authority definition of EPSG:4326 advertises latitude-first axes.

Active and additional geometry columns

Frame-level spatial methods act on one active geometry column:

python
gdf["buffered"] = gdf.geometry.buffer(10)
gdf = gdf.set_geometry("buffered")

assert gdf.active_geometry_name == "buffered"
assert gdf.geometry.name == "buffered"

gdf = gdf.rename_geometry("analysis_geometry")

Important distinctions:

  • gdf.geometry always returns the active geometry, not necessarily a column literally named "geometry".
  • rename_geometry() updates the active-column bookkeeping. A plain pandas rename(columns=...) must be followed by set_geometry().
  • A GeoDataFrame can hold multiple geometry columns with different CRS metadata. Switching the active column switches the CRS exposed as gdf.crs.
  • Ordinary vector formats generally support only one geometry per layer. GeoParquet and Feather can preserve multiple geometry columns.
  • GeoPandas 1.0 changed set_geometry(named_series): the Series name becomes the active column name and the old geometry column is preserved. Avoid the deprecated drop= parameter; rename/drop explicitly.

Check every geometry column independently:

python
geometry_columns = [
    name for name, dtype in gdf.dtypes.items() if str(dtype) == "geometry"
]
column_crs = {name: gdf[name].crs for name in geometry_columns}

Missing, empty, and invalid are different

Treat these states separately:

StateTestMeaning
Missingseries.isna()Unknown geometry, represented by None
Emptyseries.is_emptyA geometry object with no coordinates
Invalid~series.is_valid after excluding missingCoordinates violate topology rules
python
missing = gdf.geometry.isna()
empty = gdf.geometry.is_empty
invalid = (~missing) & (~empty) & (~gdf.geometry.is_valid)
usable = ~(missing | empty | invalid)

Missing values generally propagate through element-wise operations and are ignored by reductions such as union_all(). Empty geometries participate as geometries: they may have area 0.0 and remain empty after intersection. Never use only dropna() to remove unusable geometries.

Index alignment

Binary geometry methods are row-wise, not all-pairs operations. With a GeoSeries argument, align=None defaults to label alignment:

python
left = gpd.GeoSeries([Point(0, 0), Point(1, 1)], index=["a", "b"])
right = gpd.GeoSeries([Point(1, 1), Point(0, 0)], index=["b", "a"])

by_label = left.intersects(right, align=True)
by_position = left.intersects(right, align=False)

Use align=False only after proving equal lengths and intended row order. GeoPandas 1.0 raises on some unaligned pandas Series method arguments to avoid ambiguous automatic alignment. For all-pairs matching use a spatial join or spatial-index query.

Assignment also aligns by index:

python
result = gdf.copy()
derived = result.geometry.buffer(10)
result.loc[:, "buffered"] = derived  # label-aligned

Reset or preserve indices deliberately before positional work. Never assume the pandas index is a stable feature identifier.

Feature identity and duplicate controls

Keep a non-null, stable feature-ID column across reads, joins, explode, overlay, dissolve, and exports:

python
ids = gdf["feature_id"]
if ids.isna().any() or ids.duplicated(keep=False).any():
    raise ValueError("feature_id must be non-null and unique for this workflow")

Cardinality-changing operations need explicit provenance:

  • explode(ignore_index=False, index_parts=False) defaults to no part-level MultiIndex in GeoPandas 1.0+. Create a part number if parts need identity.
  • Spatial joins can repeat either side; retain both source IDs.
  • Overlay can split one feature into many. Add source IDs before overlay and generate a derived ID afterward.
  • Dissolve intentionally combines IDs; record group keys and aggregation rules.
  • pd.concat requires compatible geometry-column CRS and can preserve duplicate indices unless ignore_index=True.

Geometry type and dimensionality

geom_type, has_z, and (with Shapely 2.1) has_m describe different properties. Mixed geometry types are valid in memory but can break overlay or export contracts. Z and M ordinates are not used by GeoPandas' planar topology:

python
summary = {
    "types": gdf.geometry.geom_type.value_counts(dropna=False).to_dict(),
    "has_z": int(gdf.geometry.has_z.sum()),
    "has_m": int(gdf.geometry.has_m.sum()),
}

Do not silently drop Z/M. If a target format or operation is 2D-only, record the loss and create a new derived artifact.

Copying and conversion

  • Use gdf.copy() before replacing an active geometry.
  • Call merge() from the GeoDataFrame side; plain_df.merge(gdf, ...) can return a non-spatial DataFrame.
  • Reading a non-spatial layer with read_file() returns a pandas DataFrame in GeoPandas 1.0+.
  • np.asarray(gdf.geometry) or gdf.geometry.to_numpy() replaces removed access to GeometryArray.data.
  • Do not serialize geometries with pickle for exchange. Use GeoPackage, GeoParquet, WKB, or WKT with an explicit CRS contract.

Minimum structure audit

Record, without emitting coordinates or identifiers:

  1. row and column counts;
  2. active and additional geometry-column names;
  3. CRS per geometry column;
  4. counts of missing, empty, invalid, Z/M, and each geometry type;
  5. index uniqueness and stable-ID null/duplicate counts;
  6. source hash, parser/driver, package/native versions, and operation timestamp.

The bundled scripts/vector_inventory.py emits a redacted metadata inventory; scripts/geometry_validity_report.py adds bounded geometry-state counts.

Sources (verified 2026-07-23)