Screening Wetland & Floodplain Constraints from NWI and FEMA Data

The scenario: a screening run treats every National Wetlands Inventory polygon and every FEMA flood zone as an exclusion, reports that 38 percent of the study area is unbuildable, and the project is dropped. A wetland scientist then points out that most of the NWI polygons in that county are seasonally flooded agricultural depressions, that Zone X is not a regulated floodplain at all, and that the real constrained fraction is closer to 12 percent. This page is about reading the attribute codes before excluding on them, and it sits under environmental constraint and exclusion screening.

Root-cause analysis

Three assumptions cause the over-exclusion, and each has an under-exclusion twin.

  1. All NWI polygons are jurisdictional wetlands. They are not. NWI is a remote-sensing derived inventory with a Cowardin classification code on every polygon, and the code distinguishes permanently flooded open water from temporarily flooded farmed depressions. Jurisdiction under Section 404 is a legal determination that NWI does not make. Treating every polygon as a hard exclusion over-states the constraint; treating none of them as one under-states it badly.
  2. All FEMA flood zones are equivalent. Zone A and AE are the 100-year floodplain and carry real development restrictions; Zone X is outside it and carries none; the floodway within Zone AE is far more restrictive than the rest of it. A single “in a flood zone” filter merges three different regulatory realities.
  3. The two layers are independent. They overlap heavily by construction — wetlands are where water sits — so summing their areas over-states the combined constraint by the overlap, which is the arithmetic covered in building multi-layer exclusion masks.
Attribute codes turn 38 percent nominal constraint into 12 percent real Two stacked bars. The first, National Wetlands Inventory coverage, divides into palustrine emergent at 9.1 percent, palustrine scrub-shrub at 1.6, palustrine forested at 2.3, open water systems at 1.4 and farmed or temporarily flooded depressions at 3.2. The second, FEMA flood hazard coverage, divides into the regulatory floodway at 2.1 percent, the rest of Zone AE at 11.9, and Zone X at 11.0. Each segment is coloured by its constraint class, and a summary shows hard exclusions at 3.5 percent, permittable at 8.6 and advisory at the remainder. One study area, coded rather than counted National Wetlands Inventory — 17.6% of the study area 9.1% 2.3% 3.2% PEM emergent PFO forested farmed depressions FEMA flood hazard — 25.0% of the study area 2.1% 11.9% 11.0% regulatory floodway Zone AE (rest) Zone X hard exclusion 3.5% of the study area permittable 8.6% — with a 404 path advisory only Zone X and farmed ground The 38% headline was the union of two layers nobody had read the attributes of.

Pre-flight validation

The pre-flight step is a code inventory rather than a geometry check: list the distinct Cowardin codes and FEMA zone codes present in the study area, with their areas, before deciding anything.

python
import geopandas as gpd


def inventory_constraint_codes(
    nwi: gpd.GeoDataFrame,
    fema: gpd.GeoDataFrame,
    study_geom,
    *,
    working_epsg: int,
) -> dict[str, dict[str, float]]:
    """Area by attribute code — the table the exclusion decision should be made from."""
    out: dict[str, dict[str, float]] = {}
    for name, gdf, field in (("nwi", nwi, "ATTRIBUTE"), ("fema", fema, "FLD_ZONE")):
        layer = gdf.to_crs(working_epsg).clip(study_geom)
        if layer.empty:
            out[name] = {}
            continue
        layer["_ha"] = layer.geometry.area / 10_000.0
        # Cowardin codes are hierarchical: the first two characters carry the system
        # and subsystem, which is the level a screening decision is made at.
        key = layer[field].astype(str).str[:2] if name == "nwi" else layer[field].astype(str)
        out[name] = layer.groupby(key)["_ha"].sum().sort_values(ascending=False).to_dict()
    return out

Running that inventory first is what turns “38 percent is wetland” into “PEM covers 9 percent, PFO covers 2 percent, PUB covers 0.4 percent, and the rest is PSS and farmed depressions” — a statement a wetland scientist can act on.

Fix implementation

The classification below is deliberately explicit and deliberately conservative in the right places: open water and permanently flooded systems are hard exclusions, forested and emergent wetlands are permittable, and the floodway is separated from the wider 100-year zone.

python
import geopandas as gpd

# Cowardin system/subsystem prefixes → constraint class.
NWI_CLASS = {
    "L1": "hard",         # lacustrine, limnetic — open water
    "L2": "hard",         # lacustrine, littoral
    "R2": "hard",         # riverine, lower perennial
    "R3": "hard",         # riverine, upper perennial
    "PUB": "hard",        # palustrine unconsolidated bottom — open water
    "PAB": "hard",        # palustrine aquatic bed
    "PFO": "permittable", # palustrine forested
    "PSS": "permittable", # palustrine scrub-shrub
    "PEM": "permittable", # palustrine emergent
}

FEMA_CLASS = {
    "AE": "permittable",  # 100-year with base flood elevation
    "A": "permittable",   # 100-year, no BFE determined
    "AO": "permittable",
    "AH": "permittable",
    "VE": "hard",         # coastal high hazard
    "X": "advisory",      # outside the 100-year zone
    "0.2 PCT ANNUAL CHANCE FLOOD HAZARD": "advisory",
}


def classify_wetland_flood_constraints(
    nwi: gpd.GeoDataFrame,
    fema: gpd.GeoDataFrame,
    *,
    working_epsg: int,
    floodway_field: str = "ZONE_SUBTY",
) -> dict[str, gpd.GeoDataFrame]:
    """Split both sources into hard, permittable and advisory layers."""
    nwi_p = nwi.to_crs(working_epsg).copy()
    nwi_p["_key"] = nwi_p["ATTRIBUTE"].astype(str).str[:3]
    nwi_p["constraint_class"] = (
        nwi_p["_key"].map(NWI_CLASS).fillna(nwi_p["_key"].str[:2].map(NWI_CLASS)).fillna("advisory")
    )

    fema_p = fema.to_crs(working_epsg).copy()
    fema_p["constraint_class"] = fema_p["FLD_ZONE"].astype(str).map(FEMA_CLASS).fillna("advisory")
    # The regulatory floodway is materially more restrictive than the rest of Zone AE.
    if floodway_field in fema_p.columns:
        is_floodway = fema_p[floodway_field].astype(str).str.contains("FLOODWAY", case=False, na=False)
        fema_p.loc[is_floodway, "constraint_class"] = "hard"

    combined = gpd.GeoDataFrame(
        gpd.pd.concat([nwi_p[["constraint_class", "geometry"]],
                       fema_p[["constraint_class", "geometry"]]], ignore_index=True),
        crs=nwi_p.crs,
    )
    return {cls: combined[combined["constraint_class"] == cls].copy()
            for cls in ("hard", "permittable", "advisory")}
The overlap between wetlands and floodplain, drawn and counted A Venn-style diagram over the study area with two overlapping regions: National Wetlands Inventory coverage at 17.6 percent and FEMA flood hazard coverage at 25.0 percent, sharing an overlap of 11.2 percentage points along the river valley. Beside it, three figures: the sum of the two coverages at 42.6 percent, the union at 31.4 percent, and the difference of 11.2 points labelled as the double count. A note explains that the overlap is expected, because both agencies map the same valley for different reasons. Two layers, one valley, 11.2 points counted twice NWI 17.6% FEMA 25.0% 11.2% both sum of the two 42.6% union of the two 31.4% counted twice 11.2% The overlap is expected: wetlands are where water sits, and the 100-year floodplain is where water goes. Two agencies mapped the same valley for different reasons, and neither is wrong.

Fallback routing and performance tuning

  • Download by county, not by state. Both NWI and the FEMA National Flood Hazard Layer publish county extracts; a state download is an order of magnitude larger and is clipped away immediately.
  • Cache the raw extract with its vintage. FEMA revises flood maps continuously through Letters of Map Revision, and NWI is updated on an irregular state-by-state schedule. The vintage is part of the answer.
  • Dissolve by class before unioning. Both layers carry many small polygons; dissolving each class first reduces the union input by an order of magnitude with no change to the result.
  • Expect unmapped areas. Parts of the country have no detailed FEMA study and appear as an absence rather than as Zone X. Treat unmapped as unknown and flag it, never as unconstrained.
  • Keep the codes on the output. A downstream reviewer will ask which code produced a given exclusion, and re-deriving it costs a rerun.

Downstream validation

python
def assert_constraint_split(layers: dict[str, gpd.GeoDataFrame], study_ha: float) -> None:
    """CI gate: the split has to be exhaustive, bounded and non-empty where it matters."""
    total = sum(float(g.geometry.area.sum()) / 10_000.0 for g in layers.values())
    assert total <= study_ha * 3, "classified area exceeds three times the study area — layers unclipped"
    assert not layers["hard"].empty or not layers["permittable"].empty, (
        "no constrained area at all — check the attribute field names against the source vintage"
    )
    for cls, gdf in layers.items():
        assert gdf.geometry.is_valid.all(), f"{cls}: invalid geometry survived classification"
        assert gdf["constraint_class"].eq(cls).all(), f"{cls}: mislabelled rows in the split"
Where each source code ends up, and what the project can do about it A routing diagram from source codes to constraint classes to project consequences. Open water codes L1, L2, R2, R3 and PUB and the FEMA regulatory floodway route to the hard class and to a buildable figure that no permit changes. Palustrine emergent, scrub-shrub and forested codes and FEMA Zones A, AE, AO and AH route to the permittable class and to a second figure conditional on a Section 404 individual permit or a local floodplain development permit. Zone X and farmed or temporarily flooded depressions route to the advisory class and into the weighted suitability score. From attribute code to what the project can actually do L1 · L2 · R2 · R3 · PUB FEMA regulatory floodway hard no permit resolves this PEM · PSS · PFO FEMA A · AE · AO · AH permittable second figure, with a 404 or FDP path Zone X farmed / temporarily flooded advisory weighted score, never a mask

Frequently asked questions

Does an NWI polygon mean the Corps will assert jurisdiction?

No. NWI is an inventory, not a determination, and the two disagree in both directions: NWI misses small features below its mapping threshold and includes features that are not jurisdictional. A screening model should treat NWI as evidence of likely constraint and a delineation as the answer, which is exactly why these polygons belong in the permittable class rather than the hard one.

Should Zone X be excluded at all?

Not as a constraint. Zone X is outside the 100-year floodplain and carries no federal development restriction, so excluding it removes buildable land for no legal reason. It can reasonably carry a small advisory weight in a scoring model, because insurers and lenders sometimes ask about the 0.2-percent-annual-chance zone, but that is a cost signal rather than an exclusion.

How do I handle a study area with no FEMA data?

Flag it and stop, rather than assuming absence means safety. Unmapped areas are common in rural counties and are the case where the screening model is most likely to be wrong, because the flood risk is unknown rather than absent. A field survey or a state-level hazard layer is the substitute, and the output should say which was used.

Why do the wetland and floodplain layers disagree along rivers?

Because they map different things: NWI maps vegetation and hydrology as observed, and FEMA maps modelled flood extent at a given recurrence interval. Along a river the two follow the same valley and diverge in detail, which is exactly why they must be unioned rather than summed — and why the overlap between them is one of the numbers worth publishing.

Are state wetland programmes stricter than the federal one?

Often, and the difference is what a screening model most easily misses. Several states regulate isolated wetlands that fall outside federal jurisdiction, and a few regulate buffers around wetlands rather than only the wetland itself. The consequence for the pipeline is that the classification table belongs in per-state configuration rather than in code, because the same Cowardin code can be permittable in one state and effectively hard in its neighbour.

How much does a wetland delineation change the screening figure?

Enough to be worth commissioning early on a shortlisted site. Field delineation typically moves the regulated wetland area by 20 to 40 percent relative to NWI in either direction, because NWI misses small features and includes some that are not jurisdictional. The screening figure is for ranking sites; the delineation is for designing on one.

Should the floodway be treated as buildable for solar?

No, and it is the one flood-zone answer that is nearly unambiguous. The regulatory floodway is the channel that must convey the base flood without increasing flood height, so development there is restricted far more tightly than in the wider Zone AE — which is exactly why it belongs in the hard class while the rest of AE sits in the permittable one.