GeoPandas Essentials¶
pandas + geometry: a
GeoDataFrameis a DataFrame with ageometrycolumn 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
- Core Syntax
- Common Operations
- Practical Examples
- Common Patterns
- Best Practices
- Common Mistakes
- Troubleshooting
- Related Cheat Sheets
- Official Documentation
Installation / Setup¶
pip install geopandas matplotlib # or: uv add geopandas matplotlib
# optional extras
pip install contextily mapclassify # basemap tiles + choropleth classification
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
sjoinwith an explicitpredicate("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 |
Related Cheat Sheets¶
- folium.md — interactive leaflet maps from the same data
- matplotlib.md — styling and exports for GeoPandas plots
- pandas.md — the DataFrame API GeoPandas extends
- Data Visualization category index · Home · Index
Official Documentation¶
- GeoPandas documentation
- GeoPandas: data structures
- Mapping with GeoPandas
- Shapely manual
- contextily basemaps
Last Updated¶
2026-09-06 — Initial version.