Skill Tracer
Skills › Scientific Agent Skills › geopandas › data-structures.md

data-structures.md

207 lines · skills/geopandas/references/data-structures.md at 5f2bc03

Open on GitHub

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

1# GeoPandas data structures
2
3GeoPandas 1.2.0 extends pandas with a `geometry` extension dtype backed by
4Shapely 2. A `GeoSeries` is one geometry-valued pandas Series; a `GeoDataFrame`
5is a DataFrame with one active geometry column and may contain additional
6geometry columns.
7

GeoPandas data structures

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

Raw lines 1 to 7
8## Construction
9
10Always assign CRS at construction when it is known from authoritative metadata.
11Do not infer it from coordinate ranges.
13```python
14import geopandas as gpd
15import pandas as pd
16from shapely import Point, box
18points = gpd.GeoSeries(
19 [Point(0, 0), Point(1, 1), None],
20 index=["feature-a", "feature-b", "feature-c"],
21 crs="EPSG:3857",
22 name="location",
25gdf = gpd.GeoDataFrame(
26 {
27 "feature_id": ["feature-a", "feature-b"],
28 "value": [10, 20],
29 "geometry": [Point(0, 0), Point(1, 1)],
30 },
31 geometry="geometry",
32 crs="EPSG:3857",
35table = pd.DataFrame({"x": [0.0, 1.0], "y": [0.0, 1.0]})
36from_xy = gpd.GeoDataFrame(
37 table,
38 geometry=gpd.points_from_xy(table["x"], table["y"]),
39 crs="EPSG:3857",
41```
43`points_from_xy` interprets arguments as x then y. For geographic data, that is
44normally longitude then latitude in the coordinate array, even though the
45authority definition of EPSG:4326 advertises latitude-first axes.

Construction

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

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.

Raw lines 8 to 46
47## Active and additional geometry columns
49Frame-level spatial methods act on one active geometry column:
51```python
52gdf["buffered"] = gdf.geometry.buffer(10)
53gdf = gdf.set_geometry("buffered")
55assert gdf.active_geometry_name == "buffered"
56assert gdf.geometry.name == "buffered"
58gdf = gdf.rename_geometry("analysis_geometry")
59```
61Important distinctions:
63- `gdf.geometry` always returns the active geometry, not necessarily a column
64 literally named `"geometry"`.
65- `rename_geometry()` updates the active-column bookkeeping. A plain pandas
66 `rename(columns=...)` must be followed by `set_geometry()`.
67- A `GeoDataFrame` can hold multiple geometry columns with different CRS
68 metadata. Switching the active column switches the CRS exposed as `gdf.crs`.
69- Ordinary vector formats generally support only one geometry per layer.
70 GeoParquet and Feather can preserve multiple geometry columns.
71- GeoPandas 1.0 changed `set_geometry(named_series)`: the Series name becomes
72 the active column name and the old geometry column is preserved. Avoid the
73 deprecated `drop=` parameter; rename/drop explicitly.
75Check every geometry column independently:
77```python
78geometry_columns = [
79 name for name, dtype in gdf.dtypes.items() if str(dtype) == "geometry"
81column_crs = {name: gdf[name].crs for name in geometry_columns}
82```

Active and additional geometry columns

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

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:

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}
Raw lines 47 to 83
84## Missing, empty, and invalid are different
86Treat these states separately:
88| State | Test | Meaning |
89|---|---|---|
90| Missing | `series.isna()` | Unknown geometry, represented by `None` |
91| Empty | `series.is_empty` | A geometry object with no coordinates |
92| Invalid | `~series.is_valid` after excluding missing | Coordinates violate topology rules |
94```python
95missing = gdf.geometry.isna()
96empty = gdf.geometry.is_empty
97invalid = (~missing) & (~empty) & (~gdf.geometry.is_valid)
98usable = ~(missing | empty | invalid)
99```
101Missing values generally propagate through element-wise operations and are
102ignored by reductions such as `union_all()`. Empty geometries participate as
103geometries: they may have area `0.0` and remain empty after intersection.
104Never use only `dropna()` to remove unusable geometries.

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
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.

Raw lines 84 to 105
106## Index alignment
108Binary geometry methods are **row-wise**, not all-pairs operations. With a
109GeoSeries argument, `align=None` defaults to label alignment:
111```python
112left = gpd.GeoSeries([Point(0, 0), Point(1, 1)], index=["a", "b"])
113right = gpd.GeoSeries([Point(1, 1), Point(0, 0)], index=["b", "a"])
115by_label = left.intersects(right, align=True)
116by_position = left.intersects(right, align=False)
117```
119Use `align=False` only after proving equal lengths and intended row order.
120GeoPandas 1.0 raises on some unaligned pandas Series method arguments to avoid
121ambiguous automatic alignment. For all-pairs matching use a spatial join or
122spatial-index query.
124Assignment also aligns by index:
126```python
127result = gdf.copy()
128derived = result.geometry.buffer(10)
129result.loc[:, "buffered"] = derived # label-aligned
130```
132Reset or preserve indices deliberately before positional work. Never assume the
133pandas index is a stable feature identifier.

Index alignment

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

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:

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.

Raw lines 106 to 134
135## Feature identity and duplicate controls
137Keep a non-null, stable feature-ID column across reads, joins, explode,
138overlay, dissolve, and exports:
140```python
141ids = gdf["feature_id"]
142if ids.isna().any() or ids.duplicated(keep=False).any():
143 raise ValueError("feature_id must be non-null and unique for this workflow")
144```
146Cardinality-changing operations need explicit provenance:
148- `explode(ignore_index=False, index_parts=False)` defaults to no part-level
149 MultiIndex in GeoPandas 1.0+. Create a part number if parts need identity.
150- Spatial joins can repeat either side; retain both source IDs.
151- Overlay can split one feature into many. Add source IDs before overlay and
152 generate a derived ID afterward.
153- Dissolve intentionally combines IDs; record group keys and aggregation rules.
154- `pd.concat` requires compatible geometry-column CRS and can preserve duplicate
155 indices unless `ignore_index=True`.

Feature identity and duplicate controls

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

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.
Raw lines 135 to 156
157## Geometry type and dimensionality
159`geom_type`, `has_z`, and (with Shapely 2.1) `has_m` describe different
160properties. Mixed geometry types are valid in memory but can break overlay or
161export contracts. Z and M ordinates are not used by GeoPandas' planar topology:
163```python
164summary = {
165 "types": gdf.geometry.geom_type.value_counts(dropna=False).to_dict(),
166 "has_z": int(gdf.geometry.has_z.sum()),
167 "has_m": int(gdf.geometry.has_m.sum()),
169```
171Do not silently drop Z/M. If a target format or operation is 2D-only, record the
172loss and create a new derived artifact.

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:

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.

Raw lines 157 to 173
174## Copying and conversion
176- Use `gdf.copy()` before replacing an active geometry.
177- Call `merge()` from the GeoDataFrame side; `plain_df.merge(gdf, ...)` can
178 return a non-spatial DataFrame.
179- Reading a non-spatial layer with `read_file()` returns a pandas DataFrame in
180 GeoPandas 1.0+.
181- `np.asarray(gdf.geometry)` or `gdf.geometry.to_numpy()` replaces removed
182 access to `GeometryArray.data`.
183- Do not serialize geometries with pickle for exchange. Use GeoPackage,
184 GeoParquet, WKB, or WKT with an explicit CRS contract.

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.
Raw lines 174 to 185
186## Minimum structure audit
188Record, without emitting coordinates or identifiers:
1901. row and column counts;
1912. active and additional geometry-column names;
1923. CRS per geometry column;
1934. counts of missing, empty, invalid, Z/M, and each geometry type;
1945. index uniqueness and stable-ID null/duplicate counts;
1956. source hash, parser/driver, package/native versions, and operation timestamp.
197The bundled `scripts/vector_inventory.py` emits a redacted metadata inventory;
198`scripts/geometry_validity_report.py` adds bounded geometry-state counts.

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.

Raw lines 186 to 199
Runs code scripts/vector_inventory.py :197
Runs code scripts/geometry_validity_report.py :198
200## Sources (verified 2026-10-01)
202- [GeoPandas data structures](https://geopandas.org/en/stable/docs/user_guide/data_structures.html).
203- [GeoSeries API](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoSeries.html).
204- [GeoDataFrame API](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.html).
205- [Missing and empty geometries](https://geopandas.org/en/stable/docs/user_guide/missing_empty.html).
206- [GeoSeries.intersects alignment](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoSeries.intersects.html).
207- [GeoPandas 1.0.0 release and migrations](https://github.com/geopandas/geopandas/releases/tag/v1.0.0) — released 2024-06-24.