County-level damaging weather event frequency shows no broad-based national trend over 2005-2024 — outcomes are highly county-specific
NOAA NCEI Storm Events Database (disasters.storm_events), county-attributed events with recorded property damage, 2005-2024
Summary
At the county grain, the frequency of damaging weather events (NOAA NCEI Storm Events, county-attributed records with recorded property damage) shows no broad-based national trend over 2005-2024: the national count is essentially flat/noisy (regression slope -55 events/year, r2=0.02 — statistically negligible), and of 2,280 counties with at least 10 years of data, 1,195 show a declining trend and 1,084 show an increasing one (median slope -0.01 events/year, i.e. flat). Change is highly local rather than a nationwide climate signal in this measure: some counties (Maricopa AZ, Lauderdale AL, Allegheny PA) show a strong 20-year increase of 2-4 additional damaging events per year, while others (several Iowa counties, Cook IL, Charleston/Berkeley SC) show an equally strong decline. This contrasts with NOAA's separate, coarser billion-dollar-disaster metric, which shows a clear national increase — the difference is explained below.
Data and method
Source: disasters.storm_events (NOAA NCEI Storm Events Database), 2004-2026 declared and observed coverage, 1.4M rows. Restricted to cz_type='C' (county-coded records; the database also logs NWS-zone and marine records that cannot be mapped to a single county) and damage_property > 0 as the operational definition of "damaging." Window used: full calendar years 2005-2024 (2004 and 2026 are partial in this table). For each county, computed a per-year event count, then a linear trend (regr_slope of count on year) across all years with data (2280 counties had 10+ of the 20 years), and separately the 2005-2009 vs 2020-2024 five-year average change for counties with a substantial early-period base (≥3 events/yr average).
Data-quality caveats
- county_fips is null on 41% of all storm-event rows (zone- or marine-coded records) — excluded by design (cz_type='C' filter) since they cannot be attributed to one county. This exclusion share is fairly stable year to year (roughly 35-45%), so it should not itself be driving the flat trend, but it does mean roughly 4 in 10 logged severe-weather episodes nationally carry no usable county key in this dataset.
- Reporting practices, not just weather, drive this series. The Storm Events Database depends on NWS forecast-office logging, Skywarn spotters, emergency managers, and public reports — county-level participation and documentation rigor have changed unevenly over 20 years, and a county's apparent 'increase' or 'decrease' can reflect a change in how diligently damage was logged locally as much as a change in actual weather. This is very plausibly why individual counties (e.g., Polk County IA falling from 66/yr to 4/yr; Maricopa County AZ rising from 22/yr to 75/yr) show swings this large — a 90%+ county-level change over 20 years is more consistent with a shift in local reporting/damage-logging practice than an actual 20x change in storm frequency.
- Different metric than NOAA's well-known 'billion-dollar disasters' trend. NOAA's billion-dollar-disaster count (a much higher damage threshold, tallied nationally per event, not per county) rose clearly: 6.7/year in the 2000s to 13.1/year in the 2010s to 23/year in 2020-2024 (NOAA/Climate Central). That metric counts large, often multi-county events against a fixed real-dollar damage threshold and is a genuinely different measurement from a raw per-county count of any damage-flagged local storm report, which is dominated by the much larger number of small, routine severe-weather entries. The two are not in tension; they answer different questions (rare catastrophic-cost events nationally vs. all locally-logged damaging weather at county grain).
- Sample sizes are moderate at the tail (n=2,280 counties with a fitted slope; ranking lists above show only the 10 most extreme per side, each a single-county estimate not tested for statistical significance or robustness).
Every query behind this report
8 warehouse calls ran in this session, in order. Each is reproducible against the same snapshot.
query — 1 rows — 2331 ms
WITH county_year AS (
SELECT county_fips, CAST("year" AS INTEGER) AS yr, COUNT(*) AS n_events
FROM disasters.storm_events
WHERE cz_type = 'C' AND county_fips IS NOT NULL
AND CAST("year" AS INTEGER) BETWEEN 2005 AND 2024
AND damage_property > 0
GROUP BY county_fips, CAST("year" AS INTEGER)
)
SELECT COUNT(DISTINCT county_fips) AS n_counties, SUM(n_events) AS total_events, MIN(yr) AS min_yr, MAX(yr) AS max_yr
FROM county_yearquery — 20 rows — 2510 ms
SELECT CAST("year" AS INTEGER) AS yr, COUNT(*) AS n_events, COUNT(DISTINCT county_fips) AS n_counties
FROM disasters.storm_events
WHERE cz_type='C' AND county_fips IS NOT NULL AND damage_property>0
AND CAST("year" AS INTEGER) BETWEEN 2005 AND 2024
GROUP BY CAST("year" AS INTEGER)
ORDER BY yrquery — 20 rows — 2820 ms
SELECT CAST("year" AS INTEGER) AS yr,
COUNT(*) AS total_rows,
SUM(CASE WHEN county_fips IS NULL THEN 1 ELSE 0 END) AS null_fips,
SUM(CASE WHEN cz_type='C' THEN 1 ELSE 0 END) AS cz_c,
SUM(CASE WHEN cz_type='C' AND county_fips IS NOT NULL AND damage_property>0 THEN 1 ELSE 0 END) AS damaging_c
FROM disasters.storm_events
WHERE CAST("year" AS INTEGER) BETWEEN 2005 AND 2024
GROUP BY CAST("year" AS INTEGER)
ORDER BY yrquery — 20 rows — 2678 ms
SELECT CAST("year" AS INTEGER) AS yr, COUNT(*) AS n_events
FROM disasters.storm_events
WHERE cz_type='C' AND county_fips IS NOT NULL
AND CAST("year" AS INTEGER) BETWEEN 2005 AND 2024
GROUP BY CAST("year" AS INTEGER)
ORDER BY yrquery — 1 rows — 3453 ms
SELECT regr_slope(n_events, yr) AS national_slope, regr_r2(n_events, yr) AS r2, count(*) AS n
FROM (
SELECT CAST("year" AS INTEGER) AS yr, COUNT(*) AS n_events
FROM disasters.storm_events
WHERE cz_type='C' AND county_fips IS NOT NULL AND damage_property>0
AND CAST("year" AS INTEGER) BETWEEN 2005 AND 2024
GROUP BY CAST("year" AS INTEGER)
) tquery — 15 rows — 18044 ms
WITH cy AS (
SELECT county_fips, CAST("year" AS INTEGER) AS yr, COUNT(*) AS n_events
FROM disasters.storm_events
WHERE cz_type='C' AND county_fips IS NOT NULL AND damage_property>0
AND CAST("year" AS INTEGER) BETWEEN 2005 AND 2024
GROUP BY county_fips, CAST("year" AS INTEGER)
),
early AS (
SELECT county_fips, SUM(n_events)*1.0/5 AS avg_early
FROM cy WHERE yr BETWEEN 2005 AND 2009 GROUP BY county_fips
),
late AS (
SELECT county_fips, SUM(n_events)*1.0/5 AS avg_late
FROM cy WHERE yr BETWEEN 2020 AND 2024 GROUP BY county_fips
),
slopes AS (
SELECT county_fips, regr_slope(n_events, yr) AS slope, COUNT(*) AS n_years
FROM cy GROUP BY county_fips
HAVING COUNT(*) >= 10
),
cnames AS (
SELECT county_fips, MAX(county_name) AS county_name, MAX(state_fips) AS state_fips
FROM geo.counties GROUP BY county_fips
)
SELECT cn.county_name, sr.state_abbr, e.county_fips,
ROUND(e.avg_early,2) AS avg_early_2005_09,
ROUND(l.avg_late,2) AS avg_late_2020_24,
ROUND(l.avg_late - e.avg_early,2) AS abs_change,
ROUND(100.0*(l.avg_late-e.avg_early)/e.avg_early,1) AS pct_change,
ROUND(s.slope,3) AS trend_slope_per_year
FROM early e
JOIN late l ON e.county_fips = l.county_fips
JOIN slopes s ON e.county_fips = s.county_fips
LEFT JOIN cnames cn ON cn.county_fips = e.county_fips
LEFT JOIN geo.state_ref sr ON sr.state_fips = LEFT(e.county_fips,2)
WHERE e.avg_early >= 3
ORDER BY s.slope DESC
LIMIT 15query — 10 rows — 18153 ms
WITH cy AS (
SELECT county_fips, CAST("year" AS INTEGER) AS yr, COUNT(*) AS n_events
FROM disasters.storm_events
WHERE cz_type='C' AND county_fips IS NOT NULL AND damage_property>0
AND CAST("year" AS INTEGER) BETWEEN 2005 AND 2024
GROUP BY county_fips, CAST("year" AS INTEGER)
),
early AS (
SELECT county_fips, SUM(n_events)*1.0/5 AS avg_early
FROM cy WHERE yr BETWEEN 2005 AND 2009 GROUP BY county_fips
),
late AS (
SELECT county_fips, SUM(n_events)*1.0/5 AS avg_late
FROM cy WHERE yr BETWEEN 2020 AND 2024 GROUP BY county_fips
),
slopes AS (
SELECT county_fips, regr_slope(n_events, yr) AS slope, COUNT(*) AS n_years
FROM cy GROUP BY county_fips
HAVING COUNT(*) >= 10
),
cnames AS (
SELECT county_fips, MAX(county_name) AS county_name FROM geo.counties GROUP BY county_fips
)
SELECT cn.county_name, sr.state_abbr, e.county_fips,
ROUND(e.avg_early,2) AS avg_early_2005_09,
ROUND(l.avg_late,2) AS avg_late_2020_24,
ROUND(100.0*(l.avg_late-e.avg_early)/e.avg_early,1) AS pct_change,
ROUND(s.slope,3) AS trend_slope_per_year
FROM early e
JOIN late l ON e.county_fips = l.county_fips
JOIN slopes s ON e.county_fips = s.county_fips
LEFT JOIN cnames cn ON cn.county_fips = e.county_fips
LEFT JOIN geo.state_ref sr ON sr.state_fips = LEFT(e.county_fips,2)
WHERE e.avg_early >= 3
ORDER BY s.slope ASC
FETCH FIRST 10 ROWS ONLYquery — 1 rows — 6222 ms
WITH cy AS (
SELECT county_fips, CAST("year" AS INTEGER) AS yr, COUNT(*) AS n_events
FROM disasters.storm_events
WHERE cz_type='C' AND county_fips IS NOT NULL AND damage_property>0
AND CAST("year" AS INTEGER) BETWEEN 2005 AND 2024
GROUP BY county_fips, CAST("year" AS INTEGER)
),
slopes AS (
SELECT county_fips, regr_slope(n_events, yr) AS slope, COUNT(*) AS n_years, AVG(n_events) AS avg_n
FROM cy GROUP BY county_fips
HAVING COUNT(*) >= 10
)
SELECT
COUNT(*) AS n_counties_analyzed,
SUM(CASE WHEN slope > 0 THEN 1 ELSE 0 END) AS n_increasing,
SUM(CASE WHEN slope < 0 THEN 1 ELSE 0 END) AS n_decreasing,
SUM(CASE WHEN slope = 0 THEN 1 ELSE 0 END) AS n_flat,
ROUND(median(slope),4) AS median_slope,
ROUND(avg(slope),4) AS mean_slope
FROM slopesSources
- disasters.storm_events national yearly county-attributed damaging event counts, 2005-2024
Show tool call
query(sql="SELECT CAST(\"year\" AS INTEGER) AS yr, COUNT(*) AS n_events FROM disasters.storm_events WHERE cz_type='C' AND county_fips IS NOT NULL AND damage_property>0 AND CAST(\"year\" AS INTEGER) BETWEEN 2005 AND 2024 GROUP BY CAST(\"year\" AS INTEGER) ORDER BY yr") - County-level trend slopes (regr_slope) and up/down split, 2280 counties, 2005-2024
Show tool call
query(sql="WITH cy AS (SELECT county_fips, CAST(year AS INTEGER) AS yr, COUNT(*) AS n_events FROM disasters.storm_events WHERE cz_type='C' AND county_fips IS NOT NULL AND damage_property>0 AND CAST(year AS INTEGER) BETWEEN 2005 AND 2024 GROUP BY county_fips, CAST(year AS INTEGER)), slopes AS (SELECT county_fips, regr_slope(n_events, yr) AS slope, COUNT(*) AS n_years FROM cy GROUP BY county_fips HAVING COUNT(*)>=10) SELECT COUNT(*), SUM(CASE WHEN slope>0 THEN 1 ELSE 0 END), SUM(CASE WHEN slope<0 THEN 1 ELSE 0 END), median(slope), avg(slope) FROM slopes") - NOAA Climate.gov / Climate Central — U.S. billion-dollar weather disaster frequency by decade
- Climate Central — U.S. Billion-Dollar Disasters 1980-2024