Skip to content

GeoPandas Essentials

pandas + geometry: a GeoDataFrame is a DataFrame with a geometry column of shapely shapes. Read, transform, join, and plot real-world vector data (points, lines, polygons) with the pandas API you already know.

Table of Contents

Installation / Setup

pip install geopandas matplotlib    # or: uv add geopandas matplotlib
# optional extras
pip install contextily mapclassify # basemap tiles + choropleth classification
import geopandas as gpd
import matplotlib.pyplot as plt

GeoPandas installs shapely, pyproj, and pandas as dependencies. Vector file I/O uses pyogrio (default) or Fiona.

Core Syntax

The GeoDataFrame

gdf = gpd.read_file("data.geojson")     # also .shp (zipped ok), .gpkg, GeoJSON URLs
gdf.head()
gdf.geometry                            # the active geometry column
gdf.crs                                 # coordinate reference system
gdf.total_bounds                        # (minx, miny, maxx, maxy)

Building from coordinates

import geopandas as gpd
from shapely.geometry import Point

gdf = gpd.GeoDataFrame(
    df,                                  # normal columns (name, value, ...)
    geometry=[Point(x, y) for x, y in zip(df["lng"], df["lat"])],
    crs="EPSG:4326",                     # WGS84 lat/lng
)

Plotting

gdf.plot()                                  # basic map of the geometry
gdf.plot(figsize=(10, 8), color="lightblue", edgecolor="black")

# Value-filled choropleth
gdf.plot(column="value", cmap="viridis", legend=True,
         legend_kwds={"label": "value", "shrink": 0.6})
# with classification (needs mapclassify)
gdf.plot(column="value", scheme="quantiles", k=5, cmap="YlOrRd", legend=True)

Matplotlib powers GeoPandas plots — everything from matplotlib.md applies (ax.set_title, saving, subplots).

CRS & projection

gdf.crs                                  # e.g. EPSG:4326
gdf = gdf.to_crs("EPSG:3857")            # Web Mercator (meters)
gdf = gdf.to_crs(epsg=3857)              # same thing
# Metric CRS (e.g. 326xx UTM zones) enables true distance/buffer math

Common Operations

Spatial joins & overlays

# Join points to the polygon that contains them
joined = gpd.sjoin(points_gdf, polygons_gdf, how="left", predicate="within")
# predicates: intersects, within, contains, touches, crosses, overlaps

# Geometric overlay (polygons in/out)
merged = gpd.overlay(poly_a, poly_b, how="intersection")   # union, difference, symmetric_difference

Geometric computations (shapely)

gdf["area_km2"] = gdf.geometry.area / 1e6      # area in map units (use metric CRS!)
gdf["length_km"] = gdf.geometry.length / 1000
buffered = gdf.geometry.buffer(100)            # 100 m buffer (metric CRS)
distances = gdf.geometry.distance(other_geom)
centroids = gdf.geometry.centroid
gdf["bbox"] = gdf.geometry.envelope
dissolved = gdf.dissolve(by="country")         # merge polygons by attribute

Filtering by location

# GeoPandas uses .cx for coordinate slicing
subset = gdf.cx[minx:maxx, miny:maxy]

# contains points within a region
mask = gdf.contains(point)                     # or gdf.intersects(...)

Basemaps (contextily)

import contextily as ctx

ax = gdf.plot(figsize=(10, 8), alpha=0.6)
ctx.add_basemap(ax, crs=gdf.crs.to_string(), source=ctx.providers.CartoDB.Positron)
ax.set_axis_off()

Practical Examples

1. Map store points with value-scaled markers

gdf = gpd.GeoDataFrame(
    stores,
    geometry=gpd.points_from_xy(stores["lng"], stores["lat"]),
    crs="EPSG:4326",
).to_crs("EPSG:3857")                       # metric for realistic marker sizing

ax = gdf.plot(column="revenue", cmap="plasma", legend=True,
              markersize=gdf["revenue"] / gdf["revenue"].max() * 200,
              figsize=(9, 6), alpha=0.8)
ax.set_title("Store revenue")
plt.axis("off")
plt.savefig("stores_map.png", dpi=200, bbox_inches="tight")

2. Choropleth of a metric per region

regions = gpd.read_file("regions.geojson")           # polygons
regions = regions.merge(metrics, on="region_id")     # plain pandas merge

fig, ax = plt.subplots(figsize=(10, 8))
regions.plot(column="metric", scheme="fisher_jenks", k=5,
             cmap="YlOrRd", legend=True,
             legend_kwds={"title": "metric"}, ax=ax)
ax.set_title("metric by region"); ax.set_axis_off()
fig.savefig("choropleth.png", dpi=200, bbox_inches="tight")

3. Count points inside polygons

points = gpd.read_file("events.geojson")             # point events
joined = gpd.sjoin(points, regions, how="left", predicate="within")
counts = joined.groupby("region_id").size().rename("n_events")
regions = regions.merge(counts, left_on="region_id", right_index=True, how="left")

ax = regions.plot(column="n_events", cmap="Blues", legend=True,
                  missing_kwds={"color": "lightgray"}, figsize=(10, 7))
ax.set_axis_off()

Common Patterns

# Consistent default styling for a notebook
import matplotlib.pyplot as plt
plt.rcParams.update({"figure.dpi": 110})
gpd.options.display_precision = 4

# Keep GeoDataFrame vs plain DataFrame straight
attrs = gdf.drop(columns="geometry")     # plain DataFrame
gdf2 = gpd.GeoDataFrame(attrs, geometry=geoms, crs=gdf.crs)

# Web tile-friendly export
gdf_merc = gdf.to_crs("EPSG:3857")

Best Practices

  • Always know your CRS (gdf.crs) — mixing projected and unprojected data silently breaks distances and joins.
  • Do distance/buffer math in a metric CRS (UTM or local projection), not EPSG:4326 degrees.
  • Use sjoin with an explicit predicate ("within", "intersects") — default "intersects" surprises people counting points.
  • Merge attributes before plotting — GeoPandas plots the column you give it; joins happen with pandas/merge.
  • Add a basemap with contextily for context; turn axes off for clean map exports.
  • For interactive maps use folium.md; for heavy operations (GDAL-level) check 30-Other-Tools or dedicated GIS tools.

Common Mistakes

Mistake Consequence Correct approach
Forgetting .to_crs before measuring Distances in degrees, nonsense to_crs metric, then compute
Joining region_id without matching dtype All-NaN merge column astype(str)/astype(int) on both keys
Plotting two GeoDataFrames with different CRS Misaligned layers Align CRS first
read_file of zipped shp failing Wrong path/extension Use zip://path.shp.zip or GeoPackage
Points all "in the sea" lat/lng swapped when building points_from_xy(x=lng, y=lat)
Counting points with default sjoin predicate Double-counting edge points predicate="within" for strict containment

Troubleshooting

Symptom Cause Fix
fiona.errors.DriverError Missing driver / bad file Install pyogrio or Fiona; check file integrity
pyproj.exceptions.CRSError Invalid CRS string Use epsg=4326 or a valid authority string
AttributeError: 'GeoDataFrame' object has no attribute 'sjoin' Old geopandas pip install -U geopandas
Plot shows single blob Geometry column invalid/mixed gdf.geometry.is_valid, gdf.explode()
scheme= ignored mapclassify not installed pip install mapclassify
Huge file slow Reading whole shapefile Filter with bbox= or rows= in read_file

Official Documentation

Last Updated

2026-09-06 — Initial version.