GeoPandas "No Geometry Column" Error: How to Fix It

On this page

Problem statement

A common GeoPandas problem happens when you call a geospatial method on an object that has no active geometry column. With GeoPandas 1.1 it shows up as one of these:

  • AttributeError: You are calling a geospatial method on the GeoDataFrame, but the active geometry column to use has not been set. β€” no column was ever made active
  • AttributeError: ... but the active geometry column ('geometry') is not present. β€” the active column was renamed or dropped
  • AttributeError: 'DataFrame' object has no attribute 'to_crs' β€” the object is not a GeoDataFrame at all
  • TypeError: Input must be valid geometry objects β€” set_geometry() was pointed at text, such as WKT
  • KeyError: 'geometry' β€” plain pandas indexing on a column that does not exist

Older GeoPandas releases said No geometry data set yet (expected in column 'geometry') for the first case; the meaning is the same.

A missing geometry column is usually caused by one of these situations:

  • the active geometry column was dropped, or renamed with rename()
  • the object is a plain DataFrame, for example after pd.read_csv(), pd.read_parquet(), or a merge or concat that started from a plain DataFrame
  • the geometry lives in a column with a different name that was never set as active
  • coordinates exist as lon/lat numbers, or shapes as WKT text or WKB bytes, that were never converted to geometry

Every example below uses real files: New York City's borough boundaries, which NYC Open Data exports as a CSV with WKT in a the_geom column; the same boroughs written to GeoParquet by DuckDB, which names its geometry column geom; and the UK's airports from OurAirports, a CSV of plain latitude and longitude.

Quick answer

If you have a column that holds geometry but it is not active, set it with set_geometry(). If the column holds text or bytes, parse it first. If you only have coordinates, build points:

import geopandas as gpd
import pandas as pd

boroughs = pd.read_csv("data/nyc_boroughs.csv")                     # the_geom is WKT text
boroughs = gpd.GeoDataFrame(boroughs, geometry=gpd.GeoSeries.from_wkt(boroughs["the_geom"]), crs="EPSG:4326")

airports = pd.read_csv("data/uk_airports.csv")                       # latitude_deg, longitude_deg
airports = gpd.GeoDataFrame(airports, crs="EPSG:4326",
                            geometry=gpd.points_from_xy(airports["longitude_deg"], airports["latitude_deg"]))

print(boroughs.geometry.name, boroughs.geom_type.unique().tolist(), "|", airports.geometry.name, len(airports))
geometry ['MultiPolygon'] | geometry 88

Use set_geometry() when the geometry already exists as shapes. Build a new GeoDataFrame when you only have raw coordinates, WKT strings or WKB bytes.

Choosing the repair path

Restore the geometry columnIs it a GeoDataFrame?Rebuild: gpd.GeoDataFrame(df,geometry=..., crs=...)NoYesGeometry under another name?set_geometry("geom")YesNoHave coords or WKT? points_from_xy or from_wktspatialworkflow.io
Choosing how to restore an active geometry column.

Find the geometry column and make it active

Choosing the repair path when GeoPandas reports no geometry column. what is actually in your variable?Where did the object lose its geometry?plain DataFrameBuild geometrypoints_from_xy()column existsActivate itset_geometry("geom")renamed / droppedRestore the columnthen set_geometry()The error means "no active geometry", which is not the same as "no geometry" spatialworkflow.io
Work out which of the three you have before touching the data β€” the fixes differ.

Step 1: Confirm whether the object is a GeoDataFrame

A frequent cause is that you are working with a plain DataFrame. Check the type first:

raw = pd.read_csv("data/nyc_boroughs.csv")
print(type(raw).__name__, isinstance(raw, gpd.GeoDataFrame))
print(raw.dtypes.to_dict())
DataFrame False
{'BoroCode': dtype('int64'), 'BoroName': <StringDtype(na_value=nan)>, 'Shape_Area': dtype('float64'), 'Shape_Length': dtype('float64'), 'the_geom': <StringDtype(na_value=nan)>}

A CSV never contains geometry, so pd.read_csv() always returns a plain DataFrame, and the_geom is a column of text. If isinstance(obj, gpd.GeoDataFrame) is False, you need to build a GeoDataFrame, not just set the geometry.

Step 2: Find the geometry column

Wrapping that DataFrame in GeoDataFrame() does not help on its own β€” GeoPandas does not guess which column holds shapes:

wrapped = gpd.GeoDataFrame(raw)
try:
    wrapped.geometry
except AttributeError as err:
    print("AttributeError:", str(err).splitlines()[0])
    print(" ", str(err).splitlines()[1])
AttributeError: You are calling a geospatial method on the GeoDataFrame, but the active geometry column to use has not been set. 
  There are no existing columns with geometry data type. You can add a geometry column as the active geometry column with df.set_geometry. 

Look for a column that holds shapes but is not named geometry β€” geom, the_geom, shape, wkt and wkb_geometry are all common β€” and check what is actually in it. The second line of the message tells you whether any column already has the geometry dtype.

Step 3: Set the geometry column as active

set_geometry() needs shapes, not text:

try:
    wrapped.set_geometry("the_geom")
except TypeError as err:
    print("TypeError:", str(err)[:60], "...")

wrapped["the_geom"] = gpd.GeoSeries.from_wkt(wrapped["the_geom"])
boroughs = wrapped.set_geometry("the_geom").set_crs("EPSG:4326")
print(boroughs.geometry.name, "->", boroughs.rename_geometry("geometry").geometry.name)
TypeError: Input must be valid geometry objects: MULTIPOLYGON (((-74.05 ...
the_geom -> geometry

rename_geometry() renames the active column and keeps it active, if you want it called geometry.

What four readers return for the NYC borough boundaries, and what the geometry column is. the same five boroughs, four readerscallreturnsgeometry ispd.read_csv(csv)DataFrameWKT text in the_geompd.read_parquet(pq)DataFrameWKB bytes in geomgpd.read_parquet(pq)GeoDataFrameactive, named geomgpd.read_file(gpkg)GeoDataFrameactive, named geometryTwo of these have shapes but no geometry; two name it differently spatialworkflow.io
Measured on the NYC borough files: the reader, not the data, decides whether there is an active geometry and what it is called.

Step 4: Rebuild geometry from coordinates

If you only have coordinate columns, create point geometry with points_from_xy():

airports = pd.read_csv("data/uk_airports.csv")
print(airports[["name", "latitude_deg", "longitude_deg"]].head(2))

airports = gpd.GeoDataFrame(airports, crs="EPSG:4326",
                            geometry=gpd.points_from_xy(airports["longitude_deg"], airports["latitude_deg"]))
print(airports.geometry.name, airports.total_bounds.round(1))
                            name  latitude_deg  longitude_deg
0  Belfast International Airport     54.657501      -6.215830
1  Enniskillen/St Angelo Airport     54.398274      -7.651179
geometry [-7.7 50.1  1.3 59.9]

Note the argument order: points_from_xy(x, y) takes longitude first. OurAirports stores latitude first, which is exactly how the columns get swapped.

Step 5: Verify the fix

After setting or rebuilding the geometry, check that GeoPandas sees it and that it means what you think. The NYC export carries each borough's area in square feet from the city's own GIS, which makes a good test:

check = boroughs.to_crs("EPSG:2263")                                  # NY State Plane, US feet
print(isinstance(check, gpd.GeoDataFrame), check.geometry.name, check.crs.axis_info[0].unit_name)
print((check.area / check["Shape_Area"]).round(4).tolist())
True the_geom US survey foot
[1.0, 1.0, 1.0, 1.0, 1.0]

The areas computed from the parsed WKT match the published Shape_Area to four decimal places for all five boroughs, so the geometry, the CRS and the parsing all agree.

Losing and restoring geometry on New York's boroughs

The data used on this page

  • NYC borough boundaries β€” NYC Open Data's Borough Boundaries (dataset gthc-hcne) exported as CSV: five rows with BoroCode, BoroName, Shape_Area, Shape_Length and the shape as WKT in the_geom.
  • Boroughs as GeoParquet β€” the same boundaries written to GeoParquet by DuckDB's spatial extension, which names the geometry column geom, as it does for everything it reads with ST_Read.
  • UK airports β€” the large and medium airports in the UK from OurAirports' public-domain airports.csv.
import os
import time

import duckdb
import geopandas as gpd
import pandas as pd
import requests

os.makedirs("data", exist_ok=True)
if not os.path.exists("data/nyc_boroughs.csv"):
    for attempt in range(5):                     # NYC Open Data answers 503 now and then
        reply = requests.get("https://data.cityofnewyork.us/api/views/gthc-hcne/rows.csv?accessType=DOWNLOAD",
                             timeout=120)
        if reply.status_code != 503:
            break
        time.sleep(10 * (attempt + 1))
    reply.raise_for_status()
    open("data/nyc_boroughs.csv", "wb").write(reply.content)

if not os.path.exists("data/nyc_boroughs.parquet"):
    con = duckdb.connect()
    con.install_extension("spatial")
    con.load_extension("spatial")
    con.sql("""COPY (SELECT BoroCode, BoroName, ST_GeomFromText(the_geom) AS geom
                     FROM read_csv('data/nyc_boroughs.csv')) TO 'data/nyc_boroughs.parquet' (FORMAT parquet)""")

if not os.path.exists("data/uk_airports.csv"):
    everything = pd.read_csv("https://davidmegginson.github.io/ourairports-data/airports.csv")
    uk = everything[(everything["iso_country"] == "GB") & everything["type"].isin(["large_airport", "medium_airport"])]
    uk[["ident", "name", "latitude_deg", "longitude_deg"]].to_csv("data/uk_airports.csv", index=False)

for name in ("nyc_boroughs.csv", "nyc_boroughs.parquet", "uk_airports.csv"):
    print(f"{name:22} {os.path.getsize('data/' + name):>10,} bytes")
nyc_boroughs.csv        3,085,880 bytes
nyc_boroughs.parquet    1,239,059 bytes
uk_airports.csv             3,967 bytes

Example 1: GeoParquet read with pandas instead of GeoPandas

import geopandas as gpd
import pandas as pd

as_table = pd.read_parquet("data/nyc_boroughs.parquet")
print(type(as_table).__name__, type(as_table["geom"].iloc[0]).__name__)

as_geo = gpd.read_parquet("data/nyc_boroughs.parquet")
print(type(as_geo).__name__, "active:", as_geo.geometry.name, "| CRS:", as_geo.crs)

rebuilt = gpd.GeoDataFrame(as_table, geometry=gpd.GeoSeries.from_wkb(as_table["geom"]), crs="OGC:CRS84")
print(rebuilt.geometry.name, rebuilt.geom_type.iloc[0])
DataFrame bytes
GeoDataFrame active: geom | CRS: OGC:CRS84
geometry MultiPolygon

pd.read_parquet() hands back the geometry as raw WKB bytes; gpd.read_parquet() reads the file's GeoParquet metadata and makes geom active under its original name. Note the CRS: DuckDB never had one, and GeoParquet's default for a file without one is OGC:CRS84 β€” longitude/latitude, which is right here by luck.

Example 2: Geometry column with a different name

The name of the geometry column depends on how you read the file, not only on what is in it:

import geopandas as gpd

from_parquet = gpd.read_parquet("data/nyc_boroughs.parquet")
from_parquet.to_file("output_boroughs.gpkg")                   # the GeoPackage stores it as "geom"
from_gpkg = gpd.read_file("output_boroughs.gpkg")

print("read_parquet:", from_parquet.geometry.name, from_parquet.columns.tolist())
print("read_file:   ", from_gpkg.geometry.name, from_gpkg.columns.tolist())
read_parquet: geom ['BoroCode', 'BoroName', 'geom']
read_file:    geometry ['BoroCode', 'BoroName', 'geometry']

read_file() always calls the geometry geometry; read_parquet() and read_postgis() keep the source's name. Code that does gdf["geometry"] works on one and raises KeyError on the other. gdf.geometry works on both.

Example 3: Rebuild geometry from WKT strings

import geopandas as gpd
import pandas as pd

df = pd.read_csv("data/nyc_boroughs.csv")
print(df["the_geom"].str[:40].iloc[0], "...")

gdf = gpd.GeoDataFrame(df.drop(columns="the_geom"), geometry=gpd.GeoSeries.from_wkt(df["the_geom"]), crs="EPSG:4326")
print(gdf.geometry.name, gdf[["BoroName", "geometry"]].assign(parts=gdf.geometry.count_geometries()).drop(columns="geometry"))
MULTIPOLYGON (((-74.05050806403247 40.56 ...
geometry         BoroName  parts
0  Staten Island      4
1       Brooklyn     28
2         Queens     23
3      Manhattan     36
4          Bronx     26

Dropping the text column afterwards avoids carrying a multi-megabyte string copy of every shape. The part counts are real: every borough is a multipolygon, because every borough includes islands.

Example 4: The call order decides whether geometry survives

import geopandas as gpd
import pandas as pd

gdf = gpd.read_parquet("data/nyc_boroughs.parquet")
names = pd.read_csv("data/nyc_boroughs.csv")[["BoroCode", "Shape_Area"]]

print("gdf.merge(df):   ", type(gdf.merge(names, on="BoroCode")).__name__)
print("df.merge(gdf):   ", type(names.merge(gdf, on="BoroCode")).__name__)
print("concat, gdf first:", type(pd.concat([gdf, gdf])).__name__)
print("gdf[[cols]]:      ", type(gdf[["BoroCode", "BoroName"]]).__name__)

lost = names.merge(gdf, on="BoroCode")
restored = gpd.GeoDataFrame(lost, geometry="geom", crs=gdf.crs)
print("restored:", type(restored).__name__, restored.geometry.name)
gdf.merge(df):    GeoDataFrame
df.merge(gdf):    DataFrame
concat, gdf first: GeoDataFrame
gdf[[cols]]:       DataFrame
restored: GeoDataFrame geom

Calling merge() on the GeoDataFrame keeps it a GeoDataFrame; calling it on the plain DataFrame does not, though the geometry column is still there to restore.

The active geometry column

A GeoDataFrame is a DataFrame with one extra concept: an active geometry column. This is the column that spatial methods read from. When you call gdf.plot(), gdf.to_crs(), gdf.buffer(), or gpd.sjoin(), GeoPandas looks up the active geometry column by name and operates on it.

A DataFrame with a geometry-shaped column versus a GeoDataFrame with an active one. the difference is one pointerDataFrame (no active geometry)βœ•geometry may exist as a columnβœ•.crs raises AttributeErrorβœ•.plot() draws a bar chartβœ•sjoin / clip refuse to runGeoDataFrame (geometry set)βœ“one column is marked activeβœ“.crs returns the CRSβœ“.plot() draws a mapβœ“every spatial method worksset_geometry() marks the column; it does not create the data spatialworkflow.io
GeoPandas needs to be told which column is spatial β€” it never guesses.

By default that column is named geometry, but it does not have to be. A GeoDataFrame can hold geometry in any column name, and even hold more than one geometry column. Only one column is active at a time, and gdf.geometry.name tells you which one it is.

The "active geometry column … has not been set" error means GeoPandas tried to find the active geometry column and could not. Either no column was ever set as active, the active column was dropped or renamed, or the object is a plain DataFrame that has no geometry concept at all.

This is why the fix depends on the situation. If geometry exists in a column, you make that column active with set_geometry(). If the object is a plain DataFrame, you rebuild a GeoDataFrame. If you only have coordinates, WKT or WKB, you create geometry first, then assign it.

Operations that drop the geometry

Only a plain DataFrame on the left loses the geometry

gdf.merge(df) returns a GeoDataFrame; df.merge(gdf) returns a plain DataFrame (Example 4). The same goes for pd.concat(): the type of the first object decides. Rebuild with gpd.GeoDataFrame(result, geometry="<name>", crs=gdf.crs).

Reading a CSV or Parquet file with pandas never gives you geometry

pd.read_csv() returns text and numbers; pd.read_parquet() returns geometry as WKB bytes. Use gpd.points_from_xy(), GeoSeries.from_wkt() or GeoSeries.from_wkb() β€” or gpd.read_parquet(), which reads the GeoParquet metadata for you.

Multiple geometry columns

A GeoDataFrame can hold several geometry columns, for example the boundary and a centroid. Only the active one is used by spatial methods. Switch with gdf.set_geometry("centroid"); the other columns stay as geometry and can still be used as GeoSeries directly.

Dropping the geometry column

Selecting columns with gdf[["BoroCode", "BoroName"]] returns a plain DataFrame, and so does gdf.drop(columns="geometry"), after which to_crs fails with AttributeError: 'DataFrame' object has no attribute 'to_crs'. Keep the geometry column in the selection if you need to stay spatial.

Renaming the active column with rename()

gdf.rename(columns={"geometry": "geom"}) returns a GeoDataFrame whose active geometry is still recorded as geometry β€” a column that no longer exists β€” so the next spatial call fails with "the active geometry column ('geometry') is not present". Use gdf.rename_geometry("geom"), which renames the column and keeps it active.

For the underlying concept, read What Is a GeoDataFrame? Structure and Key Concepts.

Related task guides:

If geometry quality is the next problem, see How to Remove Null and Empty Geometries in GeoPandas.

FAQ

Why does GeoPandas say "No geometry data set" when my data has coordinates?

Coordinates in lon/lat columns are just numbers, and wrapping the DataFrame in GeoDataFrame() does not change that. GeoPandas needs an actual geometry column. Build one with gpd.points_from_xy() and construct a GeoDataFrame, or set an existing geometry column active with set_geometry().

How do I check which column is the active geometry?

Use gdf.geometry.name, which returns the name of the active geometry column. If it raises an AttributeError, no geometry column is set yet, or the one that was set has been renamed or dropped.

Why did my GeoDataFrame turn into a plain DataFrame after a merge?

It did not come from gdf.merge(...), which keeps the GeoDataFrame; it comes from df.merge(gdf, ...) or pd.merge(df, gdf, ...) with the plain DataFrame first. Put the GeoDataFrame first, or rebuild with gpd.GeoDataFrame(merged, geometry="geometry", crs=gdf.crs).

What is the difference between set_geometry() and building a new GeoDataFrame?

set_geometry() activates a column that already contains geometry objects. Building a new GeoDataFrame is for when you only have raw coordinates or WKT strings and need to create the geometry first.

How do I keep the GeoDataFrame type after a pd.concat()?

pd.concat() can return a plain DataFrame, dropping the active geometry. Put a GeoDataFrame first β€” pd.concat([gdf1, gdf2]) returns a GeoDataFrame β€” or rebuild it afterward with gpd.GeoDataFrame(combined, geometry="geometry", crs=gdf1.crs).

Why does gpd.sjoin() complain about no geometry on the right frame?

gpd.sjoin() needs a GeoDataFrame on both sides; given a plain DataFrame it raises ValueError: 'right_df' should be GeoDataFrame. If the right side came from a merge or column selection that lost its geometry, rebuild it as a GeoDataFrame before the join, or call set_geometry() on the column that still holds geometry.

Can a GeoDataFrame have geometry but no active geometry column set?

Yes. A GeoDataFrame can hold geometry objects in a column while having no column marked active, which still raises the "active geometry column … has not been set" error. Activate one with gdf.set_geometry("geom").

How do I convert a GeoDataFrame back to a plain DataFrame on purpose?

Drop the geometry column and cast the type: pd.DataFrame(gdf.drop(columns="geometry")). This is useful when you only need the attribute table for a CSV export or a tabular merge.