Classifying Mixed-Use Overlaps with GeoPandas Overlay
Parcels often straddle more than one land-use designation, and deciding whether a lot counts as single-use or genuinely mixed-use depends on how its area splits across those designations. This guide intersects parcels with a land-use zone layer using a GeoPandas overlay, measures the area share each designation claims within every parcel, then classifies each lot as single-use or mixed-use against a configurable dominance threshold. The output is a per-parcel table that planning staff can sort, filter, and defend during entitlement review.
Prerequisites
Step-by-step
The walkthrough uses a Philadelphia-region dataset and projects to UTM Zone 18N (EPSG:26918), whose unit is the meter, so every area is reported in square meters. Substitute the CRS appropriate to your jurisdiction before running any area math.
Step 1: Project both layers to a metric CRS
Area calculations are only valid in a projected system, so reproject the parcels and the land-use layer to a shared metric frame and repair geometry before intersecting.
import geopandas as gpd
METRIC_CRS = "EPSG:26918" # UTM Zone 18N, units = meters
parcels = gpd.read_file("parcels.gpkg").to_crs(METRIC_CRS)
landuse = gpd.read_file("landuse_zones.gpkg").to_crs(METRIC_CRS)
# Repair invalid rings so the overlay does not raise a topology error
parcels["geometry"] = parcels.geometry.make_valid()
landuse["geometry"] = landuse.geometry.make_valid()
# Record each parcel's full area for later share calculations
parcels["parcel_area"] = parcels.geometry.area
Step 2: Intersect parcels with land-use zones
The overlay splits each parcel wherever a land-use boundary crosses it, producing one row per parcel-and-category fragment. keep_geom_type discards stray points or lines that can appear along shared edges.
# Geometric intersection yields one fragment per parcel/land-use pair
fragments = gpd.overlay(
parcels[["parcel_id", "parcel_area", "geometry"]],
landuse[["use_category", "geometry"]],
how="intersection",
keep_geom_type=True,
)
# Area of each fragment, still in square meters thanks to the metric CRS
fragments["frag_area"] = fragments.geometry.area
Step 3: Compute each category’s area share per parcel
Aggregate the fragment areas by parcel and category, then divide by the parcel area to express every designation as a percentage of the lot.
import pandas as pd
# Sum fragment areas for each parcel/category combination
shares = (
fragments.groupby(["parcel_id", "use_category"])
.agg(cat_area=("frag_area", "sum"), parcel_area=("parcel_area", "first"))
.reset_index()
)
# Share of the parcel occupied by each land-use category
shares["share_pct"] = (shares["cat_area"] / shares["parcel_area"] * 100).round(2)
Step 4: Classify each parcel as single-use or mixed-use
A parcel is single-use when one category clears the dominance threshold; otherwise the mix is meaningful enough to label it mixed-use. Capturing the dominant category and its share keeps the decision transparent.
DOMINANCE_PCT = 85.0 # a lot is single-use if one category exceeds this
# For each parcel, find the largest category share
top = shares.sort_values("share_pct", ascending=False).drop_duplicates("parcel_id")
top["classification"] = (
top["share_pct"].ge(DOMINANCE_PCT).map({True: "SINGLE_USE", False: "MIXED_USE"})
)
result = top[["parcel_id", "use_category", "share_pct", "classification"]]
result = result.rename(columns={"use_category": "dominant_category"})
Verification
Confirm that the shares are internally consistent before publishing the classification. Every parcel’s category shares should sum to roughly 100 percent, allowing small slivers of rounding, and no share should exceed 100.
totals = shares.groupby("parcel_id")["share_pct"].sum()
assert (totals.between(98, 102)).mean() > 0.99 # allow minor sliver loss
# Confirm the working layer is metric so areas are square meters
assert fragments.crs.axis_info[0].unit_name == "metre"
print(result["classification"].value_counts())
Spot-check a handful of parcels flagged as mixed-use against an aerial or the source zoning map. If shares consistently fall short of 100 percent, the land-use layer likely has gaps, which is worth flagging separately.
Common Pitfalls
- Measuring area in a geographic CRS. Computing
geometry.areaon EPSG:4326 returns square degrees, making every share meaningless. Project to a metric CRS first and assert the unit as the verification step does. - Sliver fragments inflating category counts. Tiny overlay slivers along shared boundaries can register spurious categories. Drop fragments below a minimum area, for example
frag_area < 1.0, before aggregating shares. - Gaps in the land-use layer. If zones do not fully tile the study area, parcel shares will not sum to 100 and lots may be misclassified. Validate coverage, or treat the unclassified remainder as its own category.
Frequently Asked Questions
Why use an overlay instead of a spatial join for this task?
A join tells you which zones a parcel touches but not how much of the parcel each one covers. An overlay physically splits the parcel along zone boundaries so you can measure area shares, which is exactly what a mixed-use determination requires. This is the core technique behind land use intersection mapping.
How do I choose the dominance threshold?
The threshold encodes local policy, so set it from the ordinance rather than by intuition. A common convention treats a lot as single-use when one category covers 85 to 90 percent of it, but jurisdictions with fine-grained mixed-use goals may lower that bar. Keep the value in a config file so planners can tune it without editing code.
Can I report every category rather than only the dominant one?
Yes. The per-parcel shares table already holds every category, so you can pivot it to a wide format or emit the full breakdown alongside the classification. Retaining the complete distribution strengthens the audit trail when a determination is challenged.
What happens when a parcel splits evenly across three uses?
No category clears the threshold, so the lot is labeled mixed-use, which is the intended outcome. If you need to distinguish two-way from three-way mixes, count the categories above a minimum share and add that count as a secondary attribute.
Conclusion
Intersecting parcels with land-use zones, measuring each category’s area share, and comparing the dominant share to a configurable threshold produces a defensible mixed-use classification for an entire jurisdiction in one pass. Because the shares table preserves the full distribution, the same output supports downstream products such as density heatmaps from parcel centroids. For deeper coverage of overlay semantics, consult the GeoPandas set-operations guide and the Shapely geometry manual.