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 Thresholds: Quantile Regression → GPD

Open In Colab

The previous POT notebook used a constant threshold uu. That is fine for stationary data but breaks down whenever the location of the distribution drifts in time — a warming climate, an aging asset, a covariate-driven trend. Using a constant threshold then produces too few exceedances early on and far too many late on, biasing both the rate λu\lambda_u and the GPD scale.

This notebook introduces the quantile-regression threshold — fit a τ\tau-quantile of the data as a function of time and/or covariates via linear programming, then run a stationary GPD on the residual exceedances. It is a clean and pragmatic split: the threshold absorbs the non-stationary mean structure, the GPD models the residual tail.

What we cover

  1. Theory. Quantile regression as the LP that minimises the asymmetric pinball loss; what it estimates and what it does not.

  2. A trended series. Synthetic 80-year daily process with a slow upward trend and seasonal cycle.

  3. Constant vs trend threshold. Side-by-side fits of quantile_threshold (constant) and quantile_regression_threshold (linear-in-time); compare the rates of exceedances per decade.

  4. GPD on residual exceedances. Same NumPyro fit as the previous notebook, applied to yi=zi−u(ti)y_i = z_i - u(t_i).

  5. Time-varying return levels. With a non-stationary threshold, the year-TT return level is also time-varying.

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
scikit-learn: 1.8.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. Quantile regression in one screen

Classical OLS regression solves

β^OLS  =  arg⁡min⁡β∑i(yi−xi⊤β)2.\hat\beta_{\text{OLS}} \;=\; \arg\min_\beta \sum_{i} (y_i - x_i^\top \beta)^2.

For a target quantile τ∈(0,1)\tau \in (0, 1), quantile regression (Koenker & Bassett, 1978) replaces the squared loss with the asymmetric pinball loss

ρτ(r)  =  r⋅(τ−1r<0)  =  {τ r,r≥0,−(1−τ) r,r<0,\rho_\tau(r) \;=\; r \cdot \bigl(\tau - \mathbf{1}_{r < 0}\bigr) \;=\; \begin{cases} \tau\, r, & r \ge 0,\\ -(1-\tau)\, r, & r < 0,\end{cases}

and solves

β^τ  =  arg⁡min⁡β∑iρτ(yi−xi⊤β).\hat\beta_\tau \;=\; \arg\min_\beta \sum_{i} \rho_\tau(y_i - x_i^\top \beta).

This is a linear program (the pinball loss is piecewise linear), efficiently solved by HiGHS or any LP backend. The result is a hyperplane x⊤β^τx^\top \hat\beta_\tau that estimates the conditional quantile Qτ(Y∣X=x)Q_\tau(Y \mid X=x).

What it estimates. Qτ(Y∣X)Q_\tau(Y \mid X) — the τ\tau-th conditional quantile of YY given the features. With features X=(1,t)X = (1, t) this is a linear-in-time quantile; with X=(1,t,zt)X = (1, t, z_t) a covariate-adjusted quantile.

What it does not estimate. It says nothing about what is above the line — the tail behaviour, the rate of exceedances, the heaviness of excess events. That is the GPD’s job in the next step.

Why τ≈0.95\tau \approx 0.95 is the right choice for POT. We want the threshold u(t)u(t) to track a fixed upper percentile of the data so that the rate of exceedances λu(t)=P(Xt>u(t))\lambda_{u(t)} = \mathbb{P}(X_t > u(t)) is approximately constant across time — exactly 1−τ1 - \tau of the days. With a constant threshold under non-stationary data, λu\lambda_u drifts with the trend, and the Poisson-GPD return-level formula loses its meaning.

2. Synthetic trended daily series

We simulate a daily process with a clear upward trend (think: warming climate, sensor drift), a seasonal cycle, and Gaussian noise. The fixed-quantile threshold should bend with the trend; a constant threshold cannot.

<Figure size 1100x340 with 1 Axes>

3. Constant vs quantile-regression threshold

We fit two thresholds: a constant 95th-percentile via quantile_threshold and a linear-trend 95th-percentile via quantile_regression_threshold. The latter solves the LP

(β^0,β^1)  =  arg⁡min⁡β0,β1∑iρ0.95(zi−β0−β1ti).(\hat\beta_0, \hat\beta_1) \;=\; \arg\min_{\beta_0, \beta_1} \sum_{i} \rho_{0.95}\bigl(z_i - \beta_0 - \beta_1 t_i\bigr).

xtremax.extraction.quantile_regression_threshold is [threshold]-extra functionality that wraps sklearn.linear_model.QuantileRegressor.

Constant 95th-percentile threshold: 21.038
Quantile-regression endpoints:      18.282 → 23.109  (slope ≈ 0.0603/yr)
<Figure size 1100x360 with 1 Axes>

4. Per-decade exceedance rates

This plot quantifies the bias. With a constant threshold, the trend pushes more and more daily values above uu over time — by year 80, the 95th percentile is well below the bulk of the daily values, and the “exceedance rate” λu\lambda_u has drifted from a few percent to tens of percent. The quantile-regression threshold tracks the trend and keeps the rate stable.

<Figure size 800x400 with 1 Axes>

Reading this.

  • The black dashed line is the target rate 1−τ=0.051 - \tau = 0.05.

  • The red bars (constant uu) start near zero in the early decades and climb above 0.10 later — a factor of 2-3 distortion that ruins the Poisson-GPD return-level formula.

  • The green bars (QR u(t)u(t)) hug the target throughout — exactly what we want.

5. GPD fit on residual exceedances

With the QR threshold in hand, the GPD step is unchanged from the previous notebook, modulo computing the residual exceedances yi=zi−u(ti)y_i = z_i - u(t_i) at the time-varying threshold. We use decluster_runs for the same i.i.d. reasons as before, with the time-varying threshold passed as an xr.DataArray.

Declustered residual exceedances: 1218
Empirical mean residual:          1.702
Empirical std residual:           1.378
/tmp/claude-0/ipykernel_18707/1753939281.py:29: 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 = MCMC(kernel, num_warmup=5000, num_samples=2000, num_chains=2, progress_bar=False)

                mean       std    median      2.5%     97.5%     n_eff     r_hat
     sigma      2.04      0.07      2.04      1.91      2.17   2277.63      1.00
        xi     -0.20      0.02     -0.21     -0.24     -0.17   1801.85      1.00

Number of divergences: 0

6. Comparison: constant-threshold GPD on the same data

What if we had used the constant threshold? We refit the GPD with uconstu_{\text{const}} and all exceedance days (including the late-decades flood). The mis-specified version will see a wider mix of “tail” and “bulk” values and bias the fit.

Declustered exceedances (constant u): 1116
/tmp/claude-0/ipykernel_18707/345095261.py:13: 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_c = MCMC(

Posterior comparison

<Figure size 1100x400 with 2 Axes>

Reading this:

  • σ\sigma is much larger under the constant threshold, because in late decades the trend has lifted the bulk above the constant uu, so the fit absorbs that bulk inflation into the GPD scale.

  • ξ\xi can shift toward zero or slightly positive under mis-spec, again because the mid-bulk values look like exponential noise.

  • The QR-threshold posterior is the clean fit on what is actually the residual tail.

7. Time-varying return levels

With the QR threshold, the year-TT return level is itself time-varying:

zT(t)  =  u(t)+σξ[(Tλu)ξ−1],ξ≠0.z_T(t) \;=\; u(t) + \frac{\sigma}{\xi}\Bigl[(T \lambda_u)^{\xi} - 1\Bigr], \qquad \xi \neq 0.

We compute the 100-year return level as a function of the year by combining the QR threshold u(t)u(t) with the stationary GPD posterior on (σ,ξ)(\sigma, \xi).

Declustered events per year (QR): λ_u = 15.226
<Figure size 1100x400 with 1 Axes>
100-yr return level at start of record: 26.01
100-yr return level at end of record:   30.83
Trend in 100-yr return level:           +4.83 over 80 yr = 0.060/yr

Interpretation.

  • The 100-year return level rises in lockstep with the QR threshold. By construction, the excess part of the formula is stationary; the trend lives entirely in u(t)u(t).

  • This is a deliberately simple model: the trend is in the location of the tail, not its shape or scale. If the tail itself is changing — heavier tails over time, more volatile excesses — you need a non-stationary GPD, where σ\sigma or ξ\xi are themselves time- or covariate-dependent. That is the natural sequel to this notebook (and a v0.1 polish item for xtremax).

Takeaways

  • Quantile regression separates trend from tail. Fit the trend in the threshold; fit the tail in the residual GPD. Each step does what it does best.

  • Constant thresholds break under non-stationarity. Per-decade exceedance rates can drift by 2-3× and bias both σ\sigma and the implied return levels.

  • The QR threshold is just an LP. xtremax.extraction.quantile_regression_threshold wraps sklearn.linear_model.QuantileRegressor and accepts arbitrary covariates, not just time. Replace t with any covariate vector you care about (SST, ENSO index, AMO, …) and the recipe is unchanged.

  • Time-varying return levels emerge for free. With a stationary residual GPD and a time-varying u(t)u(t), the year-TT return level translates as zT(t)=u(t)+(constant excess)z_T(t) = u(t) + \text{(constant excess)}. The slope of zT(t)z_T(t) is the slope of the threshold — a direct readout of the trend.

  • Limitations. This is a location-shift non-stationarity model. If the tail itself is changing — heavier tails over time, more volatile excesses — you need a non-stationary GPD, where σ\sigma or ξ\xi are themselves covariate-dependent. That is the natural follow-on extension.

This concludes the six-notebook tour of xtremax’s extreme-value distributions and extraction utilities. The closing reading list from notebook 1 (Coles 2001; Embrechts, Klüppelberg & Mikosch 1997; Beirlant, Goegebeur, Segers & Teugels 2004) is the right entry point if you want to push beyond stationarity assumptions.