Prof. Marc Los Huertos • Pomona College • Environmental Analysis
Concepts Packages Settings Download Empirical Method Gumbel Fit Log-Pearson III Split Periods Helper Functions Summary
readNWISpeak()
from the dataRetrieval package.
What this activity covers. On April 5, 2025, dozens of USGS gauges across the central United States registered their highest discharge on record. Flood frequency analysis (FFA) places events like that in a probabilistic context: given the historical record at a gauge, how rare is this peak? This activity builds the complete FFA pipeline — from raw annual peak data through empirical plotting positions, Gumbel distribution fitting, and a two-period stationarity comparison — using the
dataRetrievalpackage and base-R graphics.
A hydrograph shows discharge over time during and after a storm. Its key features are:
Flood frequency analysis focuses exclusively on the annual peak — the single highest discharge recorded in each water year (October 1 – September 30).
A T-year flood is a discharge that has a 1/T probability of being equalled or exceeded in any single year. The 100-year flood is not a flood that happens once a century; it is a flood that has a 1% annual probability of occurrence.
⚙️ Annual exceedance probability
P(Q ≥ Q_T in
any one year) = 1 / T
P(Q ≥ Q_T at least once in n years) = 1 −
(1 − 1/T)^n
For a 100-year flood over a 30-year mortgage: 1 − (1
− 0.01)^30 = 26%.
That is the probability a 100-year flood affects a
property during a typical mortgage term.
Classical FFA assumes floods are stationary — drawn from a distribution whose parameters do not change over time. Milly et al. (2008) famously declared “stationarity is dead” given evidence that land use change, reservoir operations, and climate change all alter flood statistics. Comparing frequency curves fitted to early vs. recent sub-periods of a record is a first diagnostic for non-stationarity.
📌 Annual peak vs. daily mean discharge. Activity 10
used daily mean discharge from readNWISdv(). This activity
uses instantaneous annual peak discharge from
readNWISpeak(). Peak instantaneous discharge can be 2–5×
larger than the daily mean for the same event because the daily mean
averages the entire 24-hour period including the rising and falling
limbs. Always use annual peaks — not daily means — for flood frequency
analysis.
# install.packages(c("dataRetrieval", "tidyverse", "knitr", "kableExtra", "e1071"))
library(dataRetrieval) # USGS NWIS data access
library(tidyverse) # dplyr, ggplot2
library(knitr) # tables
library(kableExtra) # styled HTML tables
library(e1071) # skewness() for Log-Pearson III
All user-defined parameters are collected here. Change only these values to analyse a different station.
# ── Station settings ──────────────────────────────────────────────────────────
mysite <- "07061000" # Current River at Van Buren, MO
startDate <- "" # blank = use full period of record
endDate <- ""
# ── Reference discharge for a recent extreme event ────────────────────────────
# April 5, 2025 peak at this station (cfs)
Qmax_ref <- 180000
# ── Date to split the record into two sub-periods ────────────────────────────
splitdate <- "2000-01-01"
💡 Choosing a split date. The split date divides the
record into “historical” and “recent” periods. A good choice is a
watershed-change event: a dam completion, a major land-use shift, or a
climatological breakpoint identified in the literature. For the Current
River, Missouri, 2000 roughly separates a period of relative
stationarity from the more volatile recent decades. For your own
station, check whether a change-point analysis (e.g., the
changepoint package) identifies a structural break.
readNWISpeak() queries the USGS annual peak flow file —
a curated record of the single highest instantaneous discharge in each
water year, maintained by USGS hydrographers who quality-control and
approve every value.
annualpeak <- readNWISpeak(mysite, startDate, endDate)
# Remove records with missing peak discharge or date
annualpeak <- annualpeak %>%
filter(!is.na(peak_va), !is.na(peak_dt))
cat("Station :", mysite, "\n")
## Station : 07061000
cat("Period :", as.character(min(annualpeak$peak_dt)),
"to", as.character(max(annualpeak$peak_dt)), "\n")
## Period : 1904-03-27 to 2024-07-18
cat("Annual peaks :", nrow(annualpeak), "\n")
## Annual peaks : 95
cat("Discharge range (cfs):", round(min(annualpeak$peak_va)),
"to", format(max(annualpeak$peak_va), big.mark = ","), "\n")
## Discharge range (cfs): 22400 to 382,000
ggplot(annualpeak, aes(x = peak_dt, y = peak_va)) +
geom_col(fill = "#2e7d94", alpha = 0.75, width = 200) +
geom_hline(yintercept = Qmax_ref, linetype = "dashed",
color = "#c0392b", linewidth = 0.9) +
annotate("text", x = min(annualpeak$peak_dt),
y = Qmax_ref * 1.04,
label = paste0("Reference Q = ",
format(Qmax_ref, big.mark = ",")),
color = "#c0392b", hjust = 0, size = 3.4, fontface = "italic") +
scale_y_continuous(labels = scales::comma) +
labs(
title = paste("Annual Peak Discharge \u2014 USGS", mysite),
subtitle = "Red dashed line = reference extreme event",
x = NULL, y = "Peak discharge (cfs)"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(family = "serif", face = "bold",
color = "#1e3a2f", size = 13),
plot.subtitle = element_text(color = "#7a7a72", size = 10),
panel.grid.major.x = element_blank(),
panel.grid.minor = element_blank()
)
The simplest estimate of a return period requires no distributional assumption. Rank the annual peaks from largest to smallest, then apply the Weibull formula:
⚙️ Weibull plotting position
T = (n + 1) /
m
where n = total years of record, m = rank (1 = largest
value).
This is an unbiased estimator of the exceedance probability
1/T under no distributional assumption.
Q <- annualpeak$peak_va
n <- length(Q)
# Rank from largest (rank 1) to smallest
r <- n + 1 - rank(Q)
T_emp <- (n + 1) / r
emp_df <- tibble(
year = format(annualpeak$peak_dt, "%Y"),
discharge = Q,
rank = r,
return_period = round(T_emp, 2),
exceed_prob = round(1 / T_emp, 4)
) %>%
arrange(rank) %>%
slice_head(n = 15)
emp_df %>%
kbl(
caption = paste("Top 15 annual peaks with Weibull return periods,",
"USGS", mysite),
col.names = c("Year", "Discharge (cfs)", "Rank",
"Return period (yr)", "Exceedance probability")
) %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
| Year | Discharge (cfs) | Rank | Return period (yr) | Exceedance probability |
|---|---|---|---|---|
| 1916 | 382000 | 1 | 96.00 | 0.0104 |
| 1915 | 373000 | 2 | 48.00 | 0.0208 |
| 1927 | 369000 | 3 | 32.00 | 0.0312 |
| 1945 | 324000 | 4 | 24.00 | 0.0417 |
| 1982 | 312000 | 5 | 19.20 | 0.0521 |
| 1943 | 281000 | 6 | 16.00 | 0.0625 |
| 1935 | 266000 | 7 | 13.71 | 0.0729 |
| 1938 | 260000 | 8 | 12.00 | 0.0833 |
| 1907 | 259000 | 9 | 10.67 | 0.0938 |
| 1927 | 244000 | 10 | 9.60 | 0.1042 |
| 1949 | 236000 | 11 | 8.73 | 0.1146 |
| 1921 | 224000 | 12 | 8.00 | 0.1250 |
| 1904 | 220000 | 14 | 6.86 | 0.1458 |
| 1918 | 220000 | 14 | 6.86 | 0.1458 |
| 1933 | 220000 | 14 | 6.86 | 0.1458 |
# Gumbel reduced variate for the x-axis
y_emp <- -log(-log(1 - 1 / T_emp))
# Reference Ttick sequence for axis labels
Ttick <- c(1.01, 1.1, 1.5, 2, 5, 10, 20, 50, 100)
ytick <- -log(-log(1 - 1 / Ttick))
plot(
y_emp, Q,
xaxt = "n",
xlab = "Return period T (years)",
ylab = "Annual peak discharge (cfs)",
main = paste("Empirical flood frequency \u2014 USGS", mysite),
pch = 21, bg = "#8aad96", col = "#1e3a2f", cex = 1.1,
ylim = c(0, max(Q) * 1.1),
xlim = c(min(ytick), max(ytick))
)
axis(1, at = ytick, labels = as.character(Ttick))
abline(h = Qmax_ref, lty = 2, col = "#c0392b", lwd = 1.8)
abline(v = ytick, lty = 3, col = "lightgray")
abline(h = seq(0, max(Q) * 1.1, length.out = 6), lty = 3, col = "lightgray")
legend("topleft",
legend = c("Annual peak", paste("Reference Q =",
format(Qmax_ref, big.mark = ","))),
pch = c(21, NA),
lty = c(NA, 2),
col = c("#1e3a2f", "#c0392b"),
pt.bg = c("#8aad96", NA),
bty = "n", cex = 0.85)
📌 Reading the Gumbel probability axis. The x-axis uses the Gumbel reduced variate y = −ln(−ln(1 − 1/T)), which linearises data that follows the Gumbel distribution. If the points fall on a straight line, the Gumbel distribution is a good fit. Systematic curvature indicates a heavier or lighter tail than Gumbel predicts — try the Log-Pearson III in that case.
The Gumbel (Extreme Value Type I) distribution is the most widely applied parametric model for annual flood maxima. Its CDF is:
⚙️ Gumbel CDF and frequency factor
F(x) =
exp[−exp(−(x − μ)/β)]
Method-of-moments estimates:
β = s ·
√6 / π μ = x̄ − 0.5772 · β
Frequency factor for return period
T:
K_T = −(√6/π) · [0.5772 + ln(ln(T/(T−1)))]
Flood
quantile: Q_T = x̄ + K_T · s
# ── Fit parameters ────────────────────────────────────────────────────────────
xbar <- mean(Q); s <- sd(Q); nQ <- length(Q)
# Return period grid for fitted line
Ttick_fine <- c(1.001, 1.01, 1.1, 1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10,
15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100)
ytick_fine <- -log(-log(1 - 1 / Ttick_fine))
# Frequency factor and fitted quantiles
K_T <- -(sqrt(6) / pi) * (0.5772 + log(log(Ttick_fine / (Ttick_fine - 1))))
Q_T <- xbar + K_T * s
# 95% CI using Kite (1949) standard error formula
se_T <- (s * sqrt(1 + 1.14 * K_T + 1.1 * K_T^2)) / sqrt(nQ)
LB <- Q_T - qt(0.975, nQ - 1) * se_T
UB <- Q_T + qt(0.975, nQ - 1) * se_T
# Axis label positions
Ttick_lab <- c(1.01, 1.1, 1.5, 2, 5, 10, 20, 50, 100)
ytick_lab <- -log(-log(1 - 1 / Ttick_lab))
va_range <- range(Q)
ymax <- max(UB) * 1.05
plot(
y_emp, Q,
xaxt = "n",
xlab = "Return period T (years)",
ylab = "Annual peak discharge (cfs)",
main = paste("Gumbel frequency curve \u2014 USGS", mysite),
pch = 21, bg = "#8aad96", col = "#1e3a2f", cex = 1.1,
ylim = c(0, ymax),
xlim = c(min(ytick_fine), max(ytick_fine))
)
axis(1, at = ytick_lab, labels = as.character(Ttick_lab))
# Fitted line and confidence limits
lines(ytick_fine, Q_T, col = "#1a1a18", lwd = 2)
lines(ytick_fine, LB, col = "#2e7d94", lwd = 1.5, lty = 2)
lines(ytick_fine, UB, col = "#c0392b", lwd = 1.5, lty = 2)
# Grid and reference line
abline(v = ytick_fine, lty = 3, col = "lightgray")
abline(h = seq(0, ymax, length.out = 6), lty = 3, col = "lightgray")
abline(h = Qmax_ref, lty = 2, col = "#c0392b", lwd = 2)
legend("topleft",
legend = c("Annual peaks", "Gumbel fit",
"95% CI", paste("Ref Q =", format(Qmax_ref, big.mark = ","))),
pch = c(21, NA, NA, NA),
lty = c(NA, 1, 2, 2),
lwd = c(NA, 2, 1.5, 2),
col = c("#1e3a2f", "#1a1a18", "#2e7d94", "#c0392b"),
pt.bg = c("#8aad96", NA, NA, NA),
bty = "n", cex = 0.82)
T_standard <- c(2, 5, 10, 25, 50, 100)
y_std <- -log(-log(1 - 1 / T_standard))
K_std <- -(sqrt(6) / pi) * (0.5772 + log(log(T_standard / (T_standard - 1))))
Q_std <- xbar + K_std * s
se_std <- (s * sqrt(1 + 1.14 * K_std + 1.1 * K_std^2)) / sqrt(nQ)
lb_std <- Q_std - qt(0.975, nQ - 1) * se_std
ub_std <- Q_std + qt(0.975, nQ - 1) * se_std
tibble(
T = T_standard,
Q_T = round(Q_std),
Lower_95 = round(lb_std),
Upper_95 = round(ub_std),
exceed_prob = round(1 / T_standard, 4)
) %>%
kbl(
caption = paste("Gumbel flood quantiles \u2014 USGS", mysite),
col.names = c("Return period (yr)", "Q_T (cfs)",
"Lower 95% CI", "Upper 95% CI", "Annual exceedance prob.")
) %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
| Return period (yr) | Q_T (cfs) | Lower 95% CI | Upper 95% CI | Annual exceedance prob. |
|---|---|---|---|---|
| 2 | 109344 | 93994 | 124694 | 0.50 |
| 5 | 181897 | 156044 | 207750 | 0.20 |
| 10 | 229934 | 195015 | 264852 | 0.10 |
| 25 | 290628 | 243546 | 337709 | 0.04 |
| 50 | 335654 | 279317 | 391991 | 0.02 |
| 100 | 380348 | 314720 | 445976 | 0.01 |
The Log-Pearson Type III (LP3) distribution is the standard method recommended by the US Water Resources Council (Bulletin 17C) for federal flood frequency studies. It fits a Pearson III distribution to the base-10 logarithms of annual peaks, capturing the positive skewness typical of flood series.
# Step 1: log-transform
logQ <- log10(Q)
# Step 2: statistics of log-values
mean_logQ <- mean(logQ)
sd_logQ <- sd(logQ)
skew_logQ <- skewness(logQ)
cat(sprintf("Log10(Q): mean = %.4f SD = %.4f skewness = %.4f\n",
mean_logQ, sd_logQ, skew_logQ))
## Log10(Q): mean = 5.0043 SD = 0.2723 skewness = 0.0972
# Step 3: return periods and Pearson III frequency factor
T_lp3 <- c(2, 5, 10, 25, 50, 100)
z <- qnorm(1 - 1 / T_lp3) # standard normal variate
# Wilson-Hilferty approximation for Pearson III frequency factor K
g <- skew_logQ
K_lp3 <- z + (z^2 - 1) * g/6 - (z^3 - 6*z) * g^2/36 +
(z^2 - 1) * g^3/216 + z * g^4/648
# Step 4: estimated flood quantiles
logQ_est <- mean_logQ + K_lp3 * sd_logQ
Q_lp3 <- 10^logQ_est
tibble(
T = T_lp3,
K = round(K_lp3, 3),
log10_Q_est = round(logQ_est, 4),
Q_est_cfs = round(Q_lp3)
) %>%
kbl(
caption = paste("Log-Pearson III flood quantiles \u2014 USGS", mysite),
col.names = c("Return period (yr)", "K factor",
"log\u2081\u2080(Q) estimate", "Q estimate (cfs)")
) %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
| Return period (yr) | K factor | log₁₀(Q) estimate | Q estimate (cfs) |
|---|---|---|---|
| 2 | -0.016 | 4.9999 | 99978 |
| 5 | 0.838 | 5.2325 | 170822 |
| 10 | 1.293 | 5.3566 | 227276 |
| 25 | 1.786 | 5.4906 | 309427 |
| 50 | 2.107 | 5.5781 | 378507 |
| 100 | 2.398 | 5.6574 | 454378 |
📌 Gumbel vs. Log-Pearson III. Both methods produce similar estimates near the median (2–10 year return periods). They diverge at the extremes: LP3 accommodates positive skewness in the log domain more flexibly, which typically produces higher estimates for rare events (100-year floods) when the record has heavy-tailed behaviour. If the two methods disagree substantially at T = 100, inspect the Q-Q plots for both distributions before choosing one to report.
Splitting the record and comparing frequency curves is a visual diagnostic for non-stationarity. A rightward shift in the curve (higher discharge for the same return period) in the recent period suggests an intensification of flood magnitudes.
period1 <- annualpeak %>%
filter(peak_dt >= min(peak_dt) & peak_dt < as.Date(splitdate))
period2 <- annualpeak %>%
filter(peak_dt >= as.Date(splitdate))
cat(sprintf("Period 1: %d peaks (%s to %s)\n",
nrow(period1),
format(min(period1$peak_dt), "%Y"),
format(max(period1$peak_dt), "%Y")))
## Period 1: 71 peaks (1904 to 1994)
cat(sprintf("Period 2: %d peaks (%s to %s)\n",
nrow(period2),
format(min(period2$peak_dt), "%Y"),
format(max(period2$peak_dt), "%Y")))
## Period 2: 24 peaks (2001 to 2024)
# Helper: fit Gumbel and return a list of plotting ingredients
fit_gumbel <- function(df) {
Q <- df$peak_va
n <- length(Q)
xbar <- mean(Q); s <- sd(Q)
r <- n + 1 - rank(Q)
T_e <- (n + 1) / r
y_e <- -log(-log(1 - 1 / T_e))
Ttick <- c(1.001,1.01,1.1,1.5,2,3,4,5,6,7,8,9,10,15,20,25,
30,35,40,50,60,70,80,90,100)
yt <- -log(-log(1 - 1 / Ttick))
K <- -(sqrt(6)/pi) * (0.5772 + log(log(Ttick/(Ttick-1))))
Q_T <- xbar + K * s
se <- (s * sqrt(1 + 1.14*K + 1.1*K^2)) / sqrt(n)
LB <- Q_T - qt(0.975, n-1)*se
UB <- Q_T + qt(0.975, n-1)*se
graphlab <- paste(range(format(df$peak_dt, "%Y")), collapse = " \u2013 ")
list(Q=Q, n=n, y_emp=y_e, T_emp=T_e,
ytick=yt, Q_T=Q_T, LB=LB, UB=UB, graphlab=graphlab)
}
g1 <- fit_gumbel(period1)
g2 <- fit_gumbel(period2)
ymax_all <- max(c(g1$UB, g2$UB)) * 1.05
xlims <- c(min(g1$ytick), max(g1$ytick))
Ttick_lab <- c(1.01, 1.1, 1.5, 2, 5, 10, 20, 50, 100)
ytick_lab <- -log(-log(1 - 1 / Ttick_lab))
par(mfrow = c(1, 2), mar = c(4.5, 4.5, 3.5, 1))
for (g in list(g1, g2)) {
plot(
g$y_emp, g$Q,
xaxt = "n",
xlab = "Return period T (years)",
ylab = "Annual peak discharge (cfs)",
main = paste("USGS", mysite, "\n", g$graphlab),
pch = 21, bg = "#8aad96", col = "#1e3a2f", cex = 1.0,
ylim = c(0, ymax_all),
xlim = xlims,
cex.main = 0.92
)
axis(1, at = ytick_lab, labels = as.character(Ttick_lab), cex.axis = 0.8)
lines(g$ytick, g$Q_T, col = "#1a1a18", lwd = 2)
lines(g$ytick, g$LB, col = "#2e7d94", lwd = 1.4, lty = 2)
lines(g$ytick, g$UB, col = "#c0392b", lwd = 1.4, lty = 2)
abline(v = ytick_lab, lty = 3, col = "lightgray")
abline(h = seq(0, ymax_all, length.out = 6), lty = 3, col = "lightgray")
abline(h = Qmax_ref, lty = 2, col = "#c0392b", lwd = 2)
}
par(mfrow = c(1, 1))
# Compare T-year quantiles between periods
T_cmp <- c(2, 5, 10, 25, 50, 100)
yt_cmp <- -log(-log(1 - 1 / T_cmp))
mk_Q <- function(g) {
xbar <- mean(g$Q); s <- sd(g$Q)
K <- -(sqrt(6)/pi) * (0.5772 + log(log(T_cmp/(T_cmp-1))))
round(xbar + K * s)
}
tibble(
T = T_cmp,
Q_p1 = mk_Q(g1),
Q_p2 = mk_Q(g2),
change = paste0(ifelse(mk_Q(g2) >= mk_Q(g1), "+", ""),
round(100 * (mk_Q(g2) - mk_Q(g1)) / mk_Q(g1), 1), "%")
) %>%
kbl(
caption = paste("Gumbel flood quantiles: Period 1 vs. Period 2 \u2014 USGS", mysite),
col.names = c("Return period (yr)",
paste0("Q_T Period 1 (", g1$graphlab, ")"),
paste0("Q_T Period 2 (", g2$graphlab, ")"),
"Change (%)")
) %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
column_spec(4, bold = TRUE,
color = ifelse(
mk_Q(g2) >= mk_Q(g1), "#c0392b", "#2e7d32"
))
| Return period (yr) | Q_T Period 1 (1904 – 1994) | Q_T Period 2 (2001 – 2024) | Change (%) |
|---|---|---|---|
| 2 | 119620 | 81716 | -31.7% |
| 5 | 197828 | 122629 | -38% |
| 10 | 249608 | 149716 | -40% |
| 25 | 315033 | 183942 | -41.6% |
| 50 | 363568 | 209333 | -42.4% |
| 100 | 411746 | 234536 | -43% |
⚠️ Small-sample caution for split-period analysis. Splitting a 50-year record into two 25-year halves leaves each sub-period with very wide confidence intervals — especially at rare return periods (50, 100 year) that may not even be well-sampled within a 25-year window. Visual differences between the two curves are suggestive, not conclusive. Formal non-stationarity tests (e.g., Mann-Kendall trend test on the annual peak series, or a likelihood-ratio test on the Gumbel location parameter) are needed to make a defensible claim of change.
The analysis above is packaged into three functions you can source into any project. They wrap the complete Gumbel pipeline and allow interactive classroom use.
getAnnualPeak.fun()Downloads and cleans USGS annual peak data.
getAnnualPeak.fun <- function(site, startDate = "", endDate = "") {
library(dataRetrieval)
ap <- readNWISpeak(site, startDate, endDate)
ap <- ap[!is.na(ap$peak_va) & !is.na(ap$peak_dt), ]
ap
}
splitdata.fun()Splits an annual peak data frame at a given date.
splitdata.fun <- function(annualpeak, splitdate) {
p1 <- subset(annualpeak, peak_dt < as.Date(splitdate))
p2 <- subset(annualpeak, peak_dt >= as.Date(splitdate))
list(period1 = p1, period2 = p2)
}
plot_floodreturn.fun()Plots the Gumbel frequency curve for any annual peak data frame.
plot_floodreturn.fun <- function(annualpeak, Qmax_event = NULL) {
Q <- annualpeak$peak_va
va_range <- range(Q)
site_no <- annualpeak$site_no[1]
graphlab <- paste(range(format(annualpeak$peak_dt, "%Y")), collapse = " \u2013 ")
n <- length(Q)
r <- n + 1 - rank(Q)
T_emp <- (n + 1) / r
y_emp <- -log(-log(1 - 1 / T_emp))
Ttick <- c(1.001, 1.01, 1.1, 1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15, 16, 17, 18, 19, 20, 25, 30, 35, 40, 45, 50, 60, 70,
80, 90, 100)
xtlab <- c(1.001, 1.01, 1.1, 1.5, 2, NA, NA, 5, NA, NA, NA, NA, 10, NA,
NA, NA, NA, 15, NA, NA, NA, NA, 20, NA, 30, NA, NA, NA, 50,
NA, NA, NA, NA, 100)
ytick <- -log(-log(1 - 1 / Ttick))
xbar <- mean(Q); s <- sd(Q)
K <- -(sqrt(6)/pi) * (0.5772 + log(log(Ttick/(Ttick-1))))
Q_T <- xbar + K * s
se <- (s * sqrt(1 + 1.14*K + 1.1*K^2)) / sqrt(n)
LB <- Q_T - qt(0.975, n - 1) * se
UB <- Q_T + qt(0.975, n - 1) * se
ymax <- if (!is.null(Qmax_event)) max(max(UB), Qmax_event) * 1.05
else max(UB) * 1.05
plot(
y_emp, Q,
ylab = "Annual peak discharge (cfs)",
xaxt = "n",
xlab = "Return period T (years)",
ylim = c(0, ymax),
xlim = c(min(c(min(y_emp), min(ytick))), max(ytick)),
pch = 21, bg = "#8aad96", col = "#1e3a2f",
main = paste(site_no, graphlab),
cex.main = 0.9
)
par(cex = 0.65)
axis(1, at = ytick, labels = as.character(xtlab))
par(cex = 1)
lines(ytick, Q_T, col = "#1a1a18", lty = 1, lwd = 2)
lines(ytick, LB, col = "#2e7d94", lty = 2, lwd = 1.5)
lines(ytick, UB, col = "#c0392b", lty = 2, lwd = 1.5)
abline(v = ytick, lty = 3, col = "lightgray")
abline(h = seq(0, ymax, length.out = 6), lty = 3, col = "lightgray")
if (!is.null(Qmax_event))
abline(h = Qmax_event, lty = 2, col = "#c0392b", lwd = 2)
}
# Replicate the full-record plot using the helper function
plot_floodreturn.fun(annualpeak, Qmax_ref)
# Split and compare using the helper functions
periods <- splitdata.fun(annualpeak, splitdate)
par(mfrow = c(1, 2))
plot_floodreturn.fun(periods$period1, Qmax_ref)
plot_floodreturn.fun(periods$period2, Qmax_ref)
par(mfrow = c(1, 1))
readNWISpeak(), not
readNWISdv(), for flood frequency analysis. Annual
instantaneous peaks can be 2–5× the daily mean and are the required
input for all standard FFA methods.
Bulletin 17C. 2019. Guidelines for Determining Flood Flow Frequency. US Interagency Committee on Water Data, Hydrology Subcommittee. USGS Techniques and Methods 4-B5.
Milly, P. C. D., et al. 2008. Stationarity is dead: Whither water management? Science 319:573–574.
Merz, B., et al. 2021. Causes, impacts and patterns of disastrous river floods. Nature Reviews Earth and Environment 2:592–609.
USGS. 2019. National Water Information System. https://waterdata.usgs.gov/nwis