Most of the data work covered elsewhere on this site treats location as just
another column — a city name, a ZIP code, a country string. GeoPandas
exists for the moment that stops being enough: when "is this point inside
that area" or "how far apart are these two things" is itself the question a
row has to answer.
This article covers the two ideas everything else in this module builds on:
what a GeoDataFrame actually is, and how a spatial join matches rows by
geometry instead of by equal values.
A GeoDataFrame is a DataFrame with two extras#
import geopandas as gpd
stores = gpd.read_file("stores.geojson")territories = gpd.read_file("territories.geojson")gpd.read_file returns a
GeoDataFrame
— everything a Pandas DataFrame already gives you, plus a geometry column
holding actual shapes (points, lines, polygons), and a coordinate reference
system attached to the whole layer. Once geometry is in a normal column,
grouping, filtering, and aggregating it is
ordinary Pandas —
GeoPandas only adds the geometric operations Pandas has no concept of.
What a coordinate reference system is#
A coordinate reference system (CRS)
defines what the numbers in a geometry actually mean: which point on Earth is
(0, 0), what unit the coordinates are in, and how a curved planet gets
flattened onto a plane. EPSG:4326 — plain latitude/longitude in degrees —
and a local projected system measured in meters are both common, and a
coordinate pair means something completely different under each one.
Two layers can only be compared or joined spatially once they agree on a CRS.
Loaded from different sources, stores and territories might not:
stores = stores.to_crs(territories.crs)to_crs reprojects every geometry in stores into territories's system.
Which layer you reproject into which does not matter, as long as both end up
the same — what matters is doing it before anything that compares their
geometries, because a mismatch does not always raise an error. Depending on
the library version it can warn, or silently return wrong matches.
Joining by geometry instead of by key#
A spatial join answers a geometric question the way merge answers a
value-equality question — instead of matching rows where a key column is
equal, it matches rows where a geometric predicate holds between two
geometries:
matched = gpd.sjoin( stores, territories[["territory_id", "geometry"]], how="left", predicate="within")predicate="within" keeps every store whose point falls inside a territory
polygon and attaches that territory's id to the row. how="left" keeps
stores that fall outside every polygon too — their territory id comes back
null instead of the row disappearing, which is exactly what you want for
spotting bad data later: a null match after a spatial join usually means a
gap in the polygons or a CRS mismatch, not a store that genuinely belongs
nowhere.
What happens after the join is not new
Once matched has a territory_id column, summarizing it — counts per
territory, sums, averages — is the same groupby().agg() pattern used
throughout the Python
section. Geometry only does the matching; everything downstream of it is
ordinary tabular work.
Other predicates#
within is one of several
binary predicates
GeoPandas exposes through Shapely. The common ones:
| Predicate | True when |
|---|---|
within | the left geometry is entirely inside the right one |
contains | the reverse of within |
intersects | the geometries share any point at all (touching counts) |
crosses | the geometries cross without one containing the other |
within and intersects cover the large majority of point-in-polygon work;
reach for the others only when the relationship genuinely calls for them.
Common mistakes#
Joining before reprojecting#
sjoin on two layers in different coordinate systems can fail loudly, warn,
or silently produce nonsense matches, depending on the GeoPandas version.
Always call to_crs first and treat an all-null result as a hint to check
the CRS before anything else.
Assuming a null match means bad data#
With how="left", an unmatched row is expected, not an error — it is how you
find points outside every polygon. Switching to how="inner" to "clean up"
the output silently deletes exactly the rows worth investigating.
Reaching for intersects by default#
intersects also matches geometries that merely touch at an edge. For "is
this point inside this area," within is the more precise choice; use
intersects when a boundary touch should genuinely count as a match.
Further reading#
- GeoPandas: Managing Projections
— coordinate reference systems and
to_crsin depth. geopandas.sjoin— the full predicate list and join-type options.- Shapely binary predicates
— where
within,intersects, and the rest are actually defined. geopandas.GeoDataFrame— the full API surface beyond what this article covers.- Spatial reference system — the general concept a CRS implements.
- Data Transformations with Pandas
— the
groupby/aggwork that typically follows a spatial join.