1use crate::error::StatsError;
19use scirs2_core::ndarray::{Array1, ArrayView1};
20
21use super::distributions::{GeneralizedExtremeValue, GeneralizedPareto};
22use super::estimation::{gev_fit_lmoments, gev_fit_mle, gev_fit_pwm, gpd_fit_mle};
23
24#[derive(Debug, Clone, PartialEq)]
30pub enum EvtEstimationMethod {
31 MLE,
33 PWM,
35 LMoments,
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub enum PlottingPosition {
42 Weibull,
44 Gringorten,
46 Blom,
48 Hazen,
50}
51
52#[derive(Debug, Clone)]
58pub struct BlockMaximaResult {
59 pub block_maxima: Array1<f64>,
61 pub gev_params: GeneralizedExtremeValue,
63 pub return_levels: Vec<(f64, f64)>,
65 pub n_blocks: usize,
67}
68
69pub fn block_maxima_analysis(
85 data: ArrayView1<f64>,
86 block_size: usize,
87 return_periods: &[f64],
88 estimation: EvtEstimationMethod,
89) -> Result<BlockMaximaResult, StatsError> {
90 if block_size == 0 {
91 return Err(StatsError::InvalidArgument(
92 "block_size must be >= 1".into(),
93 ));
94 }
95 for &rp in return_periods {
96 if rp <= 1.0 {
97 return Err(StatsError::InvalidArgument(format!(
98 "All return periods must be > 1.0, got {rp}"
99 )));
100 }
101 }
102
103 let n = data.len();
104 let n_blocks = n / block_size;
105
106 if n_blocks < 2 {
107 return Err(StatsError::InsufficientData(format!(
108 "Need at least 2 complete blocks (block_size={block_size}), \
109 but data length {n} gives only {n_blocks} block(s)"
110 )));
111 }
112
113 let mut maxima = Vec::with_capacity(n_blocks);
115 let data_slice: Vec<f64> = data.iter().copied().collect();
116 for b in 0..n_blocks {
117 let start = b * block_size;
118 let end = start + block_size;
119 let block_max = data_slice[start..end]
120 .iter()
121 .copied()
122 .fold(f64::NEG_INFINITY, f64::max);
123 if block_max.is_finite() {
124 maxima.push(block_max);
125 }
126 }
127
128 if maxima.len() < 2 {
129 return Err(StatsError::InsufficientData(
130 "Could not extract at least 2 finite block maxima".into(),
131 ));
132 }
133
134 let maxima_arr = Array1::from(maxima.clone());
135
136 let gev_params = match estimation {
138 EvtEstimationMethod::MLE => gev_fit_mle(maxima_arr.view())?,
139 EvtEstimationMethod::PWM => gev_fit_pwm(maxima_arr.view())?,
140 EvtEstimationMethod::LMoments => gev_fit_lmoments(maxima_arr.view())?,
141 };
142
143 let return_levels: Vec<(f64, f64)> = return_periods
145 .iter()
146 .map(|&rp| {
147 let level = gev_params.return_level(rp).unwrap_or(f64::NAN);
148 (rp, level)
149 })
150 .collect();
151
152 Ok(BlockMaximaResult {
153 block_maxima: maxima_arr,
154 gev_params,
155 return_levels,
156 n_blocks,
157 })
158}
159
160#[derive(Debug, Clone)]
166pub struct PotResult {
167 pub threshold: f64,
169 pub exceedances: Array1<f64>,
171 pub n_exceedances: usize,
173 pub rate: f64,
175 pub gpd_params: GeneralizedPareto,
177 pub return_levels: Vec<(f64, f64)>,
179}
180
181pub fn peaks_over_threshold(
197 data: ArrayView1<f64>,
198 threshold: f64,
199 return_periods: &[f64],
200) -> Result<PotResult, StatsError> {
201 for &rp in return_periods {
202 if rp <= 1.0 {
203 return Err(StatsError::InvalidArgument(format!(
204 "All return periods must be > 1.0, got {rp}"
205 )));
206 }
207 }
208
209 let n_total = data.len();
210 if n_total == 0 {
211 return Err(StatsError::InsufficientData("Data is empty".into()));
212 }
213
214 let exceedances: Vec<f64> = data
216 .iter()
217 .filter_map(|&x| {
218 if x > threshold {
219 Some(x - threshold)
220 } else {
221 None
222 }
223 })
224 .collect();
225
226 let n_exc = exceedances.len();
227 if n_exc < 5 {
228 return Err(StatsError::InsufficientData(format!(
229 "POT requires at least 5 exceedances above threshold {threshold}; got {n_exc}"
230 )));
231 }
232
233 let rate = n_exc as f64 / n_total as f64;
234 let exc_arr = Array1::from(exceedances);
235
236 let gpd_params = gpd_fit_mle(exc_arr.view())?;
238
239 let sigma = gpd_params.sigma;
241 let xi = gpd_params.xi;
242 const XI_TOL: f64 = 1e-10;
243
244 let return_levels: Vec<(f64, f64)> = return_periods
245 .iter()
246 .map(|&rp| {
247 let lambda_t = rate * rp;
248 let level = if xi.abs() < XI_TOL {
249 threshold + sigma * lambda_t.ln()
250 } else {
251 threshold + (sigma / xi) * (lambda_t.powf(xi) - 1.0)
252 };
253 (rp, level)
254 })
255 .collect();
256
257 Ok(PotResult {
258 threshold,
259 exceedances: exc_arr,
260 n_exceedances: n_exc,
261 rate,
262 gpd_params,
263 return_levels,
264 })
265}
266
267pub fn mean_excess_plot(
288 data: ArrayView1<f64>,
289 n_thresholds: usize,
290) -> Result<(Array1<f64>, Array1<f64>), StatsError> {
291 let n = data.len();
292 if n < 10 {
293 return Err(StatsError::InsufficientData(
294 "Mean excess plot requires at least 10 observations".into(),
295 ));
296 }
297 if n_thresholds == 0 {
298 return Err(StatsError::InvalidArgument(
299 "n_thresholds must be >= 1".into(),
300 ));
301 }
302
303 let mut sorted: Vec<f64> = data.iter().copied().collect();
304 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
305
306 let lo_idx = n / 20; let hi_idx = (9 * n) / 10; let lo = sorted[lo_idx];
310 let hi = sorted[hi_idx];
311
312 if hi <= lo {
313 return Err(StatsError::ComputationError(
314 "Mean excess plot: data range too narrow".into(),
315 ));
316 }
317
318 let step = (hi - lo) / n_thresholds as f64;
319 let mut thresholds = Vec::with_capacity(n_thresholds);
320 let mut mean_excess = Vec::with_capacity(n_thresholds);
321
322 for k in 0..n_thresholds {
323 let u = lo + k as f64 * step;
324 let exceedances: Vec<f64> = sorted.iter().filter(|&&x| x > u).map(|&x| x - u).collect();
325 if exceedances.len() < 2 {
326 break;
327 }
328 let me = exceedances.iter().sum::<f64>() / exceedances.len() as f64;
329 thresholds.push(u);
330 mean_excess.push(me);
331 }
332
333 if thresholds.is_empty() {
334 return Err(StatsError::ComputationError(
335 "Mean excess plot: no valid threshold produced enough exceedances".into(),
336 ));
337 }
338
339 Ok((Array1::from(thresholds), Array1::from(mean_excess)))
340}
341
342pub fn hill_estimator(data: ArrayView1<f64>, k: usize) -> Result<f64, StatsError> {
361 let n = data.len();
362 if n < 2 {
363 return Err(StatsError::InsufficientData(
364 "Hill estimator requires at least 2 observations".into(),
365 ));
366 }
367 if k == 0 || k >= n {
368 return Err(StatsError::InvalidArgument(format!(
369 "k must be in [1, n-1] = [1, {}], got {k}",
370 n - 1
371 )));
372 }
373
374 let mut sorted: Vec<f64> = data.iter().copied().collect();
375 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
376
377 let x_threshold = sorted[n - k - 1];
379 if x_threshold <= 0.0 {
380 return Err(StatsError::InvalidArgument(
381 "Hill estimator requires all data values used to be positive".into(),
382 ));
383 }
384
385 let log_threshold = x_threshold.ln();
386 let mut sum = 0.0_f64;
387 for i in (n - k)..n {
388 if sorted[i] <= 0.0 {
389 return Err(StatsError::InvalidArgument(
390 "Hill estimator: encountered non-positive order statistic".into(),
391 ));
392 }
393 sum += sorted[i].ln() - log_threshold;
394 }
395
396 Ok(sum / k as f64)
397}
398
399pub fn pickands_estimator(data: ArrayView1<f64>, k: usize) -> Result<f64, StatsError> {
416 let n = data.len();
417 if k == 0 {
418 return Err(StatsError::InvalidArgument("k must be >= 1".into()));
419 }
420 if 4 * k >= n {
421 return Err(StatsError::InvalidArgument(format!(
422 "Pickands estimator requires 4k < n; got 4*{k}={} >= n={n}",
423 4 * k
424 )));
425 }
426
427 let mut sorted: Vec<f64> = data.iter().copied().collect();
428 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
429
430 let x1 = sorted[n - k];
435 let x2 = sorted[n - 2 * k];
436 let x3 = sorted[n - 4 * k];
437
438 let num = x1 - x2;
439 let den = x2 - x3;
440
441 if den.abs() < 1e-15 {
442 return Err(StatsError::ComputationError(
443 "Pickands estimator: degenerate order statistics (denominator ≈ 0)".into(),
444 ));
445 }
446 if num / den <= 0.0 {
447 return Err(StatsError::ComputationError(
448 "Pickands estimator: invalid ratio (non-positive)".into(),
449 ));
450 }
451
452 let xi = (num / den).ln() / 2.0_f64.ln();
453 Ok(xi)
454}
455
456pub fn return_level_confidence(
477 params: &GeneralizedExtremeValue,
478 return_period: f64,
479 n_data: usize,
480 alpha: f64,
481) -> Result<(f64, f64, f64), StatsError> {
482 if return_period <= 1.0 {
483 return Err(StatsError::InvalidArgument(format!(
484 "return_period must be > 1, got {return_period}"
485 )));
486 }
487 if !(0.0 < alpha && alpha < 0.5) {
488 return Err(StatsError::InvalidArgument(format!(
489 "alpha must be in (0, 0.5), got {alpha}"
490 )));
491 }
492 if n_data < 3 {
493 return Err(StatsError::InsufficientData(
494 "At least 3 observations needed for confidence interval".into(),
495 ));
496 }
497
498 let estimate = params.return_level(return_period)?;
499
500 let h = 1e-5;
503 let mu = params.mu;
504 let sigma = params.sigma;
505 let xi = params.xi;
506
507 let rl = |mu2: f64, sig2: f64, xi2: f64| -> f64 {
508 GeneralizedExtremeValue::new(mu2, sig2, xi2)
509 .ok()
510 .and_then(|g| g.return_level(return_period).ok())
511 .unwrap_or(f64::NAN)
512 };
513
514 let d_mu = (rl(mu + h, sigma, xi) - rl(mu - h, sigma, xi)) / (2.0 * h);
515 let d_sigma = (rl(mu, sigma + h, xi) - rl(mu, sigma - h, xi)) / (2.0 * h);
516 let d_xi = (rl(mu, sigma, xi + h) - rl(mu, sigma, xi - h)) / (2.0 * h);
517
518 if !d_mu.is_finite() || !d_sigma.is_finite() || !d_xi.is_finite() {
519 return Err(StatsError::ComputationError(
520 "Delta method: gradient computation failed at these parameters".into(),
521 ));
522 }
523
524 let nf = n_data as f64;
528 let var_mu = sigma.powi(2) / nf;
529 let var_sigma = sigma.powi(2) / (2.0 * nf);
530 let var_xi = (1.0 + xi).powi(2) / nf;
531
532 let var_rl = d_mu.powi(2) * var_mu + d_sigma.powi(2) * var_sigma + d_xi.powi(2) * var_xi;
533
534 if var_rl < 0.0 || !var_rl.is_finite() {
535 return Err(StatsError::ComputationError(
536 "Delta method: negative or invalid variance estimate".into(),
537 ));
538 }
539
540 let z = normal_quantile(1.0 - alpha / 2.0);
542 let half_width = z * var_rl.sqrt();
543
544 Ok((estimate - half_width, estimate, estimate + half_width))
545}
546
547fn normal_quantile(p: f64) -> f64 {
549 let p = p.clamp(1e-15, 1.0 - 1e-15);
551 let q = if p < 0.5 { p } else { 1.0 - p };
552 let t = (-2.0 * q.ln()).sqrt();
553 const C: [f64; 3] = [2.515517, 0.802853, 0.010328];
554 const D: [f64; 3] = [1.432788, 0.189269, 0.001308];
555 let num = C[0] + C[1] * t + C[2] * t.powi(2);
556 let den = 1.0 + D[0] * t + D[1] * t.powi(2) + D[2] * t.powi(3);
557 let z = t - num / den;
558 if p >= 0.5 {
559 z
560 } else {
561 -z
562 }
563}
564
565pub fn empirical_return_periods(
581 data: ArrayView1<f64>,
582 plotting_position: PlottingPosition,
583) -> Result<(Array1<f64>, Array1<f64>), StatsError> {
584 let n = data.len();
585 if n == 0 {
586 return Err(StatsError::InsufficientData(
587 "Data must not be empty".into(),
588 ));
589 }
590
591 let mut sorted: Vec<f64> = data.iter().copied().collect();
592 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
593 let nf = n as f64;
594
595 let return_periods: Vec<f64> = (1..=n)
596 .map(|i| {
597 let if64 = i as f64;
598 let f_i = match plotting_position {
600 PlottingPosition::Weibull => if64 / (nf + 1.0),
601 PlottingPosition::Gringorten => (if64 - 0.44) / (nf + 0.12),
602 PlottingPosition::Blom => (if64 - 0.375) / (nf + 0.25),
603 PlottingPosition::Hazen => (if64 - 0.5) / nf,
604 };
605 let f_i = f_i.clamp(1e-10, 1.0 - 1e-10);
606 1.0 / (1.0 - f_i)
607 })
608 .collect();
609
610 Ok((Array1::from(sorted), Array1::from(return_periods)))
611}
612
613#[cfg(test)]
618mod tests {
619 use super::*;
620 use scirs2_core::ndarray::{array, Array1};
621
622 fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
623 (a - b).abs() < tol
624 }
625
626 fn relative_eq(a: f64, b: f64, rtol: f64) -> bool {
627 let denom = b.abs().max(1e-12);
628 (a - b).abs() / denom < rtol
629 }
630
631 fn gumbel_sample(mu: f64, beta: f64, n: usize, seed: u64) -> Array1<f64> {
633 use super::super::distributions::Gumbel;
634 let g = Gumbel::new(mu, beta).unwrap();
635 Array1::from(g.sample(n, seed))
636 }
637
638 #[test]
641 fn test_block_maxima_basic() {
642 let data = gumbel_sample(10.0, 2.0, 500, 1);
643 let result = block_maxima_analysis(
644 data.view(),
645 50,
646 &[10.0, 100.0],
647 EvtEstimationMethod::LMoments,
648 )
649 .unwrap();
650 assert_eq!(result.n_blocks, 10);
651 assert_eq!(result.block_maxima.len(), 10);
652 assert_eq!(result.return_levels.len(), 2);
653 assert!(result.gev_params.sigma > 0.0);
654 }
655
656 #[test]
657 fn test_block_maxima_return_levels_increasing() {
658 let data = gumbel_sample(5.0, 1.5, 1000, 2);
659 let result = block_maxima_analysis(
660 data.view(),
661 100,
662 &[10.0, 50.0, 100.0, 1000.0],
663 EvtEstimationMethod::LMoments,
664 )
665 .unwrap();
666 let levels: Vec<f64> = result.return_levels.iter().map(|&(_, l)| l).collect();
667 for w in levels.windows(2) {
669 assert!(
670 w[1] >= w[0] - 1e-6,
671 "levels not non-decreasing: {:?}",
672 levels
673 );
674 }
675 }
676
677 #[test]
678 fn test_block_maxima_mle() {
679 let data = gumbel_sample(0.0, 1.0, 400, 10);
680 let result =
681 block_maxima_analysis(data.view(), 40, &[10.0, 100.0], EvtEstimationMethod::MLE);
682 assert!(
683 result.is_ok(),
684 "MLE block maxima failed: {:?}",
685 result.err()
686 );
687 }
688
689 #[test]
690 fn test_block_maxima_pwm() {
691 let data = gumbel_sample(0.0, 1.0, 400, 11);
692 let result = block_maxima_analysis(data.view(), 40, &[10.0], EvtEstimationMethod::PWM);
693 assert!(result.is_ok());
694 }
695
696 #[test]
697 fn test_block_maxima_zero_block_size_error() {
698 let data = gumbel_sample(0.0, 1.0, 100, 3);
699 assert!(
700 block_maxima_analysis(data.view(), 0, &[10.0], EvtEstimationMethod::LMoments).is_err()
701 );
702 }
703
704 #[test]
705 fn test_block_maxima_too_few_blocks_error() {
706 let data = gumbel_sample(0.0, 1.0, 50, 4);
708 assert!(
709 block_maxima_analysis(data.view(), 60, &[10.0], EvtEstimationMethod::LMoments).is_err()
710 );
711 }
712
713 #[test]
714 fn test_block_maxima_invalid_return_period_error() {
715 let data = gumbel_sample(0.0, 1.0, 200, 5);
716 assert!(
717 block_maxima_analysis(data.view(), 20, &[0.5], EvtEstimationMethod::LMoments).is_err()
718 );
719 }
720
721 #[test]
724 fn test_pot_basic() {
725 let data = gumbel_sample(5.0, 2.0, 500, 20);
726 let threshold = 7.0;
727 let result = peaks_over_threshold(data.view(), threshold, &[10.0, 100.0]).unwrap();
728 assert_eq!(result.threshold, threshold);
729 assert!(result.n_exceedances > 0);
730 assert!(result.rate > 0.0 && result.rate < 1.0);
731 assert!(result.gpd_params.sigma > 0.0);
732 }
733
734 #[test]
735 fn test_pot_return_levels_increasing() {
736 let data = gumbel_sample(0.0, 1.0, 1000, 21);
737 let result = peaks_over_threshold(data.view(), 1.0, &[5.0, 10.0, 50.0, 100.0]).unwrap();
738 let levels: Vec<f64> = result.return_levels.iter().map(|&(_, l)| l).collect();
739 for w in levels.windows(2) {
740 assert!(w[1] >= w[0] - 1e-6, "{:?}", levels);
741 }
742 }
743
744 #[test]
745 fn test_pot_insufficient_exceedances_error() {
746 let data = array![1.0, 2.0, 3.0, 4.0, 100.0];
747 assert!(peaks_over_threshold(data.view(), 50.0, &[10.0]).is_err());
749 }
750
751 #[test]
752 fn test_pot_invalid_return_period_error() {
753 let data = gumbel_sample(0.0, 1.0, 200, 22);
754 assert!(peaks_over_threshold(data.view(), 0.0, &[0.5]).is_err());
755 }
756
757 #[test]
760 fn test_mean_excess_plot_basic() {
761 let data = gumbel_sample(0.0, 1.0, 200, 30);
762 let (thresholds, me) = mean_excess_plot(data.view(), 20).unwrap();
763 assert!(!thresholds.is_empty());
764 assert_eq!(thresholds.len(), me.len());
765 assert!(me.iter().all(|&v| v >= 0.0));
766 }
767
768 #[test]
769 fn test_mean_excess_plot_insufficient_data_error() {
770 let data = array![1.0, 2.0, 3.0];
771 assert!(mean_excess_plot(data.view(), 10).is_err());
772 }
773
774 #[test]
775 fn test_mean_excess_exponential_linear() {
776 use super::super::distributions::GeneralizedPareto;
778 let gpd = GeneralizedPareto::new(0.0, 2.0, 0.0).unwrap(); let samples = gpd.sample(500, 42);
780 let arr = Array1::from(samples);
781 let (_, me) = mean_excess_plot(arr.view(), 15).unwrap();
782 let first = me[0];
784 let last = me[me.len() - 1];
785 assert!(first > 0.0 && last > 0.0);
787 }
788
789 #[test]
792 fn test_hill_basic_heavy_tail() {
793 let alpha = 2.0_f64;
795 let mut data: Vec<f64> = (1..=500)
797 .map(|i| {
798 let u = i as f64 / 501.0;
799 (1.0 - u).powf(-1.0 / alpha)
800 })
801 .collect();
802 data.sort_by(|a, b| a.partial_cmp(b).unwrap());
803 let arr = Array1::from(data);
804 let xi_hat = hill_estimator(arr.view(), 50).unwrap();
805 assert!(relative_eq(xi_hat, 1.0 / alpha, 0.25), "xi_hat={xi_hat}");
807 }
808
809 #[test]
810 fn test_hill_invalid_k_error() {
811 let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
812 assert!(hill_estimator(data.view(), 0).is_err());
813 assert!(hill_estimator(data.view(), 5).is_err()); }
815
816 #[test]
817 fn test_hill_insufficient_data_error() {
818 let data = array![1.0];
819 assert!(hill_estimator(data.view(), 1).is_err());
820 }
821
822 #[test]
825 fn test_pickands_basic() {
826 let alpha = 2.0_f64;
828 let data: Vec<f64> = (1..=200)
829 .map(|i| {
830 let u = i as f64 / 201.0;
831 (1.0 - u).powf(-1.0 / alpha)
832 })
833 .collect();
834 let arr = Array1::from(data);
835 let k = 10;
836 let xi_hat = pickands_estimator(arr.view(), k).unwrap();
837 assert!(xi_hat.is_finite(), "xi_hat={xi_hat}");
839 }
840
841 #[test]
842 fn test_pickands_invalid_k_error() {
843 let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
844 assert!(pickands_estimator(data.view(), 0).is_err());
845 assert!(pickands_estimator(data.view(), 3).is_err()); }
847
848 #[test]
851 fn test_return_level_ci_basic() {
852 let gev = GeneralizedExtremeValue::new(0.0, 1.0, 0.1).unwrap();
853 let (lo, est, hi) = return_level_confidence(&gev, 100.0, 50, 0.05).unwrap();
854 assert!(lo < est, "lo={lo} should be < est={est}");
855 assert!(est < hi, "est={est} should be < hi={hi}");
856 }
857
858 #[test]
859 fn test_return_level_ci_wider_for_small_n() {
860 let gev = GeneralizedExtremeValue::new(0.0, 1.0, 0.0).unwrap();
861 let (lo50, _, hi50) = return_level_confidence(&gev, 100.0, 50, 0.05).unwrap();
862 let (lo500, _, hi500) = return_level_confidence(&gev, 100.0, 500, 0.05).unwrap();
863 let width_50 = hi50 - lo50;
864 let width_500 = hi500 - lo500;
865 assert!(width_50 > width_500, "Smaller n should give wider CI");
866 }
867
868 #[test]
869 fn test_return_level_ci_invalid_inputs() {
870 let gev = GeneralizedExtremeValue::new(0.0, 1.0, 0.0).unwrap();
871 assert!(return_level_confidence(&gev, 0.5, 100, 0.05).is_err());
872 assert!(return_level_confidence(&gev, 100.0, 100, 0.0).is_err());
873 assert!(return_level_confidence(&gev, 100.0, 100, 0.6).is_err());
874 }
875
876 #[test]
879 fn test_empirical_return_periods_weibull() {
880 let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
881 let (sorted, rp) =
882 empirical_return_periods(data.view(), PlottingPosition::Weibull).unwrap();
883 assert_eq!(sorted.len(), 5);
884 assert_eq!(rp.len(), 5);
885 for w in rp.iter().collect::<Vec<_>>().windows(2) {
887 assert!(w[1] > w[0]);
888 }
889 }
890
891 #[test]
892 fn test_empirical_return_periods_all_methods() {
893 let data = gumbel_sample(0.0, 1.0, 100, 50);
894 for method in [
895 PlottingPosition::Weibull,
896 PlottingPosition::Gringorten,
897 PlottingPosition::Blom,
898 PlottingPosition::Hazen,
899 ] {
900 let (s, rp) = empirical_return_periods(data.view(), method).unwrap();
901 assert_eq!(s.len(), 100);
902 assert_eq!(rp.len(), 100);
903 assert!(rp.iter().all(|&r| r >= 1.0));
904 }
905 }
906
907 #[test]
908 fn test_empirical_return_periods_empty_error() {
909 let data: Array1<f64> = Array1::zeros(0);
910 assert!(empirical_return_periods(data.view(), PlottingPosition::Weibull).is_err());
911 }
912
913 #[test]
914 fn test_normal_quantile_symmetry() {
915 let z_95 = normal_quantile(0.975);
916 let z_05 = normal_quantile(0.025);
917 assert!(approx_eq(z_95, -z_05, 1e-6));
918 assert!(approx_eq(z_95, 1.96, 0.01));
920 }
921}