pub mod analysis;
pub mod distributions;
pub mod estimation;
pub mod gev;
pub mod gpd;
#[path = "block_maxima.rs"]
pub mod block_maxima_module;
#[path = "pot.rs"]
pub mod pot_module;
pub mod return_levels;
pub use block_maxima_module::{
extract_block_maxima, BlockMaximaFitter, BlockMaximaResult as BlockMaximaFitterResult,
};
pub use gev::GEV;
pub use gpd::GPD as GpdModel;
pub use pot_module::{
extract_exceedances, threshold_selection_mrl, threshold_selection_stability, POTFitter,
POTResult as PotFitterResult,
};
pub use return_levels::{return_level_confidence_intervals, return_level_gev, return_level_gpd};
pub use analysis::{
block_maxima_analysis, empirical_return_periods, hill_estimator, mean_excess_plot,
peaks_over_threshold, pickands_estimator, return_level_confidence, BlockMaximaResult,
EvtEstimationMethod, PlottingPosition, PotResult,
};
pub use distributions::{Frechet, GeneralizedExtremeValue, GeneralizedPareto, Gumbel};
pub use estimation::{
gev_fit_lmoments, gev_fit_mle, gev_fit_pwm, gev_goodness_of_fit, gpd_fit_mle,
gumbel_fit_lmoments, gumbel_fit_mle, sample_lmoments,
};
#[derive(Debug, Clone)]
pub struct GevFit {
pub distribution: GeneralizedExtremeValue,
pub log_likelihood: f64,
pub aic: f64,
pub bic: f64,
pub n_obs: usize,
pub method: EvtEstimationMethod,
}
impl GevFit {
pub fn mu(&self) -> f64 {
self.distribution.mu
}
pub fn sigma(&self) -> f64 {
self.distribution.sigma
}
pub fn xi(&self) -> f64 {
self.distribution.xi
}
pub fn return_level(&self, return_period: f64) -> crate::error::StatsResult<f64> {
self.distribution.return_level(return_period)
}
}
pub type GevDistribution = GeneralizedExtremeValue;
pub type GpdDistribution = GeneralizedPareto;
pub fn gev_pdf(x: f64, mu: f64, sigma: f64, xi: f64) -> f64 {
match GeneralizedExtremeValue::new(mu, sigma, xi) {
Ok(g) => g.pdf(x),
Err(_) => 0.0,
}
}
pub fn gev_cdf(x: f64, mu: f64, sigma: f64, xi: f64) -> f64 {
match GeneralizedExtremeValue::new(mu, sigma, xi) {
Ok(g) => g.cdf(x),
Err(_) => 0.0,
}
}
pub fn fit_gev(data: &[f64]) -> crate::error::StatsResult<GevFit> {
use scirs2_core::ndarray::Array1;
if data.len() < 3 {
return Err(crate::error::StatsError::InsufficientData(
"GEV fitting requires at least 3 observations".into(),
));
}
let arr = Array1::from(data.to_vec());
let distribution = gev_fit_mle(arr.view())?;
let n = data.len();
let ll: f64 = data
.iter()
.map(|&x| {
let p = distribution.pdf(x);
if p > 0.0 && p.is_finite() {
p.ln()
} else {
-1e10
}
})
.sum();
let k = 3.0;
let nf = n as f64;
Ok(GevFit {
distribution,
log_likelihood: ll,
aic: -2.0 * ll + 2.0 * k,
bic: -2.0 * ll + k * nf.ln(),
n_obs: n,
method: EvtEstimationMethod::MLE,
})
}
pub fn block_maxima(data: &[f64], block_size: usize) -> crate::error::StatsResult<Vec<f64>> {
if block_size == 0 {
return Err(crate::error::StatsError::InvalidArgument(
"block_size must be >= 1".into(),
));
}
let n = data.len();
let n_blocks = n / block_size;
if n_blocks < 2 {
return Err(crate::error::StatsError::InsufficientData(format!(
"Need at least 2 complete blocks (block_size={block_size}), \
data length {n} gives {n_blocks} block(s)"
)));
}
let maxima: Vec<f64> = (0..n_blocks)
.map(|b| {
let start = b * block_size;
let end = start + block_size;
data[start..end]
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max)
})
.collect();
Ok(maxima)
}
pub fn peaks_over_threshold_simple(data: &[f64], threshold: f64) -> Vec<f64> {
data.iter()
.filter_map(|&x| {
if x > threshold {
Some(x - threshold)
} else {
None
}
})
.collect()
}
pub fn return_level(fit: &GevFit, return_period: f64) -> crate::error::StatsResult<f64> {
fit.distribution.return_level(return_period)
}
pub fn extreme_value_index(data: &[f64]) -> crate::error::StatsResult<f64> {
use scirs2_core::ndarray::Array1;
let n = data.len();
if n < 10 {
return Err(crate::error::StatsError::InsufficientData(
"extreme_value_index requires at least 10 observations".into(),
));
}
let k = (n / 10).max(5).min(n - 1);
let arr = Array1::from(data.to_vec());
hill_estimator(arr.view(), k)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_gev_pdf_gumbel_case() {
let pdf = gev_pdf(0.0, 0.0, 1.0, 0.0);
let expected = (-1.0_f64).exp();
assert!(approx_eq(pdf, expected, 1e-8), "pdf={pdf}");
}
#[test]
fn test_gev_pdf_frechet_positive_xi() {
let pdf = gev_pdf(2.0, 0.0, 1.0, 0.5);
assert!(pdf > 0.0, "Fréchet PDF should be positive, got {pdf}");
}
#[test]
fn test_gev_pdf_weibull_negative_xi() {
let pdf_inside = gev_pdf(1.0, 0.0, 1.0, -0.5);
let pdf_outside = gev_pdf(2.5, 0.0, 1.0, -0.5);
assert!(pdf_inside > 0.0, "Inside support: {pdf_inside}");
assert_eq!(pdf_outside, 0.0, "Outside support: {pdf_outside}");
}
#[test]
fn test_gev_pdf_invalid_sigma() {
let pdf = gev_pdf(0.0, 0.0, -1.0, 0.0);
assert_eq!(pdf, 0.0);
}
#[test]
fn test_gev_cdf_gumbel_at_zero() {
let cdf = gev_cdf(0.0, 0.0, 1.0, 0.0);
assert!(approx_eq(cdf, (-1.0_f64).exp(), 1e-8), "cdf={cdf}");
}
#[test]
fn test_gev_cdf_reduces_to_gumbel_at_xi_zero() {
let c0 = gev_cdf(1.0, 0.0, 1.0, 0.0);
let c_small = gev_cdf(1.0, 0.0, 1.0, 1e-14);
assert!(approx_eq(c0, c_small, 1e-5), "c0={c0}, c_small={c_small}");
}
#[test]
fn test_gev_cdf_frechet_lower_bound() {
let cdf = gev_cdf(-2.1, 0.0, 1.0, 0.5);
assert_eq!(cdf, 0.0, "Below Fréchet lower bound: {cdf}");
}
#[test]
fn test_gev_cdf_weibull_upper_bound() {
let cdf = gev_cdf(2.5, 0.0, 1.0, -0.5);
assert_eq!(cdf, 1.0, "Above Weibull upper bound: {cdf}");
}
#[test]
fn test_fit_gev_basic() {
use distributions::Gumbel;
let g = Gumbel::new(5.0, 2.0).expect("valid gumbel");
let samples = g.sample(200, 42);
let fit = fit_gev(&samples).expect("fit should succeed");
assert!(fit.distribution.sigma > 0.0);
assert!(fit.log_likelihood.is_finite());
assert!(fit.aic.is_finite());
assert!(fit.bic.is_finite());
assert_eq!(fit.n_obs, 200);
}
#[test]
fn test_fit_gev_insufficient_data() {
assert!(fit_gev(&[1.0, 2.0]).is_err());
}
#[test]
fn test_fit_gev_accessors() {
let fit = fit_gev(&[1.0, 2.0, 3.0, 4.0, 5.0]).expect("fit");
let _ = fit.mu();
let _ = fit.sigma();
let _ = fit.xi();
assert!(fit.sigma() > 0.0);
}
#[test]
fn test_block_maxima_basic() {
let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
let maxima = block_maxima(&data, 10).expect("should succeed");
assert_eq!(maxima.len(), 10);
assert_eq!(maxima[0], 9.0);
assert_eq!(maxima[9], 99.0);
}
#[test]
fn test_block_maxima_zero_block_size_error() {
let data = vec![1.0, 2.0, 3.0];
assert!(block_maxima(&data, 0).is_err());
}
#[test]
fn test_block_maxima_too_few_blocks_error() {
let data = vec![1.0, 2.0, 3.0];
assert!(block_maxima(&data, 2).is_err()); }
#[test]
fn test_block_maxima_values_correct() {
let data = vec![3.0, 1.0, 2.0, 7.0, 5.0, 6.0];
let maxima = block_maxima(&data, 3).expect("2 blocks");
assert_eq!(maxima, vec![3.0, 7.0]);
}
#[test]
fn test_peaks_over_threshold_simple() {
let data = vec![1.0, 2.0, 5.0, 3.0, 8.0, 1.5];
let exc = peaks_over_threshold_simple(&data, 2.0);
assert_eq!(exc.len(), 3);
let mut exc_sorted = exc.clone();
exc_sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
assert!(approx_eq(exc_sorted[0], 1.0, 1e-12));
assert!(approx_eq(exc_sorted[1], 3.0, 1e-12));
assert!(approx_eq(exc_sorted[2], 6.0, 1e-12));
}
#[test]
fn test_peaks_over_threshold_empty_result() {
let data = vec![1.0, 1.5, 1.8];
let exc = peaks_over_threshold_simple(&data, 5.0);
assert!(exc.is_empty());
}
#[test]
fn test_return_level_basic() {
let samples: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
let fit = fit_gev(&samples).expect("fit");
let rl = return_level(&fit, 100.0).expect("return level");
assert!(rl.is_finite());
let sample_max = 9.9_f64;
assert!(rl > 0.0, "rl={rl}, sample_max={sample_max}");
}
#[test]
fn test_return_level_invalid_period() {
let samples: Vec<f64> = (0..100).map(|i| i as f64).collect();
let fit = fit_gev(&samples).expect("fit");
assert!(return_level(&fit, 0.5).is_err());
assert!(return_level(&fit, 1.0).is_err());
}
#[test]
fn test_extreme_value_index_heavy_tail() {
let alpha = 2.0_f64;
let data: Vec<f64> = (1..=200)
.map(|i| {
let u = i as f64 / 201.0;
(1.0 - u).powf(-1.0 / alpha)
})
.collect();
let xi = extreme_value_index(&data).expect("should succeed");
assert!(xi > 0.0, "Heavy-tailed data should give xi>0, got {xi}");
assert!((xi - 0.5).abs() < 0.4, "xi={xi} not close to 0.5");
}
#[test]
fn test_extreme_value_index_insufficient_data() {
assert!(extreme_value_index(&[1.0, 2.0, 3.0]).is_err());
}
#[test]
fn test_gev_type_aliases() {
let _gev: GevDistribution = GeneralizedExtremeValue::new(0.0, 1.0, 0.0).expect("valid");
let _gpd: GpdDistribution = GeneralizedPareto::new(0.0, 1.0, 0.0).expect("valid");
}
#[test]
fn test_gev_fit_struct() {
let samples: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let fit = fit_gev(&samples).expect("fit");
assert_eq!(fit.method, EvtEstimationMethod::MLE);
let rl = fit.return_level(10.0).expect("return level");
assert!(rl.is_finite());
}
#[test]
fn test_gev_reduces_to_gumbel() {
for &x in &[-1.0, 0.0, 1.0, 2.0, 3.0] {
let gev_c = gev_cdf(x, 0.0, 1.0, 0.0);
let gumbel = Gumbel::new(0.0, 1.0).expect("valid").cdf(x);
assert!(
approx_eq(gev_c, gumbel, 1e-10),
"x={x}: gev={gev_c}, gumbel={gumbel}"
);
}
}
#[test]
fn test_gev_reduces_to_frechet_positive_xi() {
let xi = 0.5_f64;
let lower_bound = 0.0 - 1.0 / xi; let cdf_below = gev_cdf(lower_bound - 0.1, 0.0, 1.0, xi);
assert_eq!(cdf_below, 0.0, "Fréchet CDF below lower bound should be 0");
}
#[test]
fn test_gev_reduces_to_weibull_negative_xi() {
let xi = -0.5_f64;
let upper_bound = 0.0 - 1.0 / xi; let cdf_above = gev_cdf(upper_bound + 0.1, 0.0, 1.0, xi);
assert_eq!(cdf_above, 1.0, "Weibull CDF above upper bound should be 1");
}
}
pub mod risk_measures;
pub use risk_measures::{
conditional_var, fit_block_maxima, pot_analysis, return_level_ci, tail_index_hill,
value_at_risk, BlockMaximaFit,
};