Technical writing

The Natural Hazards Picture: From NWS Warning to NOAA Storm to FEMA Declaration

· 12 min read· AI Analytics
Natural HazardsNOAANWSUSGSData Engineering

A tornado does not arrive as a single record. First the National Weather Service issues a watch, then a warning, with a lead time measured in minutes. Then the storm strikes, and NOAA writes down, county by county, what it destroyed and whom it killed. Then, if the damage is large enough, the governor asks and the President signs, and FEMA records a federal disaster declaration. Four federal datasets capture those four moments— the warning, the event, the impact, and the response—and the work of seeing the whole hazard is the work of joining them on place and time.

This article covers how to assemble the federal natural-hazards picture from four public, key-free datasets: the National Weather Service feed of real-time watches and warnings, NOAA's Storm Events Database of what actually struck, the USGS earthquake catalog, and FEMA's disaster declarations. It walks through what each dataset is and the agency behind it; the hazard lifecycle the four datasets trace from warning to declaration; the two hazard families—severe weather and seismic—and why they enter the data differently; the join keys, which come down to place (state and county FIPS, or earthquake coordinates mapped to counties) and time; the policy questions the assembled data answers, from warning lead time and casualties to the trend in billion-dollar disasters; a Python workflow that pulls storm events and earthquakes for a region and period, lines them up against the FEMA declarations, and measures how often damaging events became federal disasters; and the caveats—granularity mismatches, the Stafford Act's political economy, and the limits of damage estimates—that govern honest analysis.

Four datasets, one hazard

The four datasets exist because no single federal program owns “a natural hazard.” The hazard is a sequence of events that crosses agency boundaries, and each agency records the slice it is responsible for. The National Weather Service (NWS), an arm of NOAA, issues the forecasts, watches, and warnings before and during a weather event—the anticipatory record. NOAA's National Centers for Environmental Information (NCEI) compiles the Storm Events Database, the after-the-fact ledger of tornadoes, floods, hurricanes, hail, wind, winter storms, and dozens of other event types, with county-level deaths, injuries, and damage estimates—the impact record. The United States Geological Survey (USGS) runs the comprehensive earthquake catalog, logging seismic events worldwide by magnitude, depth, and location—a separate hazard family with its own physics and its own record. And the Federal Emergency Management Agency (FEMA) maintains the disaster declarations, the legal record of which events drew a Presidential major-disaster or emergency declaration and thereby unlocked federal assistance—the response record. Held together, these four datasets cover the full hazard lifecycle that any one of them sees only a fragment of.

In our database the four are stored as the tables nws_alerts, noaa_storm_events, usgs_earthquakes, and fema_disasters. Three of them share a common skeleton—a geography and a date—and the earthquakes carry coordinates and a timestamp that resolve to a geography. The columns that matter for joining them are the place keys and the time keys; the substantive payload differs by table. A simplified view of the load-bearing columns across the four:

-- nws_alerts (api.weather.gov active alerts):
event            -- "Tornado Warning", "Flood Watch", etc.
sent / effective -- when the alert was issued / took effect
expires / ends   -- when the alert lapses
state / fips     -- geocoded area(s): state + county FIPS (SAME codes)
severity         -- Extreme / Severe / Moderate / Minor

-- noaa_storm_events (NCEI bulk CSV, one row per county-event):
event_type       -- Tornado, Flash Flood, Hurricane, Hail, ...
begin_date_time  -- when the event began (and end_date_time)
state_fips + cz_fips -> 5-digit county FIPS
deaths_direct / injuries_direct
damage_property / damage_crops -- e.g. "2.50K", "1.2M", "3.0B"

-- usgs_earthquakes (FDSN GeoJSON):
mag              -- preferred magnitude
time             -- event time (epoch ms)
latitude / longitude / depth -- map lat/lon -> county FIPS

-- fema_disasters (OpenFEMA, one row per declared county):
disaster_number / declaration_type -- DR (major) / EM (emergency)
incident_type    -- "Tornado", "Flood", "Earthquake", ...
declaration_date / incident_begin_date / incident_end_date
fips_state_code + fips_county_code -> same 5-digit county FIPS

The pattern to notice is that place and time recur in every table. The NWS alerts, the storm events, and the FEMA declarations all carry a state and a county FIPS code—the same federal geographic identifier—and all carry dates. The earthquakes are the exception: they carry a latitude, a longitude, and a timestamp rather than a county, so the analyst must map each quake's coordinates to the county it falls in before it can join to the others. Once that mapping is done, all four tables speak the same two languages—FIPS and dates—and the join becomes tractable.

The hazard lifecycle: warning, event, impact, response

The reason to join these tables rather than read them separately is that they trace a lifecycle, and the analytically interesting questions live in the transitions between stages. The lifecycle runs in one direction for weather: the NWS issues a watch when conditions are favorable for a hazard and then, as the threat materializes, a warning that the hazard is imminent or occurring. The interval between the warning and the moment the event begins is the lead time, the single most consequential number in the warning enterprise, because lead time is what gives people the chance to take shelter. The event then strikes; NOAA's Storm Events Database records it after the fact, with the deaths, injuries, and damage it caused. And if the impact is severe enough to overwhelm state and local capacity, the event escalates to a federal disaster declaration in the FEMA record.

Each stage is captured by a different agency with a different mandate, which is exactly why the integration is valuable and difficult at once. The NWS is forward-looking and probabilistic—it is in the business of forecasting and alerting, and its record is a stream of warnings, many of which precede events that turn out to be minor or that miss the warned area entirely. The Storm Events Database is backward-looking and forensic—it is the authoritative account of what happened, assembled from NWS field reports, emergency managers, the media, and damage surveys, and it is the only one of the four that carries casualty and damage estimates at the event level. The FEMA declarations are legal-administrative—a declaration is not a measurement of severity but a political and procedural act, the product of a governor's request and a Presidential decision, and it determines whether federal money flows. Lining the three weather stages up for the same county and the same time window is what lets an analyst ask whether the warning reached the event, whether the event's impact justified the response, and where the chain broke.

Two hazard families: severe weather and seismic

The four datasets straddle two hazard families that behave very differently in the data. The first is severe weather, which dominates three of the four tables. Weather hazards are warnable—the NWS can see most of them coming, from minutes ahead for a tornado to days ahead for a hurricane—and they are recorded by political geography, because their impacts are reported county by county. A single severe-weather episode produces a cascade of records: a watch, multiple warnings as the storm moves across counties, a row in Storm Events for each county the storm struck, and, if it escalates, a FEMA declaration listing each declared county. This is why the FIPS code is the natural spine for weather: every weather stage is already expressed in counties.

The second family is seismic, and earthquakes break the weather mold in two ways. First, they are essentially unwarnable in the conventional sense—there is no analogue to a tornado watch issued hours ahead; earthquake early warning, where it exists, buys seconds, not minutes—so the NWS-warning stage of the lifecycle has no seismic counterpart, and the USGS catalog stands closer to the “event” stage than to the “warning” stage. Second, earthquakes are recorded by physical geography, not political geography: the USGS logs a quake at a latitude and longitude, because the earth does not respect county lines, and the same event can be felt across many counties at once. To bring an earthquake into the FIPS-keyed world of the other three datasets—to ask which counties an earthquake affected and whether any of them received a FEMA earthquake declaration— the analyst must perform a spatial join, mapping the quake's coordinates (and, ideally, a felt-area radius derived from its magnitude) onto the county polygons. That coordinate-to-county step is the one piece of genuine geospatial work the integration requires, and it is what lets a magnitude in the USGS catalog meet a declaration in the FEMA record.

The join keys: place and time

Everything in this integration comes down to two keys—place and time—and the discipline of getting them to match across four datasets that were never designed to be joined. The place key is the FIPS code. The Federal Information Processing Standards county code is a five-digit identifier—two digits for the state, three for the county—that the Census Bureau and the rest of the federal government use to name counties unambiguously. The Storm Events Database, the FEMA declarations, and the geocoded NWS alerts all carry it, though they assemble it differently: Storm Events stores the state FIPS and the county/zone FIPS in separate columns that must be concatenated and zero-padded; FEMA stores a state code and a county code that likewise combine into the five-digit key; the NWS alerts carry geocodes that resolve to the same county FIPS. The earthquakes carry no FIPS at all and must be assigned one by a point-in-polygon lookup against county boundaries.

The time key is a date or, better, a window. A naive equality join on dates almost never works, because the four datasets stamp time at different points in the lifecycle and with different precision. An NWS warning carries an issuance time and an expiration time. A storm event carries a begin and an end timestamp. A FEMA declaration carries a declaration date that postdates the incident—often by days or weeks—plus an incident begin and end date that bracket the underlying event. An earthquake carries a single instant. The correct join is therefore an interval overlap: two records refer to the same hazard if they share a county and their time windows intersect within a tolerance. A storm event in Hardin County beginning on a Tuesday matches a FEMA declaration whose incident window covers that Tuesday and that lists Hardin County—even though the declaration date itself may be three weeks later. Treating time as a window rather than a point is the difference between a join that captures the real hazard chain and one that silently drops most of it.

A second subtlety in the place key deserves emphasis. The NWS issues some products by public forecast zone rather than by county, and the Storm Events Database mixes county-based and zone-based events (flood and tornado events tend to be county-based; many marine, fire-weather, and winter-weather events are zone-based). Zones and counties do not perfectly coincide. For most land-based severe-weather analysis the county FIPS is the right and sufficient spine, but an analyst doing zone-sensitive work must keep a county-to-zone crosswalk on hand and be explicit about which geography a given event was recorded in—another reminder that “join on FIPS” hides real bookkeeping underneath.

The questions the assembled data answers

Assembled, the four datasets answer the questions that sit behind hazard preparedness and climate-resilience policy—questions that no single dataset can answer, because each requires lining up two or more stages of the lifecycle.

How does warning lead time relate to casualties?Joining NWS warnings to the Storm Events record for the same county and time window pairs the warning's lead time with the deaths and injuries the event caused. Across many events this is the empirical heart of the warning enterprise: it tests whether longer lead times are associated with fewer casualties, how the relationship varies by hazard type and time of day, and where warnings arrived too late or not at all. It is the analysis that justifies investment in the forecasting infrastructure that produces the warnings.

Which counties bear the most repeated damage?Aggregating Storm Events damage and casualties by county FIPS over decades surfaces the chronically struck places—the tornado-alley counties, the flood-prone river basins, the hurricane-exposed coasts—and, joined to FEMA, reveals which of them are repeat-declaration counties drawing federal assistance again and again. That repeat-loss geography is exactly what mitigation funding and buyout programs are meant to target. How has the count of billion-dollar disasters trended?NOAA's billion-dollar-disaster framing—events whose total damage crosses one billion dollars—draws on the Storm Events impact record alongside insurance, FEMA, and other federal loss data, and tracking that count over time is among the most-cited indicators in the climate-resilience debate. And the question that ties the whole chain together: what share of damaging events actually receive a federal declaration? Joining damaging Storm Events to the FEMA declarations measures the declaration rate—how often a costly event crossed the threshold from a local disaster into a federally recognized one—which is both a measure of severity and a window into the Stafford Act's thresholds and politics.

Joining across the four tables

The practical assembly proceeds in a fixed order, because each step depends on the previous one having normalized its keys. First, normalize the FIPS codes. Build the five-digit county FIPS in each table from its component columns, zero-padding the state to two digits and the county to three, and for the earthquakes, perform the point-in-polygon spatial join that assigns each quake a county. After this step all four tables carry a comparable fips column.

Second, align the time windows. Parse every date and timestamp into a comparable representation, and decide on the join semantics: an interval overlap with a tolerance, so that an NWS warning matches the storm event it preceded, the storm event matches the FEMA declaration whose incident window covers it, and an earthquake matches a declaration whose incident window contains the quake's instant. Third, join stage by stage rather than all at once: pair warnings with events (warning-to-impact), then events with declarations (impact-to-response), keeping each join's match rate visible so that the inevitable non-matches—warnings with no corresponding event, damaging events with no declaration—are themselves part of the analysis rather than silent losses. Fourth, aggregate to the policy unit: roll the joined records up by county and year to answer the repeated-damage and declaration-rate questions, or keep them at event grain to study lead time and casualties. The order matters because a mistake in the FIPS normalization or the time semantics propagates into every downstream metric, and the most common way these analyses go wrong is a silent key mismatch that drops most of the matches and produces a declaration rate or a lead-time relationship that is an artifact of the join rather than a fact about hazards.

Python workflow: events, quakes, and declarations

The script below pulls NOAA Storm Events for a year from NCEI's bulk CSV archive, pulls earthquakes for a period from the USGS FDSN service, pulls FEMA disaster declarations for the year from OpenFEMA, normalizes the county FIPS across the storm events and the declarations, and computes the central cross-dataset metric: of the damaging county-events—those above a damage threshold—what share fell in a county that received a FEMA declaration that year. No API key is required for any of the four sources. The earthquake pull is included to show the GeoJSON shape and the coordinates that a production pipeline would map to counties via a point-in-polygon join; the declaration-rate metric uses the FIPS-keyed weather datasets, where the join is exact.

import requests, pandas as pd
from datetime import datetime

# Four federal hazard sources, all public and key-free:
#   1. NOAA Storm Events (NCEI bulk CSV)  -- what struck, with damage/deaths
#   2. USGS earthquake catalog (FDSN API) -- quakes by magnitude/location
#   3. FEMA disaster declarations (OpenFEMA) -- which events became federal
#   4. NWS active alerts (api.weather.gov) -- live watches/warnings
# The goal: line damaging events up against the FEMA declarations and
# measure how often a damaging event became a federal major disaster.

NCEI = "https://www.ncei.noaa.gov/pub/data/swdi/stormevents/csvfiles"
USGS = "https://earthquake.usgs.gov/fdsnws/event/1/query"
FEMA = "https://www.fema.gov/api/open/v2/DisasterDeclarationsSummaries"


def storm_events(year):
    # NCEI publishes one gzipped CSV of "details" per year; the filename
    # carries a version stamp, so resolve it from the directory listing.
    idx = requests.get(f"{NCEI}/", timeout=120).text
    needle = f"StormEvents_details-ftp_v1.0_d{year}_"
    name = next(line.split('"')[1] for line in idx.split("href=")[1:]
                if needle in line)
    url = f"{NCEI}/{name}"
    df = pd.read_csv(url, compression="gzip", low_memory=False)
    # Parse damage strings like "2.50K" / "1.2M" into dollars.
    def dollars(s):
        if not isinstance(s, str) or not s:
            return 0.0
        mult = {"K": 1e3, "M": 1e6, "B": 1e9}.get(s[-1].upper(), 1.0)
        try:
            return float(s.rstrip("KMBkmb")) * mult
        except ValueError:
            return 0.0
    df["damage_usd"] = (df["DAMAGE_PROPERTY"].map(dollars)
                        + df["DAMAGE_CROPS"].map(dollars))
    # Build a 5-digit county FIPS from state + county FIPS components.
    df["fips"] = (df["STATE_FIPS"].astype("Int64").astype(str).str.zfill(2)
                  + df["CZ_FIPS"].astype("Int64").astype(str).str.zfill(3))
    return df


def earthquakes(start, end, minmag=4.0):
    # USGS FDSN returns GeoJSON; one feature per event with [lon, lat, depth].
    r = requests.get(USGS, params={
        "format": "geojson", "starttime": start, "endtime": end,
        "minmagnitude": minmag, "limit": 20000}, timeout=120)
    r.raise_for_status()
    feats = r.json()["features"]
    return pd.DataFrame([{
        "mag": f["properties"]["mag"],
        "place": f["properties"]["place"],
        "time": pd.to_datetime(f["properties"]["time"], unit="ms"),
        "lon": f["geometry"]["coordinates"][0],
        "lat": f["geometry"]["coordinates"][1],
    } for f in feats])


def fema_declarations(year):
    # OpenFEMA: one row per declared county. fipsStateCode + fipsCountyCode
    # give the same 5-digit county key as Storm Events.
    out, top, skip = [], 1000, 0
    while True:
        r = requests.get(FEMA, params={
            "$filter": f"fyDeclared eq {year}", "$top": top, "$skip": skip},
            timeout=120)
        rows = r.json().get("DisasterDeclarationsSummaries", [])
        if not rows:
            break
        out += rows
        skip += top
    df = pd.DataFrame(out)
    df["fips"] = (df["fipsStateCode"].astype(str).str.zfill(2)
                  + df["fipsCountyCode"].astype(str).str.zfill(3))
    return df


def declaration_rate(year, min_damage=1e6):
    se = storm_events(year)
    fd = fema_declarations(year)
    declared = set(fd["fips"].dropna())
    damaging = se[se["damage_usd"] >= min_damage]
    hit = damaging["fips"].isin(declared)
    print(f"{year}: {len(damaging):,} county-events over "
          f"${min_damage:,.0f} in damage; "
          f"{hit.mean():.1%} fell in a FEMA-declared county")
    return se, fd


declaration_rate(2023)
# eq = earthquakes("2023-01-01", "2024-01-01", minmag=4.5)

Two refinements turn this first pass into a defensible analysis. First, the declaration-rate calculation here matches a damaging event to any declaration in the same county that year, which over-counts: a county can receive a flood declaration in spring and suffer an unrelated hail event in autumn, and the script would credit the hail event to the flood declaration. A rigorous version must respect both the time window—matching the event only to a declaration whose incident period covers it—and the incident type, so that a tornado event is tested against tornado or severe-storm declarations rather than against a wildfire declaration that happens to share the county. The columns to do both are present in the pulled data; the script leaves the temporal and type refinement as the obvious next step. Second, for multi-year or national work, the NCEI bulk files (one CSV per year, decades deep) and the OpenFEMA full-dataset downloads are far more efficient than re-fetching, and the earthquake-to-county spatial join should be done once against an authoritative county boundary file rather than approximated.

Limitations and analytical caveats

The four datasets are the most complete public record of US natural hazards available, but the act of joining them introduces hazards of its own, and several structural limitations govern what the assembled data can honestly support.

The granularity does not perfectly match. Storm Events mixes county-based and zone-based records; NWS products are issued sometimes by county and sometimes by forecast zone; FEMA declares by county; earthquakes are points. Every join therefore involves some reconciliation, and naive FIPS equality silently drops the zone-based records and the unmapped earthquakes. A declaration rate computed without accounting for the zone-versus-county distinction, or a lead-time analysis that quietly excludes zone-issued warnings, measures a biased subset of the hazard universe and should be reported as such.

A FEMA declaration is a legal-political act, not a severity measurement. Whether an event becomes a federal disaster depends on the Stafford Act's thresholds, the state's own capacity, the governor's decision to request, and a Presidential decision—a process with real political economy. Two events of similar physical impact can differ in declaration status because of state size, fiscal capacity, or timing. Using the declaration rate as a proxy for “how bad the event was” conflates severity with the politics and procedure of the federal response; the declaration rate is most honestly read as a measure of that response, with severity supplied independently by the Storm Events damage and casualty figures.

Damage estimates are estimates. The property and crop damage figures in Storm Events are field estimates—assembled from emergency managers, insurers, the media, and damage surveys, with varying methods and varying completeness— not audited losses, and they are recorded in nominal dollars that must be inflation-adjusted before any trend comparison. The reporting completeness of both casualties and damage has also changed over the decades as the database matured, so apparent long-run increases can partly reflect better reporting rather than worse hazards. Any billion-dollar-disaster or repeated-damage analysis must adjust for inflation, and ideally for exposure growth, before it can separate a real trend from an accounting artifact.

The NWS feed is live, and the others are retrospective.The active-alerts feed at api.weather.gov shows what is in effect now; the warning archive needed for a lead-time study is a separate, larger product, and an analyst who reaches for the live feed expecting a historical record will get only the present moment. The Storm Events and FEMA records, by contrast, lag the events they describe—Storm Events is finalized months after an event, and declarations postdate incidents—so the most recent weeks are systematically incomplete in every table except the live one. Held with these caveats in mind, the four tables together—nws_alerts, noaa_storm_events, usgs_earthquakes, and fema_disasters—let an analyst follow a hazard from the warning that preceded it, through the damage it caused, to the federal response it did or did not draw, the one view in which the warning, the impact, and the response finally line up.

Related writing

NOAA Storm Events Database: The Federal Record Behind 50 Years of US Weather Disasters — The impact stage of the hazard lifecycle examined on its own terms, with the county-level deaths, injuries, and damage estimates that this integration joins to the warnings that preceded the events and the declarations that followed them.

FEMA Disaster Declarations: The Federal Database Behind 70 Years of US Natural Disasters — The response stage in depth, including the Stafford Act thresholds and the governor-to-President process that decide which damaging events cross from local disasters into federally declared ones—the political economy behind the declaration rate computed here.

Seismic record: using the USGS earthquake catalog to analyze fault risk and induced seismicity — The seismic hazard family treated in full, including the magnitude, depth, and coordinate fields that must be mapped to county FIPS by a spatial join before an earthquake can meet a FEMA declaration in the integrated view.