Iowa River Copula Example

Author

Avital Breverman

rm(list = ls())

# install.packages("remotes")
# remotes::install_github("rjaneUCF/MultiHazard")

library(dataRetrieval)
library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   4.0.0     ✔ tibble    3.2.1
✔ lubridate 1.9.4     ✔ tidyr     1.3.1
✔ purrr     1.0.4     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(POT)
library(lmom)

Attaching package: 'lmom'

The following object is masked from 'package:POT':

    samlmu
library(lmomco)

Attaching package: 'lmomco'

The following objects are masked from 'package:lmom':

    cdfexp, cdfgam, cdfgev, cdfglo, cdfgno, cdfgpa, cdfgum, cdfkap,
    cdfln3, cdfnor, cdfpe3, cdfwak, cdfwei, quaexp, quagam, quagev,
    quaglo, quagno, quagpa, quagum, quakap, qualn3, quanor, quape3,
    quawak, quawei

The following object is masked from 'package:POT':

    pp
library(VineCopula)
library(MultiHazard)
library(purrr)
library(geomtextpath)

This code builds two conditional extreme-flow dependence models (Scenario A/B), selects copulas by AIC, generates joint extreme simulations, derives joint return-period contours, merges them into a conservative envelope, and then ranks where along that envelope joint outcomes are most probable using KDE—yielding both curves and a representative “most likely” design point.

Here is a summary of the steps:

Functions

Define custom functions. There are additional functions in later code blocks. I’m sorry for my R sins.

# Computes a time separation criterion, in days, between events in a PDS to ensure independence
#Reference: Bulletin 17
# DA: drainage area in square miles
# returns days between events
B17_days_between <- function(DA) {
  ceiling(5 + log(DA))
}

# Madsen annualization of Generalized Pareto distribution to Generalized Extreme Value distribution 
# gpd_par: Generalized Pareto distribution parameters
# lambda: rate of events
# returns Generalized Extreme Value (GEV) distribution parameters
# Reference: Madsen, H., Rasmussen, P., & Rosbjerg, D. (1997). Comparison of annual maximum series and partial duration series methods for modeling extreme hydrologic events 1. At-site modeling. Water Resources Research, # 33(4), 747-757.
madsen_annualization <- function(gpd_par, lambda) {
  gev_xi <- gpd_par["xi"] + (gpd_par["alpha"] / gpd_par["k"]) * (1 - lambda ^ -gpd_par["k"]) # scale
  gev_alpha <- gpd_par["alpha"] * lambda ^ -gpd_par["k"] # scale
  gev_k <- gpd_par["k"]  
  
  print(gev_xi)
  print(gev_alpha)
  print(gev_k)
  
  c(xi = as.numeric(gev_xi),
  alpha = as.numeric(gev_alpha),
  k = as.numeric(gev_k))
}

# Langbein adjustment defines the relationship between empirical flood expectancies in the partial duration series and the probability of the correspoding flood as an annual series
# p_pds: partial duration non-exceedance probability
# lambda: rate of events
# returns the annual non-exceedance probability
# Reference: Langbein, W. B. (1949). Annual Floods and the Partial-duration Flood Series. American Geophysical Union, 30(6), 879-881.
langbein_adjustment <- function(lambda, p_pds) {
  exp(-lambda * (1 - p_pds))
}

# Gringorten plotting position, non-exceedance probability
gringorten_pp <- function(x) {
  n <- length(x)
  i <- rank(x)
  (i - 0.44) / (n + 0.12)
}

Download Streamflow Data using dataRetrieval

Download daily streamflow data for:

  • Iowa River at Marshalltown, IA (05451500)

  • Iowa River at Marengo, IA (05453100)

NWIS is scheduled for decommission but I’m rolling with these functions for as long as I can.

marshalltown <- "05451500"
marengo <- "05453100"

marshalltown_daily <- readNWISdv(marshalltown, "00060") %>% renameNWISColumns() %>%
  na.omit()
Warning in readNWISdv(marshalltown, "00060"): NWIS servers are slated for
decommission. Please begin to migrate to read_waterdata_daily.
GET: https://waterservices.usgs.gov/nwis/dv/?site=05451500&format=waterml%2C1.1&ParameterCd=00060&StatCd=00003&startDT=1851-01-01
marengo_daily <- readNWISdv(marengo, "00060") %>% renameNWISColumns() %>%
  na.omit()
Warning in readNWISdv(marengo, "00060"): NWIS servers are slated for
decommission. Please begin to migrate to read_waterdata_daily.
GET: https://waterservices.usgs.gov/nwis/dv/?site=05453100&format=waterml%2C1.1&ParameterCd=00060&StatCd=00003&startDT=1851-01-01
daily_flow_joined <- left_join(marshalltown_daily, marengo_daily, by = "Date") %>%
  select(Date, Flow.x, Flow.y) %>%
  rename(Marshalltown = Flow.x,
         Marengo = Flow.y) %>%
  na.omit()

Plot Daily Streamflow

marshalltown_plot <- ggplot(marshalltown_daily, aes(x = Date, y = Flow)) +
  geom_line() +
  theme_bw() +
  xlab("") +
  ylab("Flow (cfs)") +
  ggtitle("Marshalltown")
marshalltown_plot

marengo_plot <- ggplot(marengo_daily, aes(x = Date, y = Flow)) +
  geom_line() +
  theme_bw() +
  xlab("") +
  ylab("Flow (cfs)") +
  ggtitle("Marengo")
marengo_plot

Extract Marengo PDS

# Create data frame with obs and time columns
pot_marengo <- data.frame(
  obs = marengo_daily$Flow,
  time = as.numeric(marengo_daily$Date)  # Convert to numeric
)

da_marengo <- 2794 # square miles
time_sep <- B17_days_between(da_marengo)
thresh_marengo <- 10000 # cfs

# Extract partial duration series
pds_marengo <- POT::clust(data = pot_marengo,
                              u = thresh_marengo, # threshold
                              tim.cond = time_sep, # time separation
                              clust.max = TRUE) # extract cluster maxima

summary(pds_marengo)
      time            obs             idx       
 Min.   :-3937   Min.   :10100   Min.   :  904  
 1st Qu.: 1908   1st Qu.:11600   1st Qu.: 6749  
 Median : 7517   Median :13500   Median :12358  
 Mean   : 7849   Mean   :15575   Mean   :12690  
 3rd Qu.:14365   3rd Qu.:17100   3rd Qu.:19206  
 Max.   :20300   Max.   :48200   Max.   :25140  
nrow(pds_marengo)
[1] 81
# Build dataframe
df_pds_marengo <- data.frame(pds_marengo) %>%
  rename(Date = time,
         Flow = obs) %>%
  mutate(Date = as.Date(Date))  %>%
  select(Date, Flow)

# Compute rate of events
years_marengo <-
  as.numeric((
    max(marengo_daily$Date) - min(marengo_daily$Date) + 1
  ) / 365.25)
rate_marengo <- nrow(df_pds_marengo) / years_marengo

# Plot daily flow and PDS
marengo_plot +
  geom_hline(yintercept = thresh_marengo, linetype = "dashed") +
  geom_point(data = df_pds_marengo, aes(x = Date, y = Flow), colour = "#A60F2D", shape = 19, size = 2) 

Extract Marshalltown PDS

# Create data frame with obs and time columns
pot_marshalltown <- data.frame(
  obs = marshalltown_daily$Flow,
  time = as.numeric(marshalltown_daily$Date)  # Convert to numeric
)

da_marshalltown <- 1532 # square miles
time_sep <- B17_days_between(da_marshalltown)
thresh_marshalltown <- 5500 # cfs

# Extract partial duration series
pds_marshalltown <- POT::clust(data = pot_marshalltown,
                              u = thresh_marshalltown, # threshold
                              tim.cond = time_sep, # time separation
                              clust.max = TRUE) # extract cluster maxima

summary(pds_marshalltown)
      time             obs             idx       
 Min.   :-24324   Min.   : 5520   Min.   :  241  
 1st Qu.: -6296   1st Qu.: 6470   1st Qu.:12422  
 Median :  4825   Median : 8085   Median :23543  
 Mean   :  3577   Mean   : 9389   Mean   :22459  
 3rd Qu.: 13985   3rd Qu.:10375   3rd Qu.:32703  
 Max.   : 20548   Max.   :39400   Max.   :39206  
nrow(pds_marshalltown)
[1] 158
# Build dataframe
df_pds_marshalltown <- data.frame(pds_marshalltown) %>%
  rename(Date = time,
         Flow = obs) %>%
  mutate(Date = as.Date(Date)) %>%
  select(Date, Flow)

# Compute rate of events
years_marshalltown <-
  as.numeric((
    max(marshalltown_daily$Date) - min(marshalltown_daily$Date) + 1
  ) / 365.25)
rate_marshalltown <- nrow(df_pds_marshalltown) / years_marshalltown

# Plot daily flow and PDS
marshalltown_plot +
  geom_hline(yintercept = thresh_marshalltown, linetype = "dashed") +
  geom_point(data = df_pds_marshalltown, aes(x = Date, y = Flow),colour = "#A60F2D", shape = 19, size = 2) 

Rank Correlation: Kendall’s tau

Compute rank correlation between flows at Marshalltown and Marengo using Kendall’s rank correlation coefficient (Kendall’s tau) over a range of lags.

I computed Kendall’s tau under 2 scenarios:

  • Rank correlation between the PDS of Marengo flows and lagged daily flows at Marshalltown

  • Rank correlation between the Marshalltown flows and lagged daily flows at Marengo

In both scenarios, the Kendall’s tau value was highest at a lag of 0 (flows on the same date).

Extreme and Coincident Flows

lag <- 5 # days

# Prepare data frame with 3 columns: Date, Variable1, Variable2
# For auto-correlation, use same variable twice
marengo_con <- left_join(df_pds_marengo, marshalltown_daily, by = "Date") %>%
  select(Date, Flow.x, Flow.y) %>%
  rename(Marengo = Flow.x,
         Marshalltown = Flow.y) %>%
  na.omit()

head(marengo_con)
        Date Marengo Marshalltown
1 1959-03-23   14000         3690
2 1960-01-14   13100          918
3 1960-03-31   29000        17700
4 1960-05-09   13100         5070
5 1961-03-08   10200         3980
6 1962-03-29   14500         9100
# Calculate Kendall's tau at different lags
result <- MultiHazard::Kendall_Lag(Data = marengo_con,
                      Lags = seq(-lag, lag, 1),  # lag is applied to second named variable
                      PLOT = TRUE, # display plot
                      GAP = 0.1) # length of y-axis above and below max and min                                       

# Extract results
tau_values <- result$Value
p_values <- result$Test

# View tau coefficients
print(tau_values)
$Marengo_Marshalltown
 [1]  0.04515032 -0.01511521 -0.01104427 -0.05251182 -0.03879202  0.48434444
 [7] -0.06041379 -0.06130811  0.08700081 -0.16864291  0.01375430
print(p_values)
$Marengo_Marshalltown_Test
 [1] 5.657903e-01 8.464966e-01 8.867447e-01 4.953800e-01 6.121405e-01
 [6] 1.862055e-10 4.297486e-01 4.259709e-01 2.618106e-01 3.075164e-02
[11] 8.611042e-01
# Prepare data frame with 3 columns: Date, Variable1, Variable2
# For auto-correlation, use same variable twice
marshalltown_con <- left_join(df_pds_marshalltown, marengo_daily, by = "Date") %>%
  select(Date, Flow.x, Flow.y) %>%
    rename(Marshalltown = Flow.x,
           Marengo = Flow.y) %>%
  na.omit()

head(marshalltown_con)
         Date Marshalltown Marengo
43 1957-06-16         5830    1080
44 1959-03-27         8850    9940
45 1960-03-31        17700   29000
46 1960-04-18         6170    5120
47 1960-05-07         9400   11900
48 1961-03-31         7850    4800
# Calculate Kendall's tau at different lags
result <- MultiHazard::Kendall_Lag(Data = marshalltown_con,
                      Lags = seq(-lag, lag, 1),  # lag is applied to second named variable
                      PLOT = TRUE, # display plot
                      GAP = 0.1) # length of y-axis above and below max and min                                     

# Extract results
tau_values <- result$Value
p_values <- result$Test

# View tau coefficients
print(tau_values)
$Marshalltown_Marengo
 [1]  0.127083183 -0.014836320 -0.080468890 -0.001089325 -0.058256892
 [6]  0.440871605 -0.111306491  0.072512265  0.101995586 -0.049983887
[11]  0.103267138
print(p_values)
$Marshalltown_Marengo_Test
 [1] 4.853541e-02 8.170387e-01 2.074413e-01 9.863219e-01 3.570428e-01
 [6] 2.534491e-12 7.843599e-02 2.537527e-01 1.100174e-01 4.356485e-01
[11] 1.089144e-01

The maximum tau in both scenarios occurs with a lag of 0, meaning that the coincident events should be pulled from the same date as the extremal (PDS) events.

Scenario A (Extremal Marshalltown Flows) Marginal Distributions

Scenario A is defined by extremal events (PDS) at the Marshalltown gage and the coincident events at the Marengo gage. The conditioning variable is Marshalltown flows, which are conditioned on Marengo flows.

Kendall’s tau is max at a lag of 0 days. Therefore, use left_join to match coincident events to the date of the PDS events.

ggplot(marshalltown_con, aes(x = Marshalltown, y = Marengo)) +
    geom_point() +
    xlab("Marshalltown PDS Flow (cfs)") + 
    ylab("Marengo Coincident Flow (cfs)") +
    ggtitle("Iowa River") +
    theme_bw()

# Fit marginal distributions
marshalltown_gpa <- lmom::pelgpa(lmom::samlmu(marshalltown_con$Marshalltown)) 
marshalltown_gev <- madsen_annualization(marshalltown_gpa, rate_marshalltown)
     xi 
6250.71 
   alpha 
3605.862 
          k 
-0.09533262 
marengo_glo <- lmom::pelglo(lmom::samlmu(marshalltown_con$Marengo))

#lmom::evplot(marshalltown_con$Marshalltown, main = "Marshalltown"); 
#lmom::evdistq(lmom::quagev, para = marshalltown_gev)

marshalltown_con_gev <- marshalltown_con %>%
  mutate(marshalltown_cdf = lmom::cdfgev(Marshalltown, marshalltown_gev),
         marengo_cdf = lmom::cdfglo(Marengo, marengo_glo))

marshalltown_con_gpa <- marshalltown_con %>%
  mutate(marshalltown_cdf = lmom::cdfgpa(Marshalltown, marshalltown_gpa),
         marengo_cdf = lmom::cdfglo(Marengo, marengo_glo))

ggplot(marshalltown_con_gev, aes(x = marshalltown_cdf, y = marengo_cdf)) +
  geom_point() +
    xlab("CDF of Marshalltown PDS Flow") + 
    ylab("CDF of Marengo Coincident Flow (cfs)") +
    ggtitle("Iowa River") +
    theme_bw()

f <- seq(0.001, 0.999, length.out = 1000)   # nonexceedance probabilities
q <- lmom::quaglo(f, marengo_glo)

plot(qnorm(f), q, type = "l", lwd = 2,
     log = "y",
     xlab = "Probability scale (qnorm(F))",
     ylab = "Quantile (x)",
     main = "Marengo Coincident Events Generalized Logistic Distribution")
grid()

x <- marshalltown_con$Marshalltown
p_pds <- gringorten_pp(x) # partial duration space non-exceedance probability

main_txt <- "Marshalltown GEV Distribution"

p <- langbein_adjustment(rate_marshalltown, p_pds)
plot(qnorm(p), x, 
     xlab ="Reduced variate, -log(-log(F))", 
     ylab ="Quantile", 
     main = "")
title(main = main_txt, line = 3) 

pp <- seq(0.001, 0.999, length.out = 200)
qfit <- lmom::quagev(pp, marshalltown_gev)
lines(qnorm(pp), qfit, col="blue", lwd=2)

# Add a top axis with Return Period (years)
T_ticks <- c(2, 5, 10, 25, 50, 100, 200)   
p_ticks <- 1 - 1 / T_ticks                 # convert T -> nonexceedance prob
axis(side = 3,
     at = qnorm(p_ticks),
     labels = T_ticks)
mtext("Return period (years)", side = 3, line = 2)

Scenario B (Extremal Marengo Flows) Marginal Distributions

Scenario B is defined by extremal events (PDS) at the Marengo gage and the coincident events at the Marshalltown gage. The conditioning variable is Marengo flows, which are conditioned on Marshalltown flows.

ggplot(marengo_con, aes(x = Marengo, y = Marshalltown)) +
    geom_point() +
    xlab("Marengo PDS Flow (cfs)") + 
    ylab("Marshalltown Coincident Flow (cfs)") +
    ggtitle("Iowa River") +
    theme_bw()

# Fit marginal distributions
marengo_gpa <- lmom::pelgpa(lmom::samlmu(marengo_con$Marengo)) 
marengo_gev <- madsen_annualization(marengo_gpa, rate_marengo)
      xi 
10869.84 
   alpha 
4600.234 
         k 
-0.1696055 
marshalltown_glo <- lmom::pelglo(lmom::samlmu(marengo_con$Marshalltown))

#lmom::evplot(marshalltown_con$Marshalltown, main = "Marengo"); 
#lmom::evdistq(lmom::quagev, para = marshalltown_gev)

marengo_con_gev <- marengo_con %>%
  mutate(marengo_cdf = lmom::cdfgev(Marengo, marengo_gev),
         marshalltown_cdf = lmom::cdfglo(Marshalltown, marshalltown_glo))

marengo_con_gpa <- marengo_con %>%
  mutate(marengo_cdf = lmom::cdfgpa(Marengo, marengo_gpa),
         marshalltown_cdf = lmom::cdfglo(Marshalltown, marshalltown_glo))

ggplot(marengo_con_gev, aes(x = marengo_cdf, y = marshalltown_cdf)) +
  geom_point() +
    xlab("CDF of Marengo PDS Flow") + 
    ylab("CDF of MArshalltown Coincident Flow (cfs)") +
    ggtitle("Iowa River") +
    theme_bw()

q <- lmom::quaglo(f, marshalltown_glo)

plot(qnorm(f), q, type = "l", lwd = 2,
     log = "y",
     xlab = "Probability scale (qnorm(F))",
     ylab = "Quantile (x)",
     main = "Marshalltown Coincident Events Generalized Logistic Distribution")
Warning in xy.coords(x, y, xlabel, ylabel, log): 1 y value <= 0 omitted from
logarithmic plot
grid()

x <- marengo_con$Marengo
p_pds <- gringorten_pp(x) # partial duration space non-exceedance probability

main_txt <- "Marengo GEV Distribution"

p <- langbein_adjustment(rate_marengo, p_pds)
plot(qnorm(p), x, 
     xlab ="Reduced variate, -log(-log(F))", 
     ylab ="Quantile", 
     main = "")
title(main = main_txt, line = 3) 

pp <- seq(0.001, 0.999, length.out = 200)
qfit <- lmom::quagev(pp, marengo_gev)
lines(qnorm(pp), qfit, col="blue", lwd=2)

# Add a top axis with Return Period (years)
T_ticks <- c(2, 5, 10, 25, 50, 100, 200)   
p_ticks <- 1 - 1 / T_ticks                 # convert T -> nonexceedance prob
axis(side = 3,
     at = qnorm(p_ticks),
     labels = T_ticks)
mtext("Return period (years)", side = 3, line = 2)

Plot Coincident Events for Each Scenario

marshalltown_con_longer <- marshalltown_con |>
pivot_longer(cols = -Date, names_to = "series", values_to = "Gage")

ggplot(marshalltown_con_longer, aes(x = Date, y = Gage, color = series)) +
  geom_point(size = 2) +
    geom_hline(yintercept = thresh_marshalltown, color = "#A60F2D", linetype = "solid") +
    labs(color = "Gage") + 
  scale_color_manual(                            
    values = c(
      Marshalltown = "#A60F2D",
      Marengo = "black")) +
  ylab("Flow (cfs)") +
  ggtitle("Scenario A: Extremal Marshalltown Flows") +
  theme_bw()

marengo_con_longer <- marengo_con |>
  pivot_longer(cols = -Date, names_to = "series", values_to = "Gage")

ggplot(marengo_con_longer, aes(x = Date, y = Gage, color = series)) +
  geom_point(size = 2) +
    geom_hline(yintercept = thresh_marengo, color = "#A60F2D", linetype = "solid") +
    labs(color = "Gage") +
  scale_color_manual(                            
    values = c(
      Marengo = "#A60F2D",
      Marshalltown = "black")) +
  ylab("Flow (cfs)") +
  ggtitle("Scenario B: Extremal Marengo Flows") +
  theme_bw()

Write Datasets to csv

This code block is for Neptune.

write.csv(marengo_con, file = "marengo_con.csv")
write.csv(marshalltown_con, file = "marshalltown_con.csv")

Scenario A Copula Fits and Comparisons

u <- pull(marshalltown_con_gpa, marshalltown_cdf)
v <- pull(marshalltown_con_gpa, marengo_cdf)

gumbel_cop <- BiCopEst(u, v, family = 4)
print(gumbel_cop$AIC)
[1] -67.55358
plot(gumbel_cop, type = "contour", margins = "unif", main = "Gumbel", col = "blue")
points(x=u, y=v, pch = 16)

t_cop <- BiCopEst(u, v, family = 2)
print(t_cop$AIC)
[1] -50.96909
plot(t_cop, type = "contour", margins = "unif", main = "Student's t", col = "blue")
points(x=u, y=v, pch = 16)

gauss_cop <- BiCopEst(u, v, family = 1)
print(gauss_cop$AIC)
[1] -46.08045
plot(gauss_cop, type = "contour", margins = "unif", main = "Gaussian", col = "blue")
points(x=u, y=v, pch = 16)

clayton_cop <- BiCopEst(u, v, family = 3)
print(clayton_cop$AIC)
[1] -6.039612
plot(clayton_cop, type = "contour", margins = "unif", main = "Clayton", col = "blue")
points(x=u, y=v, pch = 16)

frank_cop <- BiCopEst(u, v, family = 5)
print(frank_cop$AIC)
[1] -50.61719
plot(frank_cop, type = "contour", margins = "unif", main = "Frank", col = "blue")
points(x=u, y=v, pch = 16)

joe_cop <- BiCopEst(u, v, family = 6)
print(joe_cop$AIC)
[1] -68.00894
plot(joe_cop, type = "contour", margins = "unif", main = "Joe", col = "blue")
points(x=u, y=v, pch = 16)

# Create summary table
copula_tbl <- dplyr::bind_rows(
  tibble(Copula = "Gumbel",       family = gumbel_cop$family,  par = gumbel_cop$par,  par2 = gumbel_cop$par2,  AIC = gumbel_cop$AIC),
  tibble(Copula = "Student's t",  family = t_cop$family,       par = t_cop$par,       par2 = t_cop$par2,       AIC = t_cop$AIC),
  tibble(Copula = "Gaussian",     family = gauss_cop$family,   par = gauss_cop$par,   par2 = gauss_cop$par2,   AIC = gauss_cop$AIC),
  tibble(Copula = "Clayton",      family = clayton_cop$family, par = clayton_cop$par, par2 = clayton_cop$par2, AIC = clayton_cop$AIC),
  tibble(Copula = "Frank",        family = frank_cop$family,   par = frank_cop$par,   par2 = frank_cop$par2,   AIC = frank_cop$AIC),
  tibble(Copula = "Joe",          family = joe_cop$family,     par = joe_cop$par,     par2 = joe_cop$par2,     AIC = joe_cop$AIC)
) %>%
  mutate(
    dep_parameter = ifelse(is.na(par2), 
                           sprintf("%.4f", par),
                           sprintf("par=%.4f, par2=%.4f", par, par2))
  ) %>%
  select(Copula, dep_parameter, AIC) %>%
  arrange(AIC)

copula_tbl
# A tibble: 6 × 3
  Copula      dep_parameter              AIC
  <chr>       <chr>                    <dbl>
1 Joe         par=2.2309, par2=0.0000 -68.0 
2 Gumbel      par=1.7908, par2=0.0000 -67.6 
3 Student's t par=0.6389, par2=8.9732 -51.0 
4 Frank       par=4.6233, par2=0.0000 -50.6 
5 Gaussian    par=0.5469, par2=0.0000 -46.1 
6 Clayton     par=0.2866, par2=0.0000  -6.04
# Select the Copula with the minimum AIC (in this scenario, the Joe copula)
marshalltown_cop <- joe_cop

Scenario B Copula Fits and Comparisons

u <- pull(marengo_con_gpa, marengo_cdf)
v <- pull(marengo_con_gpa, marshalltown_cdf)

gumbel_cop <- BiCopEst(u, v, family = 4)
print(gumbel_cop$AIC)
[1] -48.16928
plot(gumbel_cop, type = "contour", margins = "unif", main = "Gumbel", col = "blue")
points(x=u, y=v, pch = 16)

t_cop <- BiCopEst(u, v, family = 2)
print(t_cop$AIC)
[1] -22.25798
plot(t_cop, type = "contour", margins = "unif", main = "Student's t", col = "blue")
points(x=u, y=v, pch = 16)

gauss_cop <- BiCopEst(u, v, family = 1)
print(gauss_cop$AIC)
[1] -24.91734
plot(gauss_cop, type = "contour", margins = "unif", main = "Gaussian", col = "blue")
points(x=u, y=v, pch = 16)

clayton_cop <- BiCopEst(u, v, family = 3)
print(clayton_cop$AIC)
[1] -2.13513
plot(clayton_cop, type = "contour", margins = "unif", main = "Clayton", col = "blue")
points(x=u, y=v, pch = 16)

frank_cop <- BiCopEst(u, v, family = 5)
print(frank_cop$AIC)
[1] -43.62798
plot(frank_cop, type = "contour", margins = "unif", main = "Frank", col = "blue")
points(x=u, y=v, pch = 16)

joe_cop <- BiCopEst(u, v, family = 6)
print(joe_cop$AIC)
[1] -53.7178
plot(joe_cop, type = "contour", margins = "unif", main = "Joe", col = "blue")
points(x=u, y=v, pch = 16)

# Create summary table
copula_tbl <- dplyr::bind_rows(
  tibble(Copula = "Gumbel",       family = gumbel_cop$family,  par = gumbel_cop$par,  par2 = gumbel_cop$par2,  AIC = gumbel_cop$AIC),
  tibble(Copula = "Student's t",  family = t_cop$family,       par = t_cop$par,       par2 = t_cop$par2,       AIC = t_cop$AIC),
  tibble(Copula = "Gaussian",     family = gauss_cop$family,   par = gauss_cop$par,   par2 = gauss_cop$par2,   AIC = gauss_cop$AIC),
  tibble(Copula = "Clayton",      family = clayton_cop$family, par = clayton_cop$par, par2 = clayton_cop$par2, AIC = clayton_cop$AIC),
  tibble(Copula = "Frank",        family = frank_cop$family,   par = frank_cop$par,   par2 = frank_cop$par2,   AIC = frank_cop$AIC),
  tibble(Copula = "Joe",          family = joe_cop$family,     par = joe_cop$par,     par2 = joe_cop$par2,     AIC = joe_cop$AIC)
) %>%
  mutate(
    dep_parameter = ifelse(is.na(par2), 
                           sprintf("%.4f", par),
                           sprintf("par=%.4f, par2=%.4f", par, par2))
  ) %>%
  select(Copula, dep_parameter, AIC) %>%
  arrange(AIC)

copula_tbl
# A tibble: 6 × 3
  Copula      dep_parameter               AIC
  <chr>       <chr>                     <dbl>
1 Joe         par=2.4673, par2=0.0000  -53.7 
2 Gumbel      par=1.7578, par2=0.0000  -48.2 
3 Frank       par=5.5429, par2=0.0000  -43.6 
4 Gaussian    par=0.3968, par2=0.0000  -24.9 
5 Student's t par=0.4591, par2=30.0000 -22.3 
6 Clayton     par=0.0564, par2=0.0000   -2.14
# Select the Copula with the minimum AIC (in this scenario, the Joe copula)
marengo_cop <- joe_cop

Contour Line Values for Plotting

This code block is for Neptune

n <- 60
u <- v <- seq(0.02, 0.98, length.out = n)

# density on grid
z <- outer(u, v, function(a, b)
  BiCopPDF(a, b,
           family = marengo_cop$family,
           par    = marengo_cop$par,
           par2   = marengo_cop$par2)
)

lev <- c(0.2, 0.6, 1, 1.5, 2, 3, 5, 7)

contour(u, v, z, levels = lev, col = "blue",
        xlab = "u", ylab = "v",
        main = sprintf("BiCop density contours", marengo_cop$family))
lev

cls <- contourLines(u, v, z, levels = lev)

df <- do.call(rbind, lapply(seq_along(cls), function(i) {
  data.frame(
    level = cls[[i]]$level,
    x     = cls[[i]]$x,
    y     = cls[[i]]$y,
    line  = i,                        # optional: which polyline (useful if multiple per level)
    pt    = seq_along(cls[[i]]$x)     # optional: point index along the line
  )
}))

# If you truly want only 3 columns, drop line/pt:
# df <- df[, c("level","x","y")]

write.csv(df, "iowa_river_contours.csv", row.names = FALSE)

Copula Simulation and Transformation

Generate Samples

Generate a large number of samples). Split the number of samples across the two conditional copula models in proportion to the number of events in each dataset.

N <- 10^6
n_A <- nrow(marshalltown_con) # Number of coincident events in scenario A
n_B <- nrow(marengo_con) # Number of coincident events in scenario B
n1 <- round((N * n_A) / (n_A + n_B), 0)
n2 <- N - n1

# Simulate U(0,1)^2 from each fitted copula
s1 <- VineCopula::BiCopSim(n1, obj = marshalltown_cop)
s2 <- VineCopula::BiCopSim(n2, obj = marengo_cop)

#plot(head(s1, 1000))

Transform to Original Scale using Quantile Functions

Transform the simulated uniforms to original units using quantile functions.

# Scenario A
marshalltown_gev <- lmomco::vec2par(marshalltown_gev, type = "gev")
marengo_glo <- lmomco::vec2par(marengo_glo, type = "glo")

marshalltown_A <- quagev(s1[,1], marshalltown_gev)
marengo_A <- quaglo(s1[,2], marengo_glo)

copula_sample_A <- data.frame(Marshalltown = marshalltown_A, Marengo = marengo_A, scenario = "A")

# Scenario B
marengo_gev <- vec2par(marengo_gev, type = "gev")
marshalltown_glo <- vec2par(marshalltown_glo, type = "glo")

marengo_B <- quagev(s2[,1], marengo_gev)
marshalltown_B <- quaglo(s2[,2], marshalltown_glo)

copula_sample_B <- data.frame(Marshalltown = marshalltown_B, Marengo = marengo_B, scenario = "B")

# Combine samples from both scenarios; used in KDE 
copula_sample <- rbind(copula_sample_A, copula_sample_B)

ggplot() +
  geom_point(
    data = dplyr::slice_sample(copula_sample, n = 10000), 
    aes(x = Marshalltown, y = Marengo, color = scenario),
    inherit.aes = FALSE,
    shape = 21
  ) +
    scale_color_manual(
    name = "Scenario",  
    values = c(
      A = "blue",
      B = "red"
    )
  ) +
  theme_bw() +
  coord_fixed(1) +
  xlim(0, 180000) + ylim(0, 180000) +
  labs(x = "Marshalltown Flow (cfs)", y = "Marengo Flow (cfs)")
Warning: Removed 10 rows containing missing values or values outside the scale range
(`geom_point()`).

Isoline Derivation

Generate Unit Square

# Using "low" resolution here to reduce run times
x <- c(10^(-5), seq(999.9*10^(-5), 1-(1*10^(-6)), 10^(-4)))
y <- c(10^(-5), seq(999.9*10^(-5), 1-(1*10^(-6)), 10^(-4)))
grid <- list(x = x, y = y)
u <- expand.grid(x, y)

Example Unit Square

n_x <- 20
n_y <- 20

x_breaks <- seq(0, 1, length.out = n_x + 1)
y_breaks <- seq(0, 1, length.out = n_y + 1)

grid_pts <- expand.grid(x = x_breaks, y = y_breaks)

ggplot() +
  # Lines placed exactly at break positions
  geom_vline(xintercept = x_breaks, color = "steelblue", linewidth = 0.3) +
  geom_hline(yintercept = y_breaks, color = "steelblue", linewidth = 0.3) +
  # Circles at every (x_break, y_break) crossing
  geom_point(
    data  = grid_pts,
    aes(x, y),
    fill  = "black",
    color = "black",
    size  = 2
  ) +
  coord_fixed(xlim = c(-0.1, 1.1), ylim = c(-0.1, 1.1), expand = FALSE) +
  theme_minimal() +
  labs(
    title = sprintf("", n_x, n_y),
    x = "x", y = "y"
  )
Warning in sprintf("", n_x, n_y): 2 arguments not used by format ''

Evaluate Copula CDF

Evaluate the copula CDF at each point on the grid.

# Evaluates CDF of the specified parametric copula
uCopA <- VineCopula::BiCopCDF(u[,1], u[,2], marshalltown_cop) 
uCopB <- VineCopula::BiCopCDF(u[,1], u[,2], marengo_cop) 

User-specified Joint Return Periods

# Return periods
rp_vec <- c(10, 50, 100, 200)

Determine Return Period

Determine the return period of each point on the unit square.

This code block defines the sampling frequency for daily data and a function to compute the return period surface matrix for an x, y grid in column blocks.

mu <- 365.25 # Sampling frequency for daily data

# Computes return period surface matrix z on a grid of x and y values in blocks of columns
make_z_surface_blocked <- function(x, y, Cmat, mean_waiting_time, block = 500L){
  nx <- length(x); ny <- length(y)
  z <- matrix(NA_real_, nrow = nx, ncol = ny) # Pre-allocate output matrix z

  # Iterate over the y-axis in chunks
  for (j0 in seq.int(1L, ny, by = block)) {
    j1 <- min(j0 + block - 1L, ny)
    yb <- y[j0:j1]

    # denom is nx × nb (nb = block size)
    denom <- 1 - x - rep(yb, each = nx) + as.vector(Cmat[, j0:j1, drop=FALSE])
    z[, j0:j1] <- mean_waiting_time / matrix(denom, nrow = nx, ncol = length(yb))
  }
  z
}

Compute Contour Lines

compute_contour_lines <- function(
    return_periods,
    x, y, z,
    mu,
    stop_on_missing = TRUE
) {
  purrr::map_dfr(return_periods, function(rp) {

    xy <- grDevices::contourLines(x, y, z, levels = mu * rp)

    if (length(xy) == 0) {
      msg <- paste("No contour found for RP =", rp)
      if (stop_on_missing) stop(msg) else { warning(msg); return(NULL) }
    }

    do.call(rbind, lapply(seq_along(xy), function(i) {
      data.frame(
        id                = i,
        level             = xy[[i]]$level,
        marshalltown_cdf  = xy[[i]]$x,
        marengo_cdf       = xy[[i]]$y,
        rp                = rp
      )
    }))
  })
}

Scenario A Contour Lines

# Mean waiting time is the average time, in days, between events
mean_waiting_time_A <- mu / rate_marshalltown 

# Reshape into a matrix: rows correspond to x, columns correspond to y
# IMPORTANT: expand.grid(x,y) varies x fastest, then y
Cmat <- matrix(uCopA, nrow = length(x), ncol = length(y), byrow = FALSE)

z  <- make_z_surface_blocked(x, y, Cmat, mean_waiting_time_A, block=200L)

contourLines_A <- compute_contour_lines(
  return_periods  = rp_vec,
  x               = x,
  y               = y,
  z               = z,
  mu              = mean_waiting_time_A,
  stop_on_missing = TRUE   # set FALSE to skip missing RPs with a warning instead
)

Scenario B Return Period

# Mean waiting time is the average time, in days, between events
mean_waiting_time_B <- mu / rate_marengo

# Reshape into a matrix: rows correspond to x, columns correspond to y
# IMPORTANT: expand.grid(x,y) varies x fastest, then y
Cmat <- matrix(uCopB, nrow = length(x), ncol = length(y), byrow = FALSE)

z  <- make_z_surface_blocked(x, y, Cmat, mean_waiting_time_B, block=200L)

contourLines_B <- compute_contour_lines(
  return_periods  = rp_vec,
  x               = x,
  y               = y,
  z               = z,
  mu              = mean_waiting_time_B,
  stop_on_missing = TRUE   # set FALSE to skip missing RPs with a warning instead
)
write.csv(contourLines_A, "contourLines_A.csv")
write.csv(contourLines_B, "contourLines_B.csv")

Transform Variates to Original Scale

Scenario A Transform Variates

p_pds <- contourLines_A$marshalltown_cdf
p_marshalltown_A <- langbein_adjustment(rate_marshalltown, p_pds) # Annualized probabilities for Marshalltown

contourLines_A <- contourLines_A %>%
  mutate(marshalltown_annualized_p = p_marshalltown_A,
         marshalltown = quagev(marshalltown_annualized_p, marshalltown_gev),
         marengo = quaglo(marengo_cdf, marengo_glo),
         scenario = "A")

Scenario B Transform Variates

p_pds <- contourLines_B$marengo_cdf
p_marengo_B <- langbein_adjustment(rate_marengo, p_pds) # Annualized probabilities for Marengo

contourLines_B <- contourLines_B %>%
  mutate(marengo_annualized_p = p_marengo_B,
         marengo = quagev(marengo_annualized_p, marengo_gev),
         marshalltown = quaglo(marshalltown_cdf, marshalltown_glo),
         scenario = "B")

Plot Isolines

ggplot(contourLines_A, aes(marshalltown, marengo, group = rp)) +
  geom_path(linewidth = 1) +
  theme_bw() +
  xlab("Marshalltown Flow (cfs)") +
  ylab("Marengo Flow (cfs)") +
  ggtitle("Scenario A: Extremal Marshalltown Flows") +
  xlim(0, 80000) +
  ylim(0, 80000) +
  coord_fixed(ratio = 1) +
   geomtextpath::geom_textpath(
    aes(label = 1/rp, group = rp),
    color = "black", # label color
    linetype = 0,   # (or) linetype = 0
    linewidth = 4,
    text_smoothing = 10,     # helps spacing along the curve
    vjust = -0.1
  ) 

ggplot(contourLines_B, aes(marengo, marshalltown, group = rp)) +
  geom_path(linewidth = 1) +
  theme_bw() +
  xlab("Marengo Flow (cfs)") +
  ylab("Marshalltown Flow (cfs)") +
  ggtitle("Scenario B: Extremal Marengo Flows") +
  xlim(0, 80000) +
  ylim(0, 80000) +
  coord_fixed(ratio = 1) +
   geomtextpath::geom_textpath(
    aes(label = 1/rp, group = rp),
    color = "black", # label color
    linetype = 0,   # (or) linetype = 0
    linewidth = 4,
    text_smoothing = 10,     # helps spacing along the curve
    vjust = 1.2
  ) 
Warning: Removed 5 rows containing missing values or values outside the scale range
(`geom_path()`).
Warning: Removed 5 rows containing missing values or values outside the scale range
(`geom_textpath()`).

contourLines_A_common_cols <- contourLines_A %>%
  select(rp, marshalltown, marengo, scenario)

contourLines_B_common_cols <- contourLines_B %>%
  select(rp, marshalltown, marengo, scenario)

contourLines <- rbind(contourLines_A_common_cols, contourLines_B_common_cols) %>%
  mutate(aep = 1/rp)

ggplot(contourLines, aes(marshalltown, marengo, color = scenario, linetype = factor(aep))) +
  geom_path(linewidth = 1) +
    scale_color_manual(values = c(
    "A" = "darkgreen",
    "B" = "purple")) +
scale_linetype_manual(values = c(
  "0.1"   = "longdash",
  "0.02"  = "dashed",
  "0.01"  = "solid",
  "0.005" = "dotted"
)) +
  xlim(0, 80000) +
  ylim(0, 80000) +
  theme_bw() +
  labs(color = "Scenario", linetype = "Annual Exceedance Probability") +
  xlab("Marshalltown Flow (cfs)") +
  ylab("Marengo Flow (cfs)") +
  ggtitle("") +
  coord_fixed(ratio = 1)
Warning: Removed 5 rows containing missing values or values outside the scale range
(`geom_path()`).

# View 1/100 joint probability contour from scenario A
# contourLines[contourLines$scenario == "A" & contourLines$rp == "100", ]

Compute Envelope Curve

Combine Isolines into Envelope Curve

The make_envelope function takes a data frame containing isoline points for one return period, rp, but multiple curves, distinguished by scenario, and produces a new curve that is the upper envelope (for each x-value, it keeps the maximum y-value across all scenarios).

make_envelope <- function(df_rp, n_grid = 500L) {
  
  # Clean and sort input 
  # Drop rows where marshalltown (x) or marengo (y) are missing/non-finite
  # Sorts by marshalltown values
  df_rp <- df_rp %>%
    filter(is.finite(marshalltown), is.finite(marengo)) %>%
    arrange(marshalltown)

  # Create a common x-grid over the range covered by the isolines
  # Builds xg, a sequence from the minimum to maximum marshalltown in the data,        with n_grid points
  xg <- seq(min(df_rp$marshalltown), max(df_rp$marshalltown), length.out = n_grid)

  # Interpolate each scenario separately (safer than interpolating across mixed        curves)
  # Splits data by scenario
  # For each scenario:
    # Sorts by marshalltown
    # Removes duplicate x-values so interpolation behaves well
    # Use approx() to linearly interpolate marengo values at every xg
    # rule = 1 means no extrapolation (any xout < min(x) or xout > max(x) returns      NA)
  y_by_curve <- df_rp %>%
    group_by(scenario) %>%
    group_split() %>%
    map(function(d) {
      d <- d %>% arrange(marshalltown)
      # approx requires unique x to behave well
      d <- d %>% distinct(marshalltown, .keep_all = TRUE)

      approx(x = d$marshalltown, y = d$marengo, xout = xg,
             rule = 1, ties = "ordered")$y
    })

  # Compute the envelope = pointwise maximum across curves
  yenv <- do.call(pmax, c(y_by_curve, list(na.rm = TRUE)))

  # Return a table
  tibble(
    rp = unique(df_rp$rp),
    aep = unique(df_rp$aep),
    marshalltown = xg,
    marengo = yenv) %>%
    dplyr::filter(marshalltown >= 0, marengo >= 0)
}

Apply make_envelope function to combine isolines in contourLines into an envelope curve.

envelope_curves <- contourLines %>%
  group_by(aep) %>%
  group_split() %>%
  map_dfr(make_envelope)

envelope_curves
# A tibble: 1,934 × 4
      rp   aep marshalltown marengo
   <dbl> <dbl>        <dbl>   <dbl>
 1   200 0.005         2.09  48665.
 2   200 0.005        63.0   48665.
 3   200 0.005       124.    48665.
 4   200 0.005       185.    48665.
 5   200 0.005       246.    48665.
 6   200 0.005       307.    48665.
 7   200 0.005       367.    48665.
 8   200 0.005       428.    48665.
 9   200 0.005       489.    48665.
10   200 0.005       550.    48665.
# ℹ 1,924 more rows

Plot Individual Contour Lines and Envelope Curve

contourLinesPlot <- ggplot() +
  geom_path(
    data = contourLines,
    aes(marshalltown, marengo, color = scenario, linetype = factor(aep)),
    linewidth = 0.8, alpha = 0.7
  ) +
  geom_path(
    data = envelope_curves,
    aes(marshalltown, marengo, linetype = factor(aep)),
    color = "black", linewidth = 1.2
  ) +
  scale_color_manual(values = c(
    "A" = "darkgreen",
    "B" = "purple"
  )) +
scale_linetype_manual(values = c(
  "0.1"   = "longdash",
  "0.02"  = "dashed",
  "0.01"  = "solid",
  "0.005" = "dotted"
)) +
  theme_bw() +
  coord_fixed(ratio = 1) +
  xlim(0, 80000) +
  ylim(0, 80000) +
  labs(
    color = "Scenario",
    linetype = "Annual Exceedance Probability",
    x = "Marshalltown Flow (cfs)",
    y = "Marengo Flow (cfs)"
  )

contourLinesPlot
Warning: Removed 5 rows containing missing values or values outside the scale range
(`geom_path()`).

Compute Relative Density Along Isolines

KDE Along Isoline

Compute the KDE values along an isoline in flow space using your objects:

  • Iso: data.frame/tibble with columns marshalltown, marengo (one RP + one scenario, or already merged)

  • cop_sample: your simulated samples data.frame with columns Marshalltown, Marengo (flow space)

  • Data: optional observed data in flow space with same two columns; used when Source=“Data”

  • Source: “Sample” (default; uses copula simulations) or “Data” (uses observed pairs)

  • Sim_Max: truncation multiplier (same intent as your original function)

    • With very large simulations, heavy-tailed marginals can generate rare but enormous values. Sim_Max applies a guardrail by keeping the KDE focused on the range relevant to your contour/isoline or observed data.
kde_along_isoline <- function(Iso,
                              cop_sample,
                              Data = NULL,
                              Source = c("Sample", "Data"),
                              Sim_Max = 10,
                              gridsize = c(200, 200)) {

  Source <- match.arg(Source)

  # Keep exactly these rows and order
  Iso2 <- Iso %>% dplyr::select(marshalltown, marengo)

  # eval.points must be an n×2 numeric matrix for ks::kde
  Iso_mat <- as.matrix(Iso2)
  storage.mode(Iso_mat) <- "double"
  
  # ---- Print Iso_mat (or a subset if it's huge) ----
  #message("Iso_mat preview (first 10 rows):")
  #print(utils::head(Iso_mat, 10))
  #message("Iso_mat dimensions: ", paste(dim(Iso_mat), collapse = " x "))
  # ---------------------------------------------------

  if (Source == "Sample") {
    xdat <- cop_sample %>% dplyr::select(Marshalltown, Marengo) %>% as.matrix()
    storage.mode(xdat) <- "double"

    ref <- if (!is.null(Data)) Data %>% dplyr::select(Marshalltown, Marengo) else Iso2

    remove <- which(
      xdat[,1] > Sim_Max * max(ref[[1]], na.rm=TRUE) |
      xdat[,2] > Sim_Max * max(ref[[2]], na.rm=TRUE)
    )
    if (length(remove) > 0) xdat <- xdat[-remove, , drop = FALSE]
  } else {
    if (is.null(Data)) stop("When Source='Data', supply `Data`.")
    xdat <- Data %>% dplyr::select(Marshalltown, Marengo) %>% na.omit() %>% as.matrix()
    storage.mode(xdat) <- "double"
  }

# ---- KDE fit (do this ONCE) ----
  fit <- ks::kde(x = xdat, eval.points = Iso_mat, gridsize = gridsize)
  est <- fit$estimate

  if (length(est) != nrow(Iso_mat)) {
    stop("KDE length mismatch: got ", length(est), " but Iso has ", nrow(Iso_mat), " rows.")
  }

  rng <- range(est, na.rm = TRUE)
  scaled <- if (is.finite(rng[1]) && is.finite(rng[2]) && rng[2] > rng[1]) {
    (est - rng[1]) / (rng[2] - rng[1])
  } else rep(NA_real_, length(est))


  print(fit$H, digits = 6)
  ev <- eigen(fit$H)
  #print(ev$values, digits = 6)
  #print(ev$vectors, digits = 6)
  
  list(
    prediction = est,
    contour = scaled,
    H = fit$H,
    eigen = ev
  )
}

Find Design Event Along Each Isoline

Fine “design events” or “most likely events” on an isoline. The “most likely event” is the isoline point with the maximum KDE.

  • Iso: data.frame with marshalltown, marengo columns

  • prediction: KDE values returned by kde_along_isoline()$prediction

  • N_Ensemble: number of sampled ensemble points along isoline (weighted by prediction)

extract_design_events_2d <- function(Iso, prediction, N_Ensemble = 0) {

  Iso_mat <- Iso %>%
    dplyr::select(marshalltown, marengo) %>%
    as.data.frame()

  # Most likely = max KDE point on isoline
  idx <- which(prediction == max(prediction, na.rm = TRUE))[1]
  most <- data.frame(
    marshalltown = Iso_mat[idx, 1],
    marengo      = Iso_mat[idx, 2]
  )

  # Keep your previous "full dependence" behavior (max x and max y on isoline)
  full <- data.frame(
    marshalltown = max(Iso_mat[, 1], na.rm = TRUE),
    marengo      = max(Iso_mat[, 2], na.rm = TRUE)
  )

  ens <- NULL # Initialize ensemble output as NULL
  if (N_Ensemble > 0) {
    # Find indices of isoline points where the KDE values are finite and strictly positive
    ok <- which(is.finite(prediction) & prediction > 0)
    # If there are no valid KDE weights, return an empty 0-row matrix/data frame with the same number of columns as Iso_mat
    if (length(ok) == 0) {
      ens <- Iso_mat[0, , drop = FALSE]
    # Randomly select N_Ensemble indices from the valid set ok
    } else {
      # prob = prediction[ok] makes it weighted sampling: higher KDE density -> more likely to be chosen
      draw <- sample(ok, size = N_Ensemble, replace = TRUE, prob = prediction[ok])
      # Extracts the sampled isoline coordinates corresponding to the chosen indices
      ens <- Iso_mat[draw, , drop = FALSE]
      # Label the two columns
      names(ens) <- c("marshalltown", "marengo")
    }
  }

  list(MostLikelyEvent = most, FullDependence = full, Ensemble = ens)
}

KDE and Design Event

compute_isoline_results <- function(
    return_periods,
    envelope_curves,
    copula_sample,
    N_Ensemble = 200,
    Source     = "Sample",
    Sim_Max    = 10,
    gridsize   = c(1000, 1000)
) {
  # ── 1. Filter to requested RPs and split into a list of data frames ──────────
  iso_list <- envelope_curves %>%
    dplyr::filter(rp %in% return_periods) %>%
    dplyr::group_by(rp) %>%
    dplyr::group_split()

  # Name each element by its RP value for easy downstream access
  rp_vals       <- purrr::map_dbl(iso_list, ~ unique(.x$rp))
  names(iso_list) <- as.character(rp_vals)

  # ── 2. For each RP: KDE along isoline + most-likely design event ─────────────
  purrr::map(iso_list, function(df_iso) {

    kde_out <- kde_along_isoline(
      Iso        = df_iso,
      cop_sample = copula_sample,
      Source     = Source,
      Sim_Max    = Sim_Max,
      gridsize   = gridsize
    )

    stopifnot(length(kde_out$prediction) == nrow(df_iso))

    design <- extract_design_events_2d(
      df_iso,
      kde_out$prediction,
      N_Ensemble = N_Ensemble
    )

    list(
      isoline      = df_iso %>%
                       dplyr::mutate(
                         kde        = kde_out$prediction,
                         kde_scaled = kde_out$contour
                       ),
      kde_out      = kde_out,
      design       = design,
      design_event = design$MostLikelyEvent
    )
  })
}

# ── Run everything in one call ─────────────────────────────────────────────────
iso_results <- compute_isoline_results(
  return_periods  = rp_vec,
  envelope_curves = envelope_curves,
  copula_sample   = copula_sample,
  N_Ensemble      = 200,
  gridsize        = c(400, 400)
)
        [,1]    [,2]
[1,] 35914.2 28599.8
[2,] 28599.8 59004.3
        [,1]    [,2]
[1,] 42744.1 40888.3
[2,] 40888.3 97130.6
Warning in kdde.binned.nd(H = H, deriv.order = r, bin.par = bin.par, verbose =
verbose, : Binning grid may be too coarse for current (small) bandwidth:
consider increasing grid size for dimensions 1, 2
        [,1]     [,2]
[1,] 5175.74  4986.44
[2,] 4986.44 12267.31
Warning in kdde.binned.nd(H = H, deriv.order = r, bin.par = bin.par, verbose =
verbose, : Binning grid may be too coarse for current (small) bandwidth:
consider increasing grid size for dimensions 1, 2
        [,1]     [,2]
[1,] 4636.13  4479.26
[2,] 4479.26 11024.05
# ── Flatten all KDE-annotated isolines into one tidy data frame ───────────────
contourLines_kde <- purrr::map_dfr(
  iso_results,
  ~ .x$isoline,
  .id = "rp"                        # adds a character column "rp"
) %>%
  dplyr::mutate(rp = as.numeric(rp),
                aep = 1/rp)

# ── Flatten all design events into one data frame ─────────────────────────────
design_events_all <- purrr::map_dfr(
  iso_results,
  ~ .x$design_event,
  .id = "rp"
) %>%
  dplyr::mutate(rp = as.numeric(rp))

Demo Figures

Observed Samples

marshalltown_con_data <- marshalltown_con %>%
  mutate(Scenario = "Marshalltown_Conditioning")

marengo_con_data <- marengo_con %>%
  mutate(Scenario = "Marengo_Conditioning")

data <- rbind(marshalltown_con_data, marengo_con_data) %>%
  select(Marshalltown, Marengo, Scenario)

ggplot() +
  geom_point(
    data = data,
    aes(x = Marshalltown, y = Marengo, shape = Scenario, color = Scenario),
    inherit.aes = FALSE,
    size = 2, stroke = 1
  ) +
  scale_shape_manual(
    values = c(Marshalltown_Conditioning = 1, Marengo_Conditioning = 4),
    name = "Scenario"
  ) +
  scale_color_manual(
    values = c(Marshalltown_Conditioning = "darkgreen", Marengo_Conditioning = "purple"),
    name = "Scenario"
  ) +
  theme_bw() + coord_fixed(1) +
  xlim(0, 80000) + ylim(0, 80000) +
  labs(x = "Marshalltown Flow (cfs)", y = "Marengo Flow (cfs)")

Observed Samples and Randomly Generated Samples

ggplot() +
  geom_point(
    data = dplyr::slice_sample(copula_sample, n = 10000),   # <-- random sample
    aes(x = Marshalltown, y = Marengo),
    inherit.aes = FALSE,
    color = "darkgrey", shape = 21
  ) +
  geom_point(
    data = data,
    aes(x = Marshalltown, y = Marengo, shape = Scenario, color = Scenario),
    inherit.aes = FALSE,
    size = 2, stroke = 1
  ) +
  scale_shape_manual(
    values = c(Marshalltown_Conditioning = 1, Marengo_Conditioning = 4),
    name = "Scenario"
  ) +
  scale_color_manual(
    values = c(Marshalltown_Conditioning = "darkgreen", Marengo_Conditioning = "purple"),
    name = "Scenario"
  ) +
  theme_bw() + coord_fixed(1) +
  xlim(0, 80000) + ylim(0, 80000) +
  labs(x = "Marshalltown Flow (cfs)", y = "Marengo Flow (cfs)")
Warning: Removed 24 rows containing missing values or values outside the scale range
(`geom_point()`).

library(MASS)

Attaching package: 'MASS'
The following object is masked from 'package:dplyr':

    select
library(plotly)

Attaching package: 'plotly'
The following object is masked from 'package:MASS':

    select
The following object is masked from 'package:ggplot2':

    last_plot
The following object is masked from 'package:stats':

    filter
The following object is masked from 'package:graphics':

    layout
# ── Estimate 2D kernel density ─────────────────────────────────────────────────
kde <- MASS::kde2d(
  x    = copula_sample$Marshalltown,
  y    = copula_sample$Marengo,
  n    = 200,                                   # grid resolution
  lims = c(c(0, 150000),
           c(0, 150000))
)

# ── Interactive 3D surface ─────────────────────────────────────────────────────
plotly::plot_ly() %>%
  plotly::add_surface(
    x            = kde$x,
    y            = kde$y,
    z            = kde$z,
    colorscale   = "RdYlBu",
    reversescale = FALSE
  ) %>%
  plotly::layout(
    title = "Copula Sample — Joint Density",
    scene = list(
      xaxis = list(title = "Marshalltown Flow (cfs)"),
      yaxis = list(title = "Marengo Flow (cfs)"),
      zaxis = list(title = "Density")
    )
  )
contourLines_kde <- contourLines_kde %>%
  dplyr::group_by(rp) %>%
  dplyr::mutate(
    aep        = format(1 / .data$rp, scientific = FALSE),
    path_group = paste(.data$rp, dplyr::cur_group_id(), sep = "_")
  ) %>%
  dplyr::ungroup()

ggplot() +
  geom_point(
    data = dplyr::slice_sample(copula_sample, n = 10000),   # <-- random sample
    aes(x = Marshalltown, y = Marengo),
    inherit.aes = FALSE,
    color = "darkgrey", shape = 21
  ) +
  geom_path(data = contourLines_kde, aes(x = marshalltown, y = marengo, group = aep, color = kde_scaled), linewidth = 1.2) +
  scale_color_distiller(
    palette = "RdYlBu", direction = -1,
    name = "Relative Probability Density", limits = c(0, 1)
  ) +
  ggnewscale::new_scale_color() +
  geom_point(
    data = data,
    aes(x = Marshalltown, y = Marengo, shape = Scenario, color = Scenario),
    inherit.aes = FALSE,
    size = 2, stroke = 1
  ) +
  scale_shape_manual(
    values = c(Marshalltown_Conditioning = 1, Marengo_Conditioning = 4),
    name = "Scenario"
  ) +
  scale_color_manual(
    values = c(Marshalltown_Conditioning = "darkgreen", Marengo_Conditioning = "purple"),
    name = "Scenario"
  ) +
  geomtextpath::geom_textpath(
    data = contourLines_kde,
    aes(x = marshalltown, y = marengo,
        label = aep, group = path_group),       # <-- pre-computed character column
    color          = "black",
    linetype       = 0,
    size           = 4,
    text_smoothing = 10,
    vjust          = 1
  ) +
  theme_bw() + coord_fixed(1) +
  xlim(0, 80000) + ylim(0, 80000) +
  labs(x = "Marshalltown Flow (cfs)", y = "Marengo Flow (cfs)")
Warning: Removed 20 rows containing missing values or values outside the scale range
(`geom_point()`).

Final Plot

Plot the envelope isolines colored by relative density (KDE-scaled), with return period labels along the curve.

  • Overlays:

    • a subset of simulated pairs (grey),

    • observed coincident-event pairs from both scenarios (different shapes/colors),

    • the “most likely event” marker on the isoline.

marshalltown_con_data <- marshalltown_con %>%
  mutate(Scenario = "Marshalltown_Conditioning")

marengo_con_data <- marengo_con %>%
  mutate(Scenario = "Marengo_Conditioning")

data <- rbind(marshalltown_con_data, marengo_con_data) %>%
  select(Marshalltown, Marengo, Scenario)


contourLines_kde <- contourLines_kde %>%
  dplyr::group_by(rp) %>%
  dplyr::mutate(
    aep        = format(1 / .data$rp, scientific = FALSE),
    path_group = paste(.data$rp, dplyr::cur_group_id(), sep = "_")
  ) %>%
  dplyr::ungroup()

ggplot() +
  geom_point(
    data = dplyr::slice_sample(copula_sample, n = 10000),   # <-- random sample
    aes(x = Marshalltown, y = Marengo),
    inherit.aes = FALSE,
    color = "darkgrey", shape = 21
  ) +
  geom_path(data = contourLines_kde, aes(x = marshalltown, y = marengo, group = aep, color = kde_scaled), linewidth = 1.2) +
  scale_color_distiller(
    palette = "RdYlBu", direction = -1,
    name = "Relative Probability Density", limits = c(0, 1)
  ) +
  ggnewscale::new_scale_color() +
  geom_point(
    data = data,
    aes(x = Marshalltown, y = Marengo, shape = Scenario, color = Scenario),
    inherit.aes = FALSE,
    size = 2, stroke = 1
  ) +
  scale_shape_manual(
    values = c(Marshalltown_Conditioning = 1, Marengo_Conditioning = 4),
    name = "Scenario"
  ) +
  scale_color_manual(
    values = c(Marshalltown_Conditioning = "darkgreen", Marengo_Conditioning = "purple"),
    name = "Scenario"
  ) +
  geom_point(
    data = design_events_all,                                         # <-- single data frame
    aes(x = marshalltown, y = marengo, fill = "Design Event"),
    inherit.aes = FALSE,
    color = "black", shape = 23, linewidth = 2, stroke = 1.1
  ) +
  geomtextpath::geom_textpath(
    data = contourLines_kde,
    aes(x = marshalltown, y = marengo,
        label = aep, group = path_group),       # <-- pre-computed character column
    color          = "black",
    linetype       = 0,
    size           = 4,
    text_smoothing = 10,
    vjust          = 1
  ) +
  scale_fill_manual(values = c("Design Event" = "red"), name = "") +
  theme_bw() + coord_fixed(1) +
  xlim(0, 80000) + ylim(0, 80000) +
  labs(x = "Marshalltown Flow (cfs)", y = "Marengo Flow (cfs)")
Warning in geom_point(data = design_events_all, aes(x = marshalltown, y =
marengo, : Ignoring unknown parameters: `linewidth`
Warning: Removed 18 rows containing missing values or values outside the scale range
(`geom_point()`).