Tail dependence and joint extremes

R
extreme value theory
conservation
simulation
ecology tutorial
A correlation from the middle of a distribution says nothing about the corners. Simulating copulas in R to measure joint extreme risk in a reserve network.
Author

Tidy Ecology

Published

2026-08-01

Eight calcareous fens lie along one river catchment, each holding a population of the marsh fritillary, and each surveyed every September since the middle of the nineteen eighties by counting larval webs. A gauge at each fen logs the summer water deficit. Forty summers, eight sites, and the question the catchment partnership keeps asking is not about any one fen. It is whether the whole set can go under in the same year.

A bad summer at one fen is survivable. The webs disappear from that site and the fen fills in again over the following few years from a neighbour that was fine. A summer bad enough at every fen at once is a different event: nothing is left to recolonise from, and the network can be lost in a single year without any individual site having been more than ordinarily unlucky. The number the partnership needs is the annual probability of that joint event. Its two inputs are the site-level drought risk, which the gauge record supplies, and the dependence between the sites, which it does not.

Four posts here already treat the first input. Block maxima and the GEV fits a distribution to annual maxima, peaks over threshold keeps every exceedance instead of one per year, return levels and uncertainty puts an honest interval around the hundred-year event, and checking an extreme value model asks whether the fit deserves any of it. All four are univariate. None of them can reach the question above, because a return level for one fen says nothing whatever about what the fen four kilometres away is doing in the same summer, and no amount of extra care with the marginal fit will change that.

Dependence between populations is an old ecological subject under a different name. Liebhold, Koenig and Bjornstad (2004) review spatial synchrony, which arises from dispersal between populations, from a shared environmental driver (the Moran effect), or from predators and pathogens moving between sites; the measurement in that literature is almost always a correlation. The same question turns up in climate science as compound events, where Zscheischler and Seneviratne (2017) show that changing the dependence between two drivers moves the risk of the joint event far more than changing either driver on its own.

The dependence input is a copula question. Copulas for dependent ecological data builds the machinery, and closes on the observation that a Gaussian, a Clayton and a Gumbel copula tuned to the same Kendall’s tau put their mass in visibly different corners. This post takes that observation and makes it the whole subject, because in the corners is exactly where a reserve network lives or dies.

There is also a direct debt to SLOSS and reserve configuration, which ran the portfolio argument through a correlation coefficient and found the value at which several small stops beating single large. The addition here is that the interpolation between independent blocks and perfectly synchronised ones is not fixed by the correlation. Two networks can agree on every correlation you can measure and still differ by an order of magnitude in the probability that every block fails together, and the Gaussian assumption, which is what a multivariate normal simulation quietly imposes, sits at the optimistic end of that range.

Four measurements follow: the coefficient of tail dependence estimated and compared against theory for three copulas at one Kendall’s tau, the joint failure probability for the eight fens under each of them, the amount of data needed to tell the models apart, and the threshold at which a Gaussian copula finally admits to being asymptotically independent.

library(ggplot2)

te_pal <- list(forest = "#275139", green = "#2f8f63", sage = "#93a87f",
               clay = "#b5534e", gold = "#cda23f", line = "#dad9ca",
               ink = "#16241d", paper = "#f5f4ee")

theme_te <- function() {
  theme_minimal(base_size = 12) +
    theme(panel.grid.minor = element_blank(),
          panel.grid.major = element_line(colour = "#e7e6dc"),
          plot.background = element_rect(fill = "#f5f4ee", colour = NA),
          panel.background = element_rect(fill = "#f5f4ee", colour = NA),
          plot.title = element_text(face = "bold", colour = te_pal$ink),
          axis.title = element_text(colour = "#2c3a31"),
          axis.text = element_text(colour = "#2c3a31"))
}

The quantity, and why correlation is not it

Write \(U\) and \(V\) for two variables mapped onto the unit interval by their own distribution functions, so that \(U\) is the quantile the first variable reached this year and \(V\) the quantile the second reached. The upper coefficient of tail dependence is

\[\lambda_U = \lim_{u \to 1} \Pr(V > u \mid U > u)\]

and the lower coefficient is the same construction at the other end, \(\lambda_L = \lim_{u \to 0} \Pr(V < u \mid U < u)\). In words: given that one site had a year in its worst one per cent, how often was the other site also in its worst one per cent, and what does that fraction settle down to as the threshold is pushed out. Sibuya (1960) wrote the quantity down; Coles, Heffernan and Tawn (1999) turned it into a diagnostic that gets used.

Two properties make it the right object. It is a function of the copula alone, so it does not move when the marginal distributions are rescaled, log transformed or replaced with something else entirely. And it is a statement about the corner rather than the middle, which a correlation coefficient never is. A Pearson correlation is an average over the whole joint distribution, and almost all of the mass it averages over sits nowhere near the tail.

The awkward part is that \(\lambda_U\) can be zero for a joint distribution that looks strongly dependent everywhere you can see. That case has a name, asymptotic independence, and Ledford and Tawn (1996) built the machinery for handling it. The Gaussian copula is the standard example, and it is also the default in every multivariate normal simulation an ecologist writes without thinking about it.

Three copulas at one Kendall’s tau

All three copulas used here can be generated the same way: draw one latent factor per year that all sites share, then draw one independent shock per site. That construction is what makes the simulation short and, later, what makes the risk probabilities available in closed form.

For the Gaussian copula with exchangeable correlation \(\rho\), the latent factor is a standard normal \(W\) and site \(i\) gets \(Z_i = \sqrt{\rho}\,W + \sqrt{1-\rho}\,\varepsilon_i\), pushed through the normal distribution function. For the two Archimedean copulas the factor is a positive random variable \(V\) acting as a frailty, and \(U_i\) is a decreasing function of an exponential shock divided by \(V\); Marshall and Olkin (1988) set this out in general. The Gumbel case needs \(V\) to be positive stable with index \(1/\theta\), which is where the Chambers, Mallows and Stuck (1976) algorithm comes in; the Clayton case needs only a gamma draw.

rcop_gauss <- function(n, d, rho) {
  z <- sqrt(rho) * matrix(rnorm(n), n, d) +
    sqrt(1 - rho) * matrix(rnorm(n * d), n, d)
  pnorm(z)
}

rstable_pos <- function(n, alpha) {
  w <- runif(n, 0, pi)
  ex <- rexp(n)
  (sin(alpha * w) / (sin(w))^(1 / alpha)) *
    (sin((1 - alpha) * w) / ex)^((1 - alpha) / alpha)
}

rcop_gumbel <- function(n, d, theta) {
  fr <- rstable_pos(n, 1 / theta)
  ex <- matrix(rexp(n * d), n, d)
  exp(-(ex / fr)^(1 / theta))
}

rcop_clayton <- function(n, d, theta) {
  fr <- rgamma(n, shape = 1 / theta, rate = 1)
  ex <- matrix(rexp(n * d), n, d)
  (1 + ex / fr)^(-1 / theta)
}

The three are matched on Kendall’s tau, which is a rank statistic and therefore a property of the copula rather than of the marginals. For a Gaussian copula \(\tau = (2/\pi)\arcsin\rho\); for Gumbel \(\tau = 1 - 1/\theta\); for Clayton \(\tau = \theta/(\theta + 2)\). Fix the Gaussian correlation, read off the tau, and invert the other two.

rho_base <- 0.8
tau_base <- 2 / pi * asin(rho_base)
th_gum <- 1 / (1 - tau_base)
th_clay <- 2 * tau_base / (1 - tau_base)

lam_gum_up <- 2 - 2^(1 / th_gum)
lam_clay_low <- 2^(-1 / th_clay)

set.seed(20260801)
n_tau <- 4000
tau_meas <- c(
  gaussian = cor(rcop_gauss(n_tau, 2, rho_base), method = "kendall")[1, 2],
  gumbel = cor(rcop_gumbel(n_tau, 2, th_gum), method = "kendall")[1, 2],
  clayton = cor(rcop_clayton(n_tau, 2, th_clay), method = "kendall")[1, 2])

print(round(c(rho = rho_base, kendall_tau = tau_base,
              gumbel_theta = th_gum, clayton_theta = th_clay,
              gumbel_upper_limit = lam_gum_up,
              clayton_lower_limit = lam_clay_low), 5))
                rho         kendall_tau        gumbel_theta       clayton_theta 
            0.80000             0.59033             2.44102             2.88203 
 gumbel_upper_limit clayton_lower_limit 
            0.67162             0.78623 
print(round(tau_meas, 5))
gaussian   gumbel  clayton 
 0.59497  0.59953  0.58500 

A Gaussian correlation of 0.8 is a Kendall’s tau of 0.5903. That tau needs a Gumbel parameter of 2.441 and a Clayton parameter of 2.882, and the samplers reproduce it: 0.595, 0.5995 and 0.585 from 4000 draws each. Every rank-based summary a report would print about these three datasets agrees to within sampling error.

Their tail limits do not. The Gumbel copula has upper coefficient \(\lambda_U = 2 - 2^{1/\theta} =\) 0.6716 and lower coefficient zero. Clayton is the mirror image: lower coefficient \(2^{-1/\theta} =\) 0.7862 and upper coefficient zero. The Gaussian has zero at both ends for any correlation short of one.

Two of those are exact for every threshold, not only in the limit, because the diagonal of an Archimedean copula is a closed form. The Gaussian needs a one-dimensional integral, the inner normal probability integrated against the outer normal density.

lam_gauss_pex <- function(pex, rho) {
  ss <- sqrt(1 - rho^2)
  sapply(pex, function(pp) {
    zz <- qnorm(pp, lower.tail = FALSE)
    inner <- integrate(function(ee)
      exp(-zz * ee - ee^2 / 2) *
        pnorm((zz - rho * (zz + ee)) / ss, lower.tail = FALSE),
      0, Inf, rel.tol = 1e-10, subdivisions = 500L)$value
    dnorm(zz) * inner / pp
  })
}
lam_gauss_up <- function(uq, rho) lam_gauss_pex(1 - uq, rho)
lam_gum_up_at <- function(uq, theta) (1 - 2 * uq + uq^(2^(1 / theta))) / (1 - uq)
lam_gum_low_at <- function(uq, theta) (1 - uq)^(2^(1 / theta) - 1)
lam_clay_low_at <- function(uq, theta) {
  qq <- 1 - uq
  (2 * qq^(-theta) - 1)^(-1 / theta) / qq
}
lam_clay_up_at <- function(uq, theta) {
  cc <- (2 * uq^(-theta) - 1)^(-1 / theta)
  (1 - 2 * uq + cc) / (1 - uq)
}
uq_show <- c(0.5, 0.9, 0.95, 0.99, 0.995)
print(round(data.frame(
  threshold = uq_show,
  gauss_upper = lam_gauss_up(uq_show, rho_base),
  gumbel_upper = lam_gum_up_at(uq_show, th_gum),
  gumbel_lower = lam_gum_low_at(uq_show, th_gum),
  clayton_lower = lam_clay_low_at(uq_show, th_clay),
  clayton_upper = lam_clay_up_at(uq_show, th_clay)), 4))
  threshold gauss_upper gumbel_upper gumbel_lower clayton_lower clayton_upper
1     0.500      0.7952       0.7964       0.7964        0.8056        0.8056
2     0.900      0.5624       0.6939       0.4695        0.7864        0.3022
3     0.950      0.4951       0.6827       0.3739        0.7863        0.1698
4     0.990      0.3769       0.6738       0.2204        0.7862        0.0377
5     0.995      0.3373       0.6727       0.1755        0.7862        0.0191

The empirical estimator is a count. Rank each margin, convert to pseudo-observations, and take the fraction of the points above the threshold in one margin that are also above it in the other; Schmidt and Stadtmuller (2006) give its properties. Nothing about the estimator knows which copula generated the data.

lam_emp <- function(U, uq, tail) {
  n <- nrow(U)
  aa <- rank(U[, 1]) / (n + 1)
  bb <- rank(U[, 2]) / (n + 1)
  if (tail == "upper") {
    m <- sum(aa > uq)
    if (m == 0) return(NA_real_)
    sum(aa > uq & bb > uq) / m
  } else {
    qq <- 1 - uq
    m <- sum(aa < qq)
    if (m == 0) return(NA_real_)
    sum(aa < qq & bb < qq) / m
  }
}

set.seed(881204)
n_big <- 100000L
big_gauss <- rcop_gauss(n_big, 2, rho_base)
big_gum <- rcop_gumbel(n_big, 2, th_gum)
big_clay <- rcop_clayton(n_big, 2, th_clay)

uq_grid <- c(0.5, 0.7, 0.8, 0.9, 0.95, 0.98, 0.99, 0.995)
emp_tab <- data.frame(
  threshold = uq_grid,
  gauss_up = sapply(uq_grid, function(pt) lam_emp(big_gauss, pt, "upper")),
  gauss_low = sapply(uq_grid, function(pt) lam_emp(big_gauss, pt, "lower")),
  gum_up = sapply(uq_grid, function(pt) lam_emp(big_gum, pt, "upper")),
  gum_low = sapply(uq_grid, function(pt) lam_emp(big_gum, pt, "lower")),
  clay_up = sapply(uq_grid, function(pt) lam_emp(big_clay, pt, "upper")),
  clay_low = sapply(uq_grid, function(pt) lam_emp(big_clay, pt, "lower")))
print(round(emp_tab, 4))
  threshold gauss_up gauss_low gum_up gum_low clay_up clay_low
1     0.500   0.7973    0.7973 0.7973  0.7973  0.8075   0.8075
2     0.700   0.7095    0.7055 0.7421  0.6728  0.6343   0.7892
3     0.800   0.6488    0.6458 0.7156  0.5909  0.4965   0.7827
4     0.900   0.5632    0.5605 0.6930  0.4743  0.3037   0.7834
5     0.950   0.4892    0.4872 0.6808  0.3800  0.1686   0.7866
6     0.980   0.4235    0.4145 0.6640  0.2775  0.0740   0.7925
7     0.990   0.3940    0.3590 0.6690  0.2150  0.0340   0.7860
8     0.995   0.3340    0.3460 0.6840  0.1700  0.0200   0.7920
err_max <- max(abs(c(
  emp_tab$gauss_up - lam_gauss_up(uq_grid, rho_base),
  emp_tab$gum_up - lam_gum_up_at(uq_grid, th_gum),
  emp_tab$clay_low - lam_clay_low_at(uq_grid, th_clay))))
n_deep <- n_big * (1 - max(uq_grid))
print(round(c(n = n_big, max_abs_error_vs_theory = err_max,
              points_above_deepest_threshold = n_deep), 5))
                             n        max_abs_error_vs_theory 
                      1.00e+05                       1.71e-02 
points_above_deepest_threshold 
                      5.00e+02 

At 100000 draws the empirical curves sit within 0.0171 of the theoretical ones everywhere on the grid, the worst case being the deepest threshold where only 500 points are left to count. The estimator is doing its job, and any disagreement further on is a property of the models rather than of the code. At a threshold of 0.95 the Gaussian upper estimate is 0.4892, the Gumbel is 0.6808 and the Clayton is 0.1686. Push to 0.995 and the Gaussian has fallen to 0.334 while the Gumbel has barely moved, at 0.684 against its limit of 0.6716. The Clayton, which carries the strongest tail dependence of the three in the lower corner at 0.792, has almost nothing left in the upper one: 0.02.

Two panels side by side, one labelled lower tail and one labelled upper tail, sharing a horizontal axis of threshold quantile marked 0.5, 0.9, 0.95, 0.99 and 0.995 on a stretched scale, and a vertical axis from zero to one. In the lower panel a red Clayton curve stays almost flat near 0.79 across the whole range, a dark green Gaussian curve slopes down from 0.79 to about 0.35, and a gold Gumbel curve drops steeply to about 0.18. In the upper panel the roles of red and gold swap: the gold Gumbel curve flattens near 0.67 while the red Clayton curve plunges to near 0.02, and the dark green Gaussian curve follows the same path it took in the other panel. Small filled circles from simulation sit on every line.
Figure 1: Conditional exceedance probability against threshold for three copulas matched on Kendall’s tau, in the lower tail and the upper tail. Points are estimates from one hundred thousand simulated years; lines are the exact curves. Clayton holds its level in the lower tail, Gumbel holds its level in the upper tail, and the Gaussian falls away in both while never reaching zero anywhere on the plotted range.

Eight fens and one bad summer

Now the calculation the partnership wants. Call a summer bad at a fen if its water deficit lands in the worst 2 per cent of years for that fen, which is a one in fifty event locally. Nothing in what follows depends on the marginal distribution of the deficit: the threshold is defined by quantile, so the answer is a copula quantity from beginning to end.

Because all three copulas are one-factor constructions, the joint probability that every fen exceeds its threshold has a closed form. Conditional on the shared factor the sites are independent, so the probability is the expectation of a product. For the Gaussian that expectation is a one-dimensional integral over the latent normal. For the two Archimedean copulas an inclusion-exclusion expansion collapses onto the diagonal of the copula and gives a finite alternating sum, the Gumbel one being

\[\Pr(\text{all } d \text{ exceed } u) = \sum_{k=0}^{d} (-1)^k \binom{d}{k} u^{k^{1/\theta}}.\]

n_fen <- 8
p_bad <- 0.02

p_all_gauss <- function(pex, d, rho) {
  zz <- qnorm(pex, lower.tail = FALSE)
  integrate(function(w)
    dnorm(w) * pnorm((zz - sqrt(rho) * w) / sqrt(1 - rho), lower.tail = FALSE)^d,
    -Inf, Inf, rel.tol = 1e-12)$value
}
p_all_gum <- function(pex, d, theta) {
  uq <- 1 - pex
  kk <- 0:d
  sum((-1)^kk * choose(d, kk) * uq^(kk^(1 / theta)))
}
p_all_clay <- function(pex, d, theta) {
  uq <- 1 - pex
  kk <- 0:d
  sum((-1)^kk * choose(d, kk) * (1 + kk * (uq^(-theta) - 1))^(-1 / theta))
}

joint_now <- c(
  independent = p_bad^n_fen,
  clayton = p_all_clay(p_bad, n_fen, th_clay),
  gaussian = p_all_gauss(p_bad, n_fen, rho_base),
  gumbel = p_all_gum(p_bad, n_fen, th_gum))
print(signif(joint_now, 5))
independent     clayton    gaussian      gumbel 
 2.5600e-14  1.1744e-07  1.9438e-03  9.3714e-03 
ratio_gg <- joint_now[["gumbel"]] / joint_now[["gaussian"]]
ratio_span <- joint_now[["gumbel"]] / joint_now[["clayton"]]
d_eff <- log(joint_now) / log(p_bad)
print(round(d_eff, 4))
independent     clayton    gaussian      gumbel 
     8.0000      4.0790      1.5959      1.1938 
print(round(c(gumbel_over_gaussian = ratio_gg, gumbel_over_clayton = ratio_span), 4))
gumbel_over_gaussian  gumbel_over_clayton 
              4.8211           79797.0071 

Under independence the eight fens fail together with probability 2.56e-14, which is the number a planner gets by multiplying and which nobody believes. The three copulas, all agreeing on Kendall’s tau, give 1.174e-07 for Clayton, 0.001944 for Gaussian and 0.009371 for Gumbel. The Gumbel answer is 4.82 times the Gaussian one and 79,800 times the Clayton one.

The last of those is the wide number and the first is the useful one. Clayton is upper-tail independent, so as a model of joint drought it is the optimistic extreme in the same way independence is, only less obviously; if a copula were selected by eye from a scatterplot of the middle of the data and it happened to come out Clayton-shaped, the extrapolation to joint drought would be wrong by that whole factor. The Gaussian is the case that matters in practice, because it is what you get by default. Assuming it when the truth is Gumbel understates the joint risk by a factor of 4.82 at this threshold, with the same correlation in both.

A cleaner way to say the same thing is to ask how many independent fens the network is behaving like. Solve \(p^{d_{\text{eff}}}\) for the joint probability and read off the exponent.

horizon <- 50
p_horizon <- 1 - (1 - joint_now)^horizon
print(round(p_horizon, 6))
independent     clayton    gaussian      gumbel 
   0.000000    0.000006    0.092703    0.375486 
sloss_rho_star <- 0.6367            # crossover correlation from the SLOSS post
tau_star <- 2 / pi * asin(sloss_rho_star)
th_star <- 1 / (1 - tau_star)
four_g <- p_all_gauss(p_bad, 4, sloss_rho_star)
four_u <- p_all_gum(p_bad, 4, th_star)
print(round(c(tau = tau_star, gumbel_theta = th_star,
              gumbel_upper_limit = 2 - 2^(1 / th_star)), 4))
               tau       gumbel_theta gumbel_upper_limit 
            0.4394             1.7838             0.5251 
print(signif(c(gaussian_four = four_g, gumbel_four = four_u,
               ratio = four_u / four_g,
               deff_gaussian = log(four_g) / log(p_bad),
               deff_gumbel = log(four_u) / log(p_bad)), 5))
gaussian_four   gumbel_four         ratio deff_gaussian   deff_gumbel 
    0.0013826     0.0077038     5.5719000     1.6830000     1.2439000 

Eight fens behave like 1.596 independent ones under the Gaussian copula and like 1.194 under the Gumbel, against 8 under independence and 4.079 under Clayton. That is the portfolio benefit, and its value depends on the tail structure by roughly the same amount that it depends on whether the network has two sites or four.

Over a 50 year planning horizon the annual probabilities compound into something a committee can act on: a 9.27 per cent chance of at least one total-failure summer under the Gaussian assumption against 37.55 per cent under the Gumbel. Those are different plans.

The SLOSS post put the crossover between four blocks and one at a correlation of 0.6367, the value above which splitting the reserve stops paying. At exactly that correlation, four blocks fail together with probability 0.001383 under a Gaussian copula and 0.007704 under a Gumbel one, a factor of 5.57. The crossover correlation is a real quantity and the post was right to chase it, but it is not sufficient: two catchments that agree on it can disagree by that factor on the event the reserve network exists to survive.

p_grid <- 10^seq(log10(0.2), log10(0.005), length.out = 25)
sweep_dat <- do.call(rbind, lapply(p_grid, function(pp) data.frame(
  pex = pp,
  Clayton = p_all_clay(pp, n_fen, th_clay),
  Gaussian = p_all_gauss(pp, n_fen, rho_base),
  Gumbel = p_all_gum(pp, n_fen, th_gum))))
sweep_dat$ratio <- sweep_dat$Gumbel / sweep_dat$Gaussian
print(round(sweep_dat[c(1, 9, 17, 25), ], 8))
          pex    Clayton   Gaussian     Gumbel    ratio
1  0.20000000 0.01925199 0.05669596 0.10111780 1.783510
9  0.05848035 0.00008281 0.00904531 0.02782977 3.076707
17 0.01709976 0.00000004 0.00155804 0.00800324 5.136725
25 0.00500000 0.00000000 0.00028043 0.00232900 8.305228
print(round(range(sweep_dat$ratio), 3))
[1] 1.784 8.305
Two panels sharing a horizontal axis of single-site event probability running from 0.2 on the left down to 0.005 on the right. The left panel has a vertical axis of log base ten joint failure probability running from about minus one at the top to below minus nine at the bottom: a gold Gumbel line and a dark green Gaussian line both slope gently downwards and stay less than one unit apart, while a red Clayton line starts between them and plunges to the bottom right corner. The right panel shows the effective number of independent fens on a linear axis from one to five: the gold line drifts down from about 1.4 to 1.15, the dark green line from about 1.8 to 1.55, and the red line climbs the other way from about 2.4 to almost five.
Figure 2: Probability that all eight fens have a bad summer in the same year, and the equivalent number of independent fens, plotted against how rare a bad summer is at a single site. All three copulas carry the same Kendall’s tau. The gap between the Gaussian and the Gumbel widens as the event gets rarer, which is the direction any reserve calculation is heading.

What it would take to see this in the data

The eight fens have forty summers of record. The obvious next move is to estimate the tail dependence coefficient from the data and let it decide between the models. This section measures whether that works.

Set the question up as a test. Under the null the truth is the Gaussian copula at correlation 0.8; under the alternative it is the Gumbel copula at the matching tau. Simulate many series of length \(n\) under each, compute the empirical coefficient at a threshold of 0.95, take the upper five per cent point of the null distribution as the critical value, and count how often the Gumbel case clears it.

uq_test <- 0.95
n_grid <- c(50, 100, 200, 400, 800, 1600, 3200)
n_rep_pow <- 2000

pow_np <- data.frame(n = n_grid, crit = NA_real_, power = NA_real_,
                     mean_null = NA_real_, mean_alt = NA_real_)
for (i in seq_along(n_grid)) {
  nn <- n_grid[i]
  set.seed(51000 + i)
  s_null <- replicate(n_rep_pow, lam_emp(rcop_gauss(nn, 2, rho_base), uq_test, "upper"))
  s_alt <- replicate(n_rep_pow, lam_emp(rcop_gumbel(nn, 2, th_gum), uq_test, "upper"))
  cv <- quantile(s_null, 0.95, na.rm = TRUE, names = FALSE)
  pow_np$crit[i] <- cv
  pow_np$power[i] <- mean(s_alt > cv, na.rm = TRUE)
  pow_np$mean_null[i] <- mean(s_null, na.rm = TRUE)
  pow_np$mean_alt[i] <- mean(s_alt, na.rm = TRUE)
}
print(round(pow_np, 4))
     n   crit  power mean_null mean_alt
1   50 1.0000 0.0000    0.4360   0.6290
2  100 0.8000 0.0640    0.4753   0.6652
3  200 0.7000 0.2460    0.4830   0.6656
4  400 0.6500 0.5070    0.4938   0.6768
5  800 0.6000 0.8635    0.4918   0.6791
6 1600 0.5625 0.9975    0.4942   0.6798
7 3200 0.5500 1.0000    0.4939   0.6825

The two means separate immediately: 0.483 under the Gaussian against 0.6656 under the Gumbel at 200 years. The separation is not the problem. The spread is. At 200 years a threshold of 0.95 leaves 10 exceedances to estimate a proportion from, and the power is 0.246.

A parametric comparison should do better, because it uses every point rather than the ones in the corner. Both copulas have closed-form densities. Fit each by maximising the pseudo-likelihood over its single parameter, and use the difference in maximised log-likelihood as the statistic.

dens_gauss <- function(uu, vv, rho) {
  aa <- qnorm(uu); bb <- qnorm(vv)
  exp(-(rho^2 * (aa^2 + bb^2) - 2 * rho * aa * bb) / (2 * (1 - rho^2))) / sqrt(1 - rho^2)
}
dens_gumbel <- function(uu, vv, theta) {
  xx <- -log(uu); yy <- -log(vv)
  aa <- (xx^theta + yy^theta)^(1 / theta)
  exp(-aa) * (xx * yy)^(theta - 1) / (uu * vv) * aa^(1 - 2 * theta) * (aa + theta - 1)
}

lr_stat <- function(U) {
  n <- nrow(U)
  aa <- rank(U[, 1]) / (n + 1)
  bb <- rank(U[, 2]) / (n + 1)
  fg <- optimize(function(rr) sum(log(dens_gauss(aa, bb, rr))), c(0.01, 0.995),
                 maximum = TRUE)
  fu <- optimize(function(tt) sum(log(dens_gumbel(aa, bb, tt))), c(1.01, 25),
                 maximum = TRUE)
  fu$objective - fg$objective
}

n_grid_lr <- c(20, 40, 60, 100, 200, 400)
pow_lr <- data.frame(n = n_grid_lr, crit = NA_real_, power = NA_real_)
for (i in seq_along(n_grid_lr)) {
  nn <- n_grid_lr[i]
  set.seed(71000 + i)
  s_null <- replicate(n_rep_pow, lr_stat(rcop_gauss(nn, 2, rho_base)))
  s_alt <- replicate(n_rep_pow, lr_stat(rcop_gumbel(nn, 2, th_gum)))
  cv <- quantile(s_null, 0.95, names = FALSE)
  pow_lr$crit[i] <- cv
  pow_lr$power[i] <- mean(s_alt > cv)
}
print(round(pow_lr, 4))
    n    crit  power
1  20  1.8163 0.1750
2  40  2.0800 0.3680
3  60  2.2075 0.5080
4 100  2.1544 0.6945
5 200  0.6788 0.9400
6 400 -5.1768 0.9990
n80 <- function(tab) {
  ok <- is.finite(tab$power)
  approx(tab$power[ok], log(tab$n[ok]), xout = 0.8)$y
}
n80_np <- exp(n80(pow_np))
n80_lr <- exp(n80(pow_lr))
print(round(c(years_needed_coefficient = n80_np, years_needed_likelihood = n80_lr,
              ratio = n80_np / n80_lr, record_length = 40), 1))
years_needed_coefficient  years_needed_likelihood                    ratio 
                   707.1                    134.7                      5.2 
           record_length 
                    40.0 

Reaching eighty per cent power needs about 707 years of annual data from the coefficient and about 135 years from the likelihood ratio, a factor of 5.2 between them. The record is forty years. Neither number is within reach, and the first one is not within reach of anything: a 707 year run of parallel gauge records at eight fens does not exist and will not exist.

This is a finding rather than a shortfall in the analysis, and it has a practical consequence. Selecting a copula from the data is not available for this problem, so the choice has to come from somewhere else, which means from the mechanism. If the fens dry out because a blocking high sits over the whole catchment for six weeks, that is a shared driver that acts hardest in the worst years and the upper-tail-dependent model is the honest default. If they dry out because each fen’s own small aquifer happens to run low, tail independence is defensible. The second consequence is that a single number should not be reported. Run the calculation under both copulas and report the range: the numbers above show that range is a factor of 4.82 at the eight-fen threshold, which is small enough to state and large enough to matter.

The likelihood ratio deserves a caution as well. Its advantage comes entirely from assuming that one of the two candidate models is correct. When neither is, it selects whichever is closer in the middle of the data, where nearly all of the likelihood lives, and then hands over that model’s tail limit as though it had been estimated.

Two rising curves on a warm off-white panel with a logarithmic horizontal axis of series length from twenty to about three thousand years and a vertical axis of power from zero to one. A gold curve for the likelihood ratio rises from about 0.15 at twenty years and crosses a dashed horizontal line at 0.8 near one hundred and thirty years. A dark green curve for the tail dependence coefficient stays near 0.05 until two hundred years, then climbs steeply and crosses the same dashed line near seven hundred years. A narrow shaded vertical strip at the far left marks forty years, well below both crossings, and open circles mark the two crossing points.
Figure 3: Power to distinguish a Gumbel copula from a Gaussian copula at the same Kendall’s tau, against the length of the annual series, for the empirical tail dependence coefficient at the 0.95 threshold and for a likelihood ratio between the two fitted copulas. The horizontal line is eighty per cent power and the vertical band marks the length of the fen record.

The threshold you can afford, and the one you would need

The Gaussian copula has zero tail dependence. Everything in the tables above says otherwise at every threshold anyone can estimate, and that is not a contradiction: the limit is approached, but slowly, and the approach happens outside the data.

Make the constraint explicit. Estimating a conditional exceedance probability needs some minimum number of points in the corner; take ten as a bare floor. A series of length \(n\) can then reach a threshold of \(u = 1 - 10/n\) and no further. Evaluate the exact Gaussian coefficient along that frontier.

rho_high <- 0.95
tau_high <- 2 / pi * asin(rho_high)
th_gum_high <- 1 / (1 - tau_high)
lam_gum_high <- 2 - 2^(1 / th_gum_high)

n_front <- 10^c(2, 3, 4, 6, 8, 12)
front <- data.frame(
  n = n_front,
  threshold = 1 - 10 / n_front,
  gauss_080 = lam_gauss_pex(10 / n_front, rho_base),
  gauss_095 = lam_gauss_pex(10 / n_front, rho_high))
print(format(front, digits = 4))
      n threshold gauss_080 gauss_095
1 1e+02     0.900   0.56243    0.7792
2 1e+03     0.990   0.37690    0.6699
3 1e+04     0.999   0.26347    0.5901
4 1e+06     1.000   0.13635    0.4735
5 1e+08     1.000   0.07341    0.3892
6 1e+12     1.000   0.02257    0.2729
need_n <- function(rho, target) {
  f_root <- function(ln) lam_gauss_pex(10 / exp(ln), rho) - target
  exp(uniroot(f_root, c(log(100), log(1e300)), tol = 1e-10)$root)
}
half_low <- lam_gum_up / 2
half_high <- lam_gum_high / 2
print(signif(c(rho_low_half_target = half_low,
               years_rho_low = need_n(rho_base, half_low),
               rho_high_half_target = half_high,
               years_rho_high = need_n(rho_high, half_high),
               years_rho_low_below_tenth = need_n(rho_base, 0.1),
               years_rho_high_below_tenth = need_n(rho_high, 0.1)), 4))
       rho_low_half_target              years_rho_low 
                 3.358e-01                  2.059e+03 
      rho_high_half_target             years_rho_high 
                 4.248e-01                  1.218e+07 
 years_rho_low_below_tenth years_rho_high_below_tenth 
                 9.776e+06                  7.950e+24 
lam40 <- c(lam_gauss_pex(10 / 40, rho_base), lam_gauss_pex(10 / 40, rho_high))
print(round(c(gauss_080_at_40y = lam40[1], gauss_095_at_40y = lam40[2],
              gumbel_limit_080 = lam_gum_up, gumbel_limit_095 = lam_gum_high), 4))
gauss_080_at_40y gauss_095_at_40y gumbel_limit_080 gumbel_limit_095 
          0.6763           0.8393           0.6716           0.8496 

At correlation 0.95 the picture is worse than at 0.8, and it is the more realistic case for sites in one catchment. The matched Gumbel copula has an upper limit of 0.8496. The Gaussian copula, whose limit is zero, reads 0.7792 at the frontier a hundred years of data can reach, 0.5901 at ten thousand years, and 0.2729 at a trillion. To get the estimate down to half the matched Gumbel value takes 12,200,000 years; to get it below 0.1 takes 7.95e+24 years, which is longer than the age of the universe by a wide margin.

At correlation 0.8 the same two questions want 2060 years and 9,780,000 years. The first of those is not absurd, and a paleoecological or tree-ring reconstruction could in principle reach it, which is worth knowing: the asymptotic behaviour of a moderate correlation is a millennium away rather than a cosmology away.

With the forty summers actually in hand the frontier is a threshold quantile of 0.75, where a Gaussian copula at 0.8 reads 0.6763 and one at 0.95 reads 0.8393. Both look like clear evidence of tail dependence, and the first of them is above the limit 0.6716 of the Gumbel copula it is being compared against, while the second is within 0.0103 of the Gumbel limit 0.8496. At the only threshold the record can reach, the asymptotically independent model looks at least as tail dependent as the asymptotically dependent one, and their limits are as far apart as limits can be.

This is why the coefficient gets reported as comfortably positive from data generated by processes for which it is exactly zero, and it is not a mistake by whoever reported it: the estimate is close to unbiased for the quantity at the threshold used. The threshold is simply not the limit, and the distance between them is not crossable.

A line chart on warm off-white paper. The horizontal axis is the tail probability of the threshold, running from 0.5 on the left down to one in a trillion on the right and labelled at alternate powers of ten. The vertical axis is the coefficient of tail dependence from zero to one. An upper dark green curve for correlation 0.95 falls slowly from about 0.9 to about 0.27 across the whole width. A lower sage curve for correlation 0.8 falls from about 0.8 to about 0.02, flattening close to the axis on the right. Two horizontal dotted lines at about 0.85 and 0.67 mark the matched Gumbel limits, and a shaded vertical band at the far left, labelled forty years, marks the whole region a forty year record can reach.
Figure 4: The exact upper tail dependence coefficient of a Gaussian copula, plotted against threshold, at two correlations. Both curves tend to zero and neither gets near it within any reachable amount of data. The dotted lines are the limits of Gumbel copulas matched on Kendall’s tau, and the shaded band on the left is the region a forty year record can reach.

Honest limits

Everything above is bivariate or exchangeable. The tail dependence work is on pairs, and the eight-fen calculation gives every pair of fens the same dependence and every fen the same threshold. Real reserve networks are neither: fens sharing an aquifer will be locked together far more tightly than fens on opposite sides of a watershed, and the exchangeable answer will sit somewhere between the two subgroup answers without being right for either. Fitting a non-exchangeable structure means estimating a dependence parameter per pair, which is twenty-eight parameters here from forty years, and the data problem gets worse rather than better. Heffernan and Tawn (2004) give a conditional framework that scales past two variables without committing in advance to one regime or the other, and it is the right next step for anyone with more sites than this post handles.

The estimator conditions on rarer and rarer events, so its sampling error grows in exactly the region where the estimate carries the information. That is structural. A larger sample moves the frontier out but does not change the shape of the problem, which is why the years in the frontier table climb by powers of ten for each small step of the coefficient. No smoothing, bootstrap or bias correction fixes it, because there is no information there to correct.

The joint probabilities are exact for the copulas as written and say nothing about whether those copulas are right. A one-factor structure with a single shared driver per year is a modelling choice, and a catchment with a slow-moving drought front would want dependence that varies with lag as well as with distance. Autocorrelation is absent throughout: every year here is an independent draw, which flatters the fifty-year horizon numbers, since a real sequence of dry summers arrives in runs.

Finally, the marginal side has been assumed away. The thresholds are quantiles of the true distribution, and in a real analysis they would come from a fitted GEV or GPD with the uncertainty the return level post documents at length. That uncertainty multiplies with the copula uncertainty measured here rather than replacing it.

References

Sibuya M 1960 Annals of the Institute of Statistical Mathematics 11(3):195-210 (10.1007/BF01682329)

Ledford AW, Tawn JA 1996 Biometrika 83(1):169-187 (10.1093/biomet/83.1.169)

Coles S, Heffernan J, Tawn J 1999 Extremes 2(4):339-365 (10.1023/A:1009963131610)

Heffernan JE, Tawn JA 2004 Journal of the Royal Statistical Society Series B 66(3):497-546 (10.1111/j.1467-9868.2004.02050.x)

Schmidt R, Stadtmuller U 2006 Scandinavian Journal of Statistics 33(2):307-335 (10.1111/j.1467-9469.2005.00483.x)

Marshall AW, Olkin I 1988 Journal of the American Statistical Association 83(403):834-841 (10.1080/01621459.1988.10478671)

Chambers JM, Mallows CL, Stuck BW 1976 Journal of the American Statistical Association 71(354):340-344 (10.1080/01621459.1976.10480344)

Zscheischler J, Seneviratne SI 2017 Science Advances 3(6):e1700263 (10.1126/sciadv.1700263)

Liebhold A, Koenig WD, Bjornstad ON 2004 Annual Review of Ecology Evolution and Systematics 35:467-490 (10.1146/annurev.ecolsys.34.011802.132516)

Newsletter

Get new tutorials by email

New R and QGIS tutorials for ecologists, straight to your inbox. No spam; unsubscribe anytime.

By subscribing you agree to receive these emails and confirm your address once. See the privacy policy.