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)
One State Plane zone, four EPSG codesCalifornia zone 3 published under two datums and two linear units, showing how four authority codes describe the same zone with different measurement behaviour.DatumLinear unitPicking it by mistakeEPSG:26943NAD83metreCorrect if your code speaksmetresEPSG:2227NAD83US survey footCorrect if your code speaksfeetEPSG:26743NAD27US survey footCoordinates off by tens ofmetresEPSG:6419NAD83(2011)metreCentimetre-level differencefrom EPSG:26943
Same zone, four codes. The datum decides where the coordinates are; the unit decides what every later number means.

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")

The three assertions above are worth keeping as a reusable check rather than a one-off script, because they are exactly the checks that stop being run once the pipeline “works”. Wiring them into the ingest step means a supplier who quietly changes their export frame is caught by a failing run rather than by an anomalous report three weeks later.

Area error by the wrong choiceRelative error in computed parcel area caused by a unit swap, an adjacent zone, and an unprojected geographic frame.Adjacent State Plane zone0.4% area errorNAD27 code instead of NAD832% area errorUS survey feet read as metres976% area errorIllustrative magnitudes: the unit swap is a factor of 10.76, the zone error grows with distance from the central meridian.
The adjacent-zone error is the one that survives review: small enough to look plausible, large enough to change a floor-area ratio.

It is worth knowing the shape of each failure, because they are distinguishable from the numbers alone. A feet-for-metres swap scales every area by about 10.76, which is unmistakable once you look for it. Choosing an adjacent zone leaves areas almost right, off by a fraction of a percent that grows with distance from the zone’s central meridian — small enough to survive review and large enough to matter on a tight FAR calculation. Leaving the layer in a geographic frame produces areas in square degrees, numbers so small they are usually spotted immediately. The dangerous middle case is the adjacent zone, which is why the acreage comparison in step 4 is not optional.

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.

What if my study area spans two State Plane zones?

Process each zone in its own frame and reconcile at the boundary, or adopt a single custom projection defined for the whole area and record its full definition in the run manifest. What does not work is choosing one zone and accepting the distortion across the other, because the error grows with distance from the central meridian and will be largest exactly where the two jurisdictions meet — the parcels most likely to be contested.

Should I reproject once at ingest or per analysis step?

Once, at ingest. Repeated reprojection accumulates floating-point error, costs time proportional to vertex count, and creates the opportunity for two steps to work in different frames without noticing. Establish the frame during ingestion, assert it at every stage boundary, and treat any later transformation as a presentation step that produces an output copy rather than a new working layer.

Choosing the zone and the unitThe zone is chosen by which one covers the study area; the unit is chosen by which one the ordinance is written in.Does one State Plane zonecover the whole studyarea?two zonesSplit by zone or define a project projectionnever stretch one zone across the boundaryone zoneUse that zonethen pick the unit the ordinance speaksRecord the EPSG code and unit in the pipeline configuration
Two independent choices, made once and written into configuration where a reviewer can see both.

How do I know the county’s published acreage is the right thing to compare against?

Treat it as a strong cross-check rather than ground truth. Assessor acreages are themselves computed from a parcel fabric with its own history, and they are often rounded or carried forward from an older survey. Agreement to within a few thousandths of an acre confirms your zone and unit are right; a systematic difference across many parcels points at your configuration, while scattered differences on individual parcels usually point at their boundary history.

Part of: CRS standardization and datum management