Reprojecting Parcel Layers to State Plane in GeoPandas

State Plane coordinate systems give municipal analysts the low-distortion, locally accurate frame that zoning measurements demand, but choosing the wrong zone or the wrong unit variant quietly corrupts every area and setback that follows. This guide walks through selecting the correct State Plane EPSG code for a jurisdiction, reprojecting parcels with to_crs, and confirming that units and computed areas match the county record before the data feeds any compliance check. It is a focused application of the CRS standardization and datum management module, narrowed to the practical case of parcel fabric.

Prerequisites

Step-by-step

Step 1: Identify the correct State Plane EPSG code

State Plane zones come in datum and unit variants, and picking the right one is the decision that governs accuracy. A single county sits in exactly one zone, but that zone is published as both a metre realisation and a US-survey-foot realisation. For California zone 3 under NAD83, EPSG:26943 is the metre variant and EPSG:2227 is the US-survey-foot variant. Inspect the candidate before committing to it.

from pyproj import CRS

# California zone 3, NAD83 — metre and US survey foot realisations.
metric = CRS.from_epsg(26943)
foot = CRS.from_epsg(2227)

for crs in (metric, foot):
    axis = crs.axis_info[0]
    # unit_name distinguishes 'metre' from 'US survey foot' — the critical check.
    print(crs.to_epsg(), axis.unit_name, "| projected:", crs.is_projected)

Step 2: Reproject the parcel layer

With the target chosen, reprojection is a single vectorized call. Confirm the source CRS is present first, because to_crs cannot transform a layer whose frame is undefined. Selecting a projected State Plane target here is what makes the later area computation valid, honouring the house rule that distance and area work only in a linear CRS.

import geopandas as gpd

TARGET_CRS = "EPSG:26943"  # NAD83 / California zone 3 (metres)

parcels = gpd.read_file("parcels.gpkg")
if parcels.crs is None:
    raise ValueError("Assign the true source CRS before reprojecting.")

# Full datum-aware transformation into the authoritative State Plane frame.
parcels = parcels.to_crs(TARGET_CRS)
print("Now in:", parcels.crs.name)

Step 3: Validate the linear unit

The most damaging State Plane error is a unit mismatch: treating survey feet as metres inflates every distance by a factor of about 3.28. Read the unit directly from the active CRS rather than assuming it, and convert deliberately when an ordinance is written in different units than your frame.

unit = parcels.crs.axis_info[0].unit_name
print("Working unit:", unit)

# One US survey foot is 1200/3937 metres exactly; use this for deliberate conversion.
US_FOOT_IN_M = 1200 / 3937
if unit == "metre":
    # Example: express a 7.62 m computed distance in survey feet for a foot-based code.
    setback_ft = 7.62 / US_FOOT_IN_M
    print(f"7.62 m == {setback_ft:.2f} US survey feet")

Step 4: Verify computed areas against a known reference

Area is the acid test. Compute parcel areas in the projected frame, convert to the reporting unit, and compare against a trusted figure. A close match confirms both the zone and the unit are correct.

# Area comes back in the CRS unit squared (square metres here).
parcels["area_m2"] = parcels.geometry.area
parcels["area_acres"] = parcels["area_m2"] / 4046.8564224  # m^2 per acre

sample = parcels.iloc[0]
print(f"Parcel {sample.get('apn', 'n/a')}: {sample['area_acres']:.3f} acres")

Verification

Confirm three things before releasing the reprojected layer downstream. First, the active CRS is projected and in the expected unit: parcels.crs.is_projected is True and parcels.crs.axis_info[0].unit_name matches your intent. Second, feature counts are unchanged by reprojection; to_crs never adds or drops rows, so a different length signals an unrelated bug. Third, a sampled area agrees with the county record to within survey tolerance.

assert parcels.crs.is_projected, "Target CRS is not projected."
assert len(parcels) == len(gpd.read_file("parcels.gpkg")), "Row count changed."
delta = abs(parcels["area_acres"].iloc[0] - 0.172)  # county-reported acreage
print("Area agrees" if delta < 0.005 else f"Investigate: off by {delta:.3f} ac")

Common Pitfalls

  • Confusing feet and metres. EPSG:2227 and EPSG:26943 describe the same zone in different units. A silent swap scales every measurement by roughly 3.28, which passes casual inspection but fails audit. Always read unit_name from the CRS.
  • Choosing an adjacent zone. Parcels near a zone boundary still belong to their assigned zone; borrowing a neighbour’s zone introduces distortion that grows with distance from the central meridian.
  • Reprojecting a layer with an undefined CRS. Assigning a frame with set_crs is not the same as transforming with to_crs. Using one for the other either mislabels coordinates or moves them incorrectly, a distinction detailed in the sibling handling datum shifts from NAD83 to WGS84 guide.

Frequently Asked Questions

How do I find the right State Plane zone for my jurisdiction?

Consult an authoritative State Plane zone map or the EPSG registry filtered by state, then confirm the county falls entirely inside one zone. Most counties map to a single zone; a few large or oddly shaped counties are split, in which case use the zone that covers your specific study area. Verify by reprojecting a test parcel and checking that its area matches the county record.

Should I standardize on the metre variant or the US survey foot variant?

Match the unit your regulatory code speaks. If ordinances state setbacks in feet, working in the survey-foot realisation avoids conversion at every rule check and reduces rounding error. If you integrate with metric national datasets, the metre variant is cleaner. Whichever you pick, declare it once and enforce it everywhere, as recommended in the parent CRS standardization and datum management module.

Why did my parcel areas come out wildly wrong after reprojecting?

The usual cause is a unit mismatch or an undefined source CRS that was assumed rather than assigned. Check that the source layer declared its true frame before transformation, and read the target CRS unit with axis_info. An area that is off by a factor near 10.76 points squarely at a feet-versus-metres error.

Does reprojecting to State Plane change the number of features?

No. A coordinate transformation moves vertices but never adds or removes rows. If your feature count changes, the cause is elsewhere, such as an earlier filter, a failed read, or a geometry repair step that dropped empty geometries after transformation.