geopandas
GeoPandas brings vector geographic data into the pandas model. A GeoDataFrame is a DataFrame with an active geometry column, a coordinate reference system, file and database readers, spatial joins, overlays, projections, and plotting. Shapely performs geometry work, pyproj handles projections, and pyogrio handles most file I/O. It is the natural choice when analysis already looks like pandas and locations are points, lines, or polygons rather than raster pixels.
Install GeoPandas when vector GIS work belongs naturally in a pandas workflow. Do not treat it as a raster tool, a distributed engine, or a substitute for understanding coordinate systems.
Use it if
- Your analysis combines ordinary pandas columns with point, line, or polygon geometry
- You need spatial joins, overlays, clipping, dissolves, or coordinate transformations in a Python notebook or batch job
- You exchange common GIS files or GeoParquet and want one DataFrame-oriented API
- You want quick static maps through Matplotlib or interactive exploration without adopting a desktop GIS
- Your primary data is raster imagery or gridded climate data; GeoPandas is a vector library, so Rasterio, rioxarray, or xarray is the better center of the stack
- Your dataset does not fit comfortably in memory; GeoDataFrame follows pandas' eager in-memory model, while DuckDB spatial, PostGIS, or Dask-GeoPandas is a better fit for larger-than-memory work
- You need geodesic distance or area without managing projections; the README states that geometry operations are Cartesian, so calculations in a geographic CRS can be wrong or misleading
- You need a dependency-light install in a constrained image; the README lists pandas, Shapely, pyogrio, and pyproj and warns that their low-level geospatial libraries can be challenging to install
- You expect every binary spatial operation to reject mismatched coordinate systems; the README says like coordinates are not enforced, so CRS discipline remains your responsibility
Setup reality
A current wheel install is usually as simple as `pip install geopandas`, but the simple command hides a compiled geospatial stack. GeoPandas 1.1.4 requires Python 3.10 or newer plus pandas 2+, Shapely 2+, pyproj, and pyogrio. Wheels cover common platforms; uncommon architectures, source installs, or tightly pinned environments can expose GEOS, PROJ, and GDAL compatibility problems, which is why the project still recommends conda for difficult installations. Plotting is optional and needs Matplotlib; interactive `.explore()` work adds Folium, mapclassify, and xyzservices. Database I/O adds SQLAlchemy, a driver such as psycopg, and often GeoAlchemy2. The first analytical surprise is CRS rather than installation: loading a file may set a CRS, but constructing geometries often does not. `set_crs()` labels existing coordinates and does not transform them; `to_crs()` performs the transformation. Area, distance, buffering, overlay, and joins are Cartesian, so longitude and latitude data should normally be projected first. Large overlay and join operations can also create many rows and substantial memory pressure. Treat CRS checks, geometry validity, and output row counts as production validation, not notebook polish.
Patterns
Read selected columns from a vector fileread-vector-file
import geopandas as gpd
gdf = gpd.read_file(
'data/parcels.gpkg',
layer='parcels',
columns=['parcel_id', 'owner', 'geometry'],
)
print(gdf.crs)Check `crs` immediately. A missing or incorrectly labelled CRS makes later distance, area, and join results unreliable.
Create point geometry from longitude and latitudecreate-geodataframe
import pandas as pd
import geopandas as gpd
df = pd.DataFrame({'city': ['Oslo'], 'lon': [10.7522], 'lat': [59.9139]})
gdf = gpd.GeoDataFrame(
df,
geometry=gpd.points_from_xy(df.lon, df.lat),
crs='EPSG:4326',
)`points_from_xy` expects x then y, which means longitude then latitude for EPSG:4326.
Project geometries before metric calculationstransform-crs
projected = gdf.to_crs(gdf.estimate_utm_crs())
projected['area_m2'] = projected.geometry.area
projected['buffer'] = projected.geometry.buffer(250)`set_crs()` only labels coordinates. Use `to_crs()` to change them, and avoid area or buffer calculations directly in longitude and latitude.
Attach polygon attributes to pointsspatial-join
joined = gpd.sjoin(
points,
districts[['district_id', 'geometry']],
how='left',
predicate='within',
)
joined = joined.drop(columns='index_right')Both inputs must use compatible coordinate systems. A point on a polygon boundary is not `within`; choose `intersects` if boundary matches count.
Find the nearest feature with a distance capnearest-join
result = gpd.sjoin_nearest(
addresses.to_crs('EPSG:3857'),
stations.to_crs('EPSG:3857'),
how='left',
max_distance=5000,
distance_col='distance_m',
)Nearest distances are Cartesian. Project both frames first, and use a suitable local CRS when Web Mercator distortion is unacceptable.
Clip features to an area of interestclip-features
roads_in_city = gpd.clip(roads.to_crs(city.crs), city)
roads_in_city = roads_in_city.explode(index_parts=False).reset_index(drop=True)Clipping can change geometry types or produce multipart results, so inspect validity and types before writing a strict output format.
Intersect two polygon layersoverlay-layers
zones = zones.to_crs(parcels.crs)
intersections = gpd.overlay(
parcels[['parcel_id', 'geometry']],
zones[['zone', 'geometry']],
how='intersection',
keep_geom_type=True,
)Overlay may multiply rows when one feature meets several features, and invalid polygons can cause surprising fragments.
Merge features by a grouping columndissolve-polygons
regions = counties.dissolve(
by='region',
aggfunc={'population': 'sum', 'income': 'mean'},
as_index=False,
)Dissolve unions geometry as well as aggregating attributes; choose aggregation functions explicitly for every retained non-geometry column.
Detect and repair invalid geometryrepair-geometry
invalid = gdf.loc[~gdf.geometry.is_valid]
print(invalid[['geometry']])
gdf.loc[~gdf.geometry.is_valid, 'geometry'] = (
gdf.loc[~gdf.geometry.is_valid, 'geometry'].make_valid()
)Repairs can change a Polygon into a MultiPolygon or GeometryCollection. Validate the output type before relying on a downstream schema.
Write GeoParquet for efficient reusewrite-geoparquet
gdf.to_parquet(
'output/parcels.parquet',
index=False,
compression='zstd',
)
round_trip = gpd.read_parquet('output/parcels.parquet')Parquet support requires PyArrow. GeoParquet preserves geometry metadata better than exporting coordinates as ordinary table columns.
Read a spatial SQL queryquery-postgis
from sqlalchemy import create_engine, text
engine = create_engine('postgresql+psycopg://user:password@host/dbname')
sql = text('SELECT id, name, geom FROM places WHERE region_id = :region')
gdf = gpd.read_postgis(sql, engine, geom_col='geom', params={'region': 7})Database support needs SQLAlchemy and a matching driver. Keep credentials outside source code and filter in SQL to avoid loading an entire table.
Make a classified choroplethplot-map
ax = districts.plot(
column='population',
scheme='quantiles',
k=5,
legend=True,
missing_kwds={'color': 'lightgrey'},
)
ax.set_axis_off()Static plotting needs Matplotlib, and classification schemes such as `quantiles` need mapclassify.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| shapely | PyPI | You only need geometry creation and predicates without pandas tables or GIS file handling |
| pyogrio | PyPI | You mainly need fast vector file I/O and can work with Arrow, NumPy, or a thinner data layer |
| duckdb | PyPI | You want SQL, a spatial extension, and larger-than-memory query execution instead of eager DataFrames |