pub fn gpd_fit_pwm(excesses: &[f64]) -> (f64, f64) {
debug_assert!(
excesses.len() >= 2,
"gpd_fit_pwm requires >= 2 excesses (divide-by-zero otherwise)"
);
let n = excesses.len();
let n_f = n as f64;
let mut x: Vec<f64> = excesses.to_vec();
x.sort_by(|a, b| a.partial_cmp(b).unwrap());
let a0: f64 = x.iter().sum::<f64>() / n_f;
let mut a1: f64 = 0.0;
for i in 0..n {
a1 += ((n as f64 - 1.0 - i as f64) / (n_f - 1.0)) * x[i];
}
a1 /= n_f;
let denom = a0 - 2.0 * a1;
if denom <= 1e-12 {
return (0.95, a0.max(1e-12));
}
let gamma_raw = 2.0 - a0 / denom;
let gamma = gamma_raw.clamp(-0.5, 0.95);
let sigma = (2.0 * a0 * a1 / denom).max(1e-12);
(gamma, sigma)
}
pub fn gpd_sf(e: f64, gamma: f64, sigma: f64) -> f64 {
if gamma.abs() < 1e-9 {
return (-(e / sigma).min(700.0)).exp();
}
let arg = 1.0 + gamma * e / sigma;
if arg <= 0.0 {
return 0.0;
}
arg.powf(-1.0 / gamma)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exponential_at_gamma_zero() {
for &e in &[0.5, 1.0, 2.5, 10.0] {
let sf = gpd_sf(e, 0.0, 1.0);
let expected = (-e).exp();
assert!(
(sf - expected).abs() < 1e-12,
"e={e}: got {sf}, expected {expected}"
);
}
}
#[test]
fn heavy_tail_gamma_positive() {
let e = 5.0;
let sf_exp = gpd_sf(e, 0.0, 1.0);
let sf_heavy = gpd_sf(e, 0.5, 1.0);
assert!(sf_heavy > sf_exp, "heavy tail should exceed exponential");
}
#[test]
fn short_tail_gamma_negative() {
let e = 3.0;
let sf_short = gpd_sf(e, -0.3, 1.0);
let sf_exp = gpd_sf(e, 0.0, 1.0);
assert!(sf_short < sf_exp);
let sf_beyond = gpd_sf(10.0, -0.3, 1.0);
assert_eq!(sf_beyond, 0.0);
}
#[test]
fn pwm_recovers_known_shape() {
let true_gamma = 0.3;
let true_sigma = 1.0;
let n = 1000;
let mut seed = 12345u64;
let mut samples = Vec::with_capacity(n);
for _ in 0..n {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
let u = (seed >> 33) as f64 / (1u64 << 31) as f64;
let u = u.clamp(1e-12, 1.0 - 1e-12);
let x = true_sigma / true_gamma * ((1.0 - u).powf(-true_gamma) - 1.0);
samples.push(x);
}
let (gamma_hat, sigma_hat) = gpd_fit_pwm(&samples);
assert!(
(gamma_hat - true_gamma).abs() < 0.15,
"γ estimate {gamma_hat} vs true {true_gamma}",
);
assert!(
(sigma_hat - true_sigma).abs() < 0.3,
"σ estimate {sigma_hat} vs true {true_sigma}",
);
}
#[test]
fn pwm_clamps_gamma() {
let samples = vec![1.0; 20];
let (gamma, sigma) = gpd_fit_pwm(&samples);
assert!((-0.5..=0.95).contains(&gamma));
assert!(sigma > 0.0);
}
}