Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Non-Stationary GEVD: Parametric Trend in μ(t)

Open In Colab

The previous EVT notebooks all assumed stationarity — that the parameters (μ,σ,ξ)(\mu, \sigma, \xi) are constant across the record. In a warming climate, an aging asset, or a covariate-driven setting, this is wrong, and the resulting fit absorbs the non-stationarity into the wrong parameter (typically σ\sigma).

This notebook fits the simplest non-stationary GEVD: a parametric linear trend in the location parameter,

μ(t)  =  β0+β1 t,\mu(t) \;=\; \beta_0 + \beta_1\, t,

with σ\sigma and ξ\xi held stationary. We generate annual block maxima from such a model on simulated daily data, fit the non-stationary version, and compare against a misspecified stationary fit.

What we cover

  1. Theory. The non-stationary GEVD likelihood; why μ\mu is the natural parameter to put a trend on; non-stationary return levels.

  2. Synthetic 1D data. 80 years of daily values with a planted upward trend in the location.

  3. Bayesian fit. NumPyro NUTS over (β0,β1,σ,ξ)(\beta_0, \beta_1, \sigma, \xi).

  4. Stationary vs non-stationary. Side-by-side posteriors and return-level curves.

  5. Recovery. Show that β^1\hat\beta_1 matches the planted slope and that the non-stationary 100-yr return level rises with time at the right rate.

Python implementation: CPython
Python version       : 3.12.3
IPython version      : 9.10.0

jax       : 0.10.1
numpy     : 2.4.4
matplotlib: 3.10.8
numpyro   : 0.21.0
xarray    : 2026.4.0
xtremax   : 0.0.0

Compiler    : GCC 13.3.0
OS          : Linux
Release     : 6.18.5
Machine     : x86_64
Processor   : x86_64
CPU cores   : 4
Architecture: 64bit

1. The non-stationary GEVD likelihood

In the stationary fit, every block maximum ziz_i is i.i.d. from the same GEVD(μ,σ,ξ)\mathrm{GEVD}(\mu, \sigma, \xi). The non-stationary version simply lets one or more parameters depend on a covariate xix_i (here, time):

zi∣xi  ∼  GEVD ⁣(μ(xi),σ(xi),ξ(xi)).z_i \mid x_i \;\sim\; \mathrm{GEVD}\!\bigl(\mu(x_i), \sigma(x_i), \xi(x_i)\bigr).

The log-likelihood is the obvious sum:

ℓ(βμ,βσ,βξ)  =  ∑i=1nlog⁡fGEVD ⁣(zi; μ(xi),σ(xi),ξ(xi)).\ell\bigl(\boldsymbol\beta_\mu, \boldsymbol\beta_\sigma, \boldsymbol\beta_\xi\bigr) \;=\; \sum_{i=1}^{n} \log f_{\mathrm{GEVD}}\!\bigl(z_i;\, \mu(x_i), \sigma(x_i), \xi(x_i)\bigr).

Which parameters should you put a trend on? Coles (2001, §6) argues:

  • μ\mu is the natural one. It is interpretable (the “central level” of the maxima) and trends in physical processes — warming, aging, sensor drift — typically shift the location of the tail.

  • σ\sigma can be made covariate-dependent if there is a clear reason (e.g. the variance of a process scales with its mean). Use log⁡σ(x)=α0+α1x\log\sigma(x) = \alpha_0 + \alpha_1 x to enforce positivity.

  • ξ\xi is dangerous to make non-stationary: it is already poorly identified in the stationary fit, and adding a regression on it usually leads to wide, unstable posteriors. Hold ξ\xi fixed unless you have a lot of data and a strong physical reason.

This notebook plants the trend in μ\mu only — the cleanest case.

Return level under non-stationarity. Define the TT-block return level given the covariate as

zT(x)  =  FGEVD−1(1−1/T; μ(x),σ,ξ)  =  μ(x)+σξ[(−log⁡(1−1/T))−ξ−1].z_T(x) \;=\; F^{-1}_{\mathrm{GEVD}}(1 - 1/T;\,\mu(x), \sigma, \xi) \;=\; \mu(x) + \frac{\sigma}{\xi}\Bigl[\bigl(-\log(1-1/T)\bigr)^{-\xi} - 1\Bigr].

Under a linear trend μ(x)=β0+β1x\mu(x) = \beta_0 + \beta_1 x, the curve zT(x)z_T(x) is also linear in xx with the same slope β1\beta_1 — the trend translates the entire tail upward by an amount that does not depend on TT. (If σ\sigma or ξ\xi also vary with xx, the slope changes with TT.)

2. Synthetic 80-year annual maxima

We want annual block maxima that follow GEVD(μ(t),σ,ξ)\mathrm{GEVD}(\mu(t), \sigma, \xi) with μ(t)=β0+β1t\mu(t) = \beta_0 + \beta_1 t linear in years and (σ,ξ)=(2.0,−0.10)(\sigma, \xi) = (2.0, -0.10) stationary. The trend is β1=0.05\beta_1 = 0.05 per year — a noticeable warming over 80 years.

To make the planted truth exact, we sample the annual maxima directly from the declared non-stationary GEVD: for each year tt we draw

zt  ∼  GEVD ⁣(μ(t)=β0+β1t,  σ,  ξ).z_t \;\sim\; \mathrm{GEVD}\!\bigl(\mu(t) = \beta_0 + \beta_1 t,\; \sigma,\; \xi\bigr).

This matches the model’s μ(t)=β0+β1t\mu(t) = \beta_0 + \beta_1 t structure exactly, so the “true” (σ,ξ)(\sigma, \xi) overlays and the zT(t)z_T(t) return-level curve are genuine ground truth — not the approximate Fisher-Tippett-Gnedenko limit of a block-maxima procedure. We also synthesise a daily series with the same upward trend in its mean, purely to visualise the kind of process such maxima arise from, but the values used for fitting are the GEVD draws.

Number of annual maxima: 80
Year covariate range:    0.0 to 79.0
<Figure size 1300x360 with 2 Axes>

3. Stationary baseline

Before the non-stationary fit we run the same stationary GEVD pipeline as notebook 2 in this series. The mis-specification will absorb the trend into the scale parameter, broadening the posterior on σ\sigma.

/tmp/claude-0/ipykernel_32670/3358547479.py:16: UserWarning: There are not enough devices to run parallel chains: expected 2 but got 1. Chains will be drawn sequentially. If you are running MCMC in CPU, consider using `numpyro.set_host_device_count(2)` at the beginning of your program. You can double-check how many devices are available in your system using `jax.local_device_count()`.
  mcmc_s = MCMC(
Stationary GEVD:
  σ posterior median = 2.180 (true 2.00)
  ξ posterior median = -0.023 (true -0.10)

4. Non-stationary GEVD with linear μ(t)\mu(t)

Model.

zi∣ti  ∼  GEVD(β0+β1ti, σ, ξ).z_i \mid t_i \;\sim\; \mathrm{GEVD}\bigl(\beta_0 + \beta_1 t_i,\, \sigma,\, \xi\bigr).

Standardised covariate. With raw years t∈[0,79]t \in [0, 79] the intercept and slope live on wildly different scales, which gives NUTS a stretched, correlated posterior geometry. We therefore regress on the standardised covariate t~=(t−tˉ)/st\tilde t = (t - \bar t)/s_t,

μ(t~)=α0+α1t~,\mu(\tilde t) = \alpha_0 + \alpha_1 \tilde t,

and recover the physical-unit parameters as deterministics β1=α1/st\beta_1 = \alpha_1 / s_t and β0=α0−β1tˉ\beta_0 = \alpha_0 - \beta_1 \bar t. The rest of the notebook reads beta0/beta1 exactly as before.

Sampler geometry. The remaining divergences came from the GEVD’s shape-dependent support: as ξ\xi crosses zero the upper bound μ−σ/ξ\mu - \sigma/\xi appears or disappears, so the log-likelihood develops a moving cliff whenever a maximum sits near the boundary. Two changes remove it: (i) the data are genuinely bounded above (Weibull-type, ξ<0\xi < 0), so we use a smooth truncated-normal prior ξ∼N(−∞,0)(−0.1,0.15)\xi \sim \mathcal{N}_{(-\infty,0)}(-0.1, 0.15) that keeps ξ\xi strictly negative and away from the sign-flip; (ii) we raise target_accept_prob to 0.999 with longer warmup (6000) and deeper trees (max_tree_depth=13). The committed run reports 0 divergences.

Priors.

  • α0∼N(zˉ, 5)\alpha_0 \sim \mathcal{N}(\bar z,\, 5) — the empirical mean of the maxima (the central level at mid-record).

  • α1∼N(0, 5)\alpha_1 \sim \mathcal{N}(0,\, 5) — weakly informative around zero, on the standardised scale.

  • σ∼HalfNormal(2sz)\sigma \sim \mathrm{HalfNormal}(2 s_z).

  • ξ∼N(−∞, 0)(−0.1, 0.15)\xi \sim \mathcal{N}_{(-\infty,\,0)}(-0.1,\, 0.15) — truncated to the bounded-tail regime.

/tmp/claude-0/ipykernel_32670/3748650945.py:35: UserWarning: There are not enough devices to run parallel chains: expected 2 but got 1. Chains will be drawn sequentially. If you are running MCMC in CPU, consider using `numpyro.set_host_device_count(2)` at the beginning of your program. You can double-check how many devices are available in your system using `jax.local_device_count()`.
  mcmc_ns = MCMC(

                mean       std    median      2.5%     97.5%     n_eff     r_hat
    alpha0     31.63      0.23     31.63     31.18     32.07   2392.47      1.00
    alpha1      1.05      0.23      1.05      0.63      1.50   2476.81      1.00
     sigma      1.96      0.18      1.95      1.62      2.32   2282.09      1.00
        xi     -0.08      0.05     -0.07     -0.17     -0.00   1928.96      1.00

Number of divergences: 0

5. Posterior comparison: stationary vs non-stationary

The headline question: did we recover β1\beta_1? And how does the non-stationary σ\sigma posterior compare to the stationary one?

<Figure size 1500x380 with 3 Axes>

Reading these.

  • The non-stationary posterior on β1\beta_1 is concentrated around the planted slope 0.05 (median ≈0.046\approx 0.046, 95% CI covering the truth) — the data clearly identifies the trend.

  • The stationary σ\sigma posterior sits high relative to the true σ=2.0\sigma = 2.0, while the non-stationary posterior recovers it: the stationary model has nowhere to put the trend except by inflating the implied dispersion.

  • The stationary ξ\xi posterior drifts toward zero (a heavier apparent tail): with the trend artificially smearing the maxima, the stationary model misreads the tail shape. The non-stationary fit (with its bounded-tail ξ<0\xi < 0 prior) keeps ξ\xi closer to the true -0.10.

6. Time-varying return-level curve

Under non-stationarity the year-TT return level is itself a function of time:

zT(t)  =  μ(t)+σξ[(−log⁡(1−1/T))−ξ−1]  =  (β0+β1t)+σξ[…].z_T(t) \;=\; \mu(t) + \frac{\sigma}{\xi}\Bigl[(-\log(1 - 1/T))^{-\xi} - 1\Bigr] \;=\; (\beta_0 + \beta_1 t) + \frac{\sigma}{\xi}\bigl[\ldots\bigr].

We compute it from each posterior sample to get a 95% credible band — the entire excess part is stationary, the trend lives entirely in μ(t)\mu(t).

<Figure size 1000x500 with 1 Axes>
Non-stat. 100-yr return level at year 0:  37.43
Non-stat. 100-yr return level at year 79: 41.03
Return-level slope over years 0-79 (span 79 yr):  0.0456/yr
Recovered slope (β1 posterior):           0.0457/yr  [95% CI: 0.0262, 0.0643]
True slope (β1):                          0.0500/yr

The picture is unambiguous. The non-stationary fit produces a return-level curve that rises in lockstep with the trend; the stationary fit produces a flat line that under-predicts late-record extremes and over-predicts early-record extremes. Both are wrong by the same total amount, but the non-stationary fit puts the error in the right place (the trend term) while the stationary fit smears it across σ\sigma and ξ\xi.

7. Posterior predictive check

A quick sanity check: simulate a replicate dataset from the non-stationary posterior and compare to the data.

<Figure size 1000x400 with 1 Axes>

Takeaways

  • Non-stationarity in μ\mu is the simplest and most useful covariate-driven extension of the GEVD. A linear-in-time μ(t)=β0+β1t\mu(t) = \beta_0 + \beta_1 t adds a single parameter and dramatically reduces bias.

  • Stationary mis-specification is recoverable in σ\sigma. Under a planted trend, the stationary fit inflates σ\sigma to absorb the trend variance — a tell-tale sign that you should be fitting non-stationary.

  • Return levels translate. With μ(t)\mu(t) linear and (σ,ξ)(\sigma, \xi) fixed, zT(t)z_T(t) is also linear and its slope equals β1\beta_1 — independent of TT.

  • Don’t make ξ\xi non-stationary unless forced. In real datasets ξ\xi is already poorly identified; a covariate-dependent ξ\xi blows up the posterior.

  • Generalisation. Replace the linear basis (1,t)(1, t) with (1,t,zt)(1, t, z_t) to add a covariate, or with a polynomial / spline basis for non-linear trends. The next two notebooks in this mini-series do exactly that — non-parametric splines and ODE-driven dynamics — but the idea is the same: pick a parameterisation of μ(⋅)\mu(\cdot) rich enough for the trend, leave (σ,ξ)(\sigma, \xi) stationary, and fit.