The 1-in-100-year flood happened twice in ten years

On this page

Problem statement

A flood is mapped as the 1-in-100-year event, and then the river reaches that level twice in a decade. Someone concludes the map is wrong, or that the climate has changed, or that return periods are meaningless. Usually none of those is the reason.

"1-in-100" is a probability, not a schedule: a 1% chance in every year, independent of what happened last year. Over a thirty-year mortgage there is a 26% chance of at least one. And the number itself is an estimate from a short record with a wide interval β€” on 121 years of peaks at Trenton on the Delaware, the 1-in-100 discharge is somewhere between the 1-in-47 and the 1-in-218 level at ordinary confidence.

Quick answer

The arithmetic that answers most of the complaints is one line:

def at_least_one(annual_p, years):
    return 1 - (1 - annual_p) ** years

for years in (1, 10, 30, 50, 100):
    print(f"at least one 1-in-100 event in {years:3} years: {at_least_one(0.01, years):6.1%}")

p10 = 0.1                                     # a 1-in-10 event
two_or_more = 1 - (1 - p10) ** 10 - 10 * p10 * (1 - p10) ** 9
print(f"two or more 1-in-10 events in a decade:      {two_or_more:6.1%}")
print(f"two or more 1-in-100 events in a decade:     "
      f"{1 - 0.99 ** 10 - 10 * 0.01 * 0.99 ** 9:6.2%}")
at least one 1-in-100 event in   1 years:   1.0%
at least one 1-in-100 event in  10 years:   9.6%
at least one 1-in-100 event in  30 years:  26.0%
at least one 1-in-100 event in  50 years:  39.5%
at least one 1-in-100 event in 100 years:  63.4%
two or more 1-in-10 events in a decade:       26.4%
two or more 1-in-100 events in a decade:      0.43%

Two 1-in-10 floods in a decade is the expected outcome, not a surprise. Two 1-in-100 floods in a decade is genuinely unusual β€” it is the case worth investigating.

Probability of at least one 1-in-100 event over windows of one to a hundred years. chance of at least one 1% eventin 1 year1.0%in 10 years9.6%in 30 years26.0% β€” a mortgagein 50 years39.5%in 100 years63.4% β€” a building life050%100%Assuming independent years; clustering makes runs more likely, not less spatialworkflow.io
The 1-in-100 flood over a lifetime: more likely than not.

Restate as a probability, check the record, add an interval

1. Restate the return period as an annual probability

1-in-100 is a 1% annual exceedance probability. Say it that way in captions and legends and most of the misreading disappears; several agencies have stopped printing return periods for exactly this reason.

2. Check the record before blaming the estimate

Count how many years in the gauge record actually exceeded the level. A fitted 1-in-100 that has been exceeded twice in 121 years is behaving as advertised.

3. Put an interval on the estimate

Bootstrap the fit. If the interval spans 1-in-50 to 1-in-200, then an event "at the 1-in-100 level" is within the noise of the estimate, and arguing about which side of it a flood fell on is arguing about the fit.

4. Separate clustering from trend

Floods cluster because weather clusters β€” a wet multi-year period produces several large peaks. A cluster is not evidence of a trend on its own.

5. Test for non-stationarity properly

Fit the halves of the record separately, or fit with a trend term, and compare. At Trenton the earlier half gives a higher 1-in-100 than the later half, which is the opposite of the expected story.

6. Say what the map is for

A 1-in-100 floodplain is a planning boundary, not a prediction of where water will be in any given year. Flooding outside it is expected β€” that is what the 500-year map is for.

Years in which the Delaware at Trenton exceeded its fitted flood levels, 1897 to 2023. 121 years of annual peaks at Trenton19021-in-1019031-in-10019361-in-1019551-in-10019961-in-1020041-in-1020051-in-5020061-in-1020111-in-10Two exceedances of the 1-in-100 against 1.2 expected β€” and three big years running spatialworkflow.io
121 years of peaks: the big ones cluster, and three "1-in-10" floods arrived in three consecutive years.

Two exceedances in one gauge record, examined

Example 1 β€” what the record actually did

import io, urllib.request
import numpy as np, pandas as pd
from scipy.stats import genextreme

def annual_peaks(site):
    url = (f"https://nwis.waterdata.usgs.gov/nwis/peak?site_no={site}"
           "&agency_cd=USGS&format=rdb")
    req = urllib.request.Request(url, headers={"User-Agent": "example/1.0"})
    text = urllib.request.urlopen(req, timeout=90).read().decode()
    rows = [ln for ln in text.splitlines() if not ln.startswith("#")]
    df = pd.read_csv(io.StringIO("\n".join(rows)), sep="\t").iloc[1:]
    df["q"] = pd.to_numeric(df.peak_va, errors="coerce")
    df["year"] = pd.to_datetime(df.peak_dt, errors="coerce", format="mixed").dt.year
    return df.dropna(subset=["q", "year"])

peaks = annual_peaks("01463500")                 # Delaware River at Trenton, NJ
q, year = peaks.q.to_numpy(), peaks.year.astype(int).to_numpy()
params = genextreme.fit(q, 0.1, loc=np.median(q), scale=q.std())
level = lambda T: genextreme.ppf(1 - 1 / T, *params)

print(f"{len(q)} annual peaks, {year.min()}–{year.max()}")
for T in (10, 50, 100):
    hit = year[q >= level(T)]
    print(f"  1-in-{T:<4} = {level(T):8,.0f} cfs   exceeded in {len(hit)} years: "
          f"{', '.join(map(str, hit))}")
print(f"  expected exceedances of the 1-in-100 in {len(q)} years: {len(q) / 100:.1f}")
121 annual peaks, 1897–2023
  1-in-10   =  163,347 cfs   exceeded in 9 years: 1902, 1903, 1936, 1955, 1996, 2004, 2005, 2006, 2011
  1-in-50   =  240,980 cfs   exceeded in 3 years: 1903, 1955, 2005
  1-in-100  =  279,127 cfs   exceeded in 2 years: 1903, 1955
  expected exceedances of the 1-in-100 in 121 years: 1.2

Two exceedances of the 1-in-100 in 121 years, against 1.2 expected. The 1-in-10 was exceeded nine times against 12 expected. Nothing here needs explaining β€” but note 2004, 2005 and 2006, three consecutive years above the 1-in-10 level, which is exactly the pattern that starts the argument.

Example 2 β€” how wide the estimate is

rng = np.random.default_rng(0)
boot = []
for _ in range(400):
    s = rng.choice(q, len(q), replace=True)
    boot.append(genextreme.ppf(0.99, *genextreme.fit(s, 0.1, loc=np.median(s), scale=s.std())))
lo, hi = np.percentile(boot, [10, 90])

period = lambda x: 1 / (1 - genextreme.cdf(x, *params))
print(f"1-in-100 estimate {level(100):,.0f} cfs")
print(f"  bootstrap 10th–90th percentile: {lo:,.0f} – {hi:,.0f} cfs")
print(f"  which are the 1-in-{period(lo):.0f} and 1-in-{period(hi):.0f} levels "
      f"under the central fit")
1-in-100 estimate 279,127 cfs
  bootstrap 10th–90th percentile: 237,360 – 326,079 cfs
  which are the 1-in-47 and 1-in-218 levels under the central fit

The estimate is not a line, it is a band, and the band is wide even on one of the longest records in the country. A flood 10% above or below the mapped level is not evidence about the map.

Example 3 β€” is there a trend, or just a cluster?

for name, sel in [("1897–1959", year < 1960), ("1960–2023", year >= 1960)]:
    s = q[sel]
    p = genextreme.fit(s, 0.1, loc=np.median(s), scale=s.std())
    print(f"{name}: n={sel.sum():3}  median peak {np.median(s):7,.0f}  "
          f"1-in-100 {genextreme.ppf(0.99, *p):9,.0f} cfs")

decades = pd.Series(q, index=(year // 10) * 10).groupby(level=0).max()
print("largest peak per decade:")
print("  " + "  ".join(f"{d}s {v / 1000:5.0f}k" for d, v in decades.items()))
1897–1959: n= 56  median peak 101,750  1-in-100   304,867 cfs
1960–2023: n= 65  median peak  83,300  1-in-100   253,345 cfs
largest peak per decade:
  1890s    73k  1900s   295k  1910s   160k  1920s   154k  1930s   227k  1940s   161k  1950s   329k  1960s   124k  1970s   135k  1980s   152k  1990s   179k  2000s   242k  2010s   195k  2020s   155k

The later half of this record gives a lower 1-in-100 estimate than the earlier half. That does not disprove a changing flood regime anywhere else; it shows that one gauge's split-half comparison is a weak test, dominated by whether a single very large peak fell in one half or the other β€” here 1955, when two hurricanes crossed in a week.

Independence, and why the phrase survives

Why independence is the assumption that gets forgotten

The binomial arithmetic above assumes each year is an independent trial. Real hydrology is not quite independent β€” wet years cluster through soil moisture, snowpack and ocean state β€” which makes runs of large floods more likely than the formula says, not less. So a cluster is even weaker evidence against the estimate than the plain arithmetic suggests.

Why "1-in-100" survives despite being misread

It is compact, it is in the regulations, and the alternative phrasings have their own problems: "1% annual chance" is correct but gets rounded to "rare" in a reader's head, and "base flood" says nothing at all. The practical answer is to print both the probability and what it means over a period of interest β€” 26% in thirty years is the sentence that lands.

Why the estimate is uncertain even with a long record

Fitting a 1% quantile means extrapolating past all but one or two observations. A century of data contains roughly one event near the level being estimated, so the tail is controlled by the assumed distribution shape as much as by the data, and different defensible distributions diverge exactly where the interest is.

The 1-in-100 discharge estimate with its bootstrap band and the return periods the bounds imply. the 1-in-100 flood at Trenton is a band, not a line279,127 cfs237,360326,079= the 1-in-47 level= the 1-in-218 level200k240k280k320k360k10th–90th bootstrap percentile on 121 annual peaks, one of the longest US records spatialworkflow.io
The bootstrap band on a 121-year record spans the 1-in-47 to the 1-in-218 level.

Regulated rivers, ungauged sites and sea level

  • Partial duration series (all peaks over a threshold) give different small return periods than annual maxima; below about 1-in-10 they are not comparable.
  • Regulated rivers: a dam built mid-record breaks stationarity outright, and the pre-dam peaks may need dropping.
  • Historic peaks flagged in the record extend it beyond the systematic gauge period and tighten the tail.
  • Ungauged sites get regional regression estimates with much wider intervals than these.
  • Sea level rise makes coastal return periods genuinely non-stationary on a decadal scale; river flood trends are harder to detect.
  • "1-in-100" for a region is not the same as for a point β€” the chance that somewhere in a large area sees one is much higher.

FAQ

Can a 1-in-100 flood happen twice in ten years?

Yes, with about a 0.4% chance if the estimate is right. Twice in a decade is unusual but not impossible.

What does 1-in-100 actually mean?

A 1% chance of being equalled or exceeded in any given year, with no memory of previous years.

What is the chance over 30 years?

About 26% for a 1-in-100 event, assuming independent years.

Does a cluster of floods mean the climate changed?

Not on its own. Test with split-half fits or a trend term, and expect a weak test from one gauge.

How accurate is a 1-in-100 estimate?

On a 121-year record, the 10th–90th percentile band spans roughly the 1-in-50 to the 1-in-200 level.

Should I still map the 1-in-100 floodplain?

Yes β€” it is the regulatory boundary. Map the 1-in-500 alongside it so readers see what is outside.