1use ferray_core::error::{FerrayError, FerrayResult};
5use ferray_core::{Array, Dimension, Element, IxDyn};
6use num_traits::Float;
7
8use super::{collect_data, make_result, output_shape, reduce_axis_general, validate_axis};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum QuantileMethod {
25 Linear,
28 Lower,
31 Higher,
34 Nearest,
38 Midpoint,
40 InvertedCdf,
44 AveragedInvertedCdf,
49 ClosestObservation,
53 InterpolatedInvertedCdf,
56 Hazen,
59 Weibull,
62 MedianUnbiased,
65 NormalUnbiased,
68}
69
70#[inline]
76fn continuous_vidx<T: Float>(n: usize, q: T, alpha: T, beta: T) -> (usize, T) {
77 let nf = T::from(n).unwrap();
78 let zero = T::zero();
79 let one = T::one();
80 let n_minus_1 = T::from(n - 1).unwrap();
81
82 let vidx = nf * q + alpha + q * (one - alpha - beta) - one;
83
84 let vidx_clamped = if vidx < zero {
86 zero
87 } else if vidx > n_minus_1 {
88 n_minus_1
89 } else {
90 vidx
91 };
92
93 let lo = vidx_clamped.floor();
94 let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
95 let gamma = vidx_clamped - lo;
96 (lo_i, gamma)
97}
98
99#[inline]
101fn round_half_to_even<T: Float>(x: T) -> T {
102 let floor = x.floor();
103 let frac = x - floor;
104 let half = T::from(0.5).unwrap();
105 if frac < half {
106 floor
107 } else if frac > half {
108 floor + T::one()
109 } else {
110 let floor_i = floor.to_i64().unwrap_or(0);
112 if floor_i.rem_euclid(2) == 0 {
113 floor
114 } else {
115 floor + T::one()
116 }
117 }
118}
119
120fn method_index_and_gamma<T: Float>(n: usize, q: T, method: QuantileMethod) -> (usize, T) {
126 let zero = T::zero();
127 let one = T::one();
128 let half = T::from(0.5).unwrap();
129 let nf = T::from(n).unwrap();
130
131 match method {
132 QuantileMethod::Linear => continuous_vidx(n, q, one, one),
134 QuantileMethod::Weibull => continuous_vidx(n, q, zero, zero),
135 QuantileMethod::Hazen => continuous_vidx(n, q, half, half),
136 QuantileMethod::InterpolatedInvertedCdf => continuous_vidx(n, q, zero, one),
137 QuantileMethod::MedianUnbiased => {
138 let third = T::from(1.0 / 3.0).unwrap();
139 continuous_vidx(n, q, third, third)
140 }
141 QuantileMethod::NormalUnbiased => {
142 let ae = T::from(3.0 / 8.0).unwrap();
143 continuous_vidx(n, q, ae, ae)
144 }
145
146 QuantileMethod::Lower => {
149 let vidx = q * T::from(n - 1).unwrap();
150 let lo_i = vidx.floor().to_usize().unwrap_or(0).min(n - 1);
151 (lo_i, zero)
152 }
153 QuantileMethod::Higher => {
154 let vidx = q * T::from(n - 1).unwrap();
155 let lo = vidx.floor();
156 let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
157 let frac = vidx - lo;
158 if frac > zero && lo_i + 1 < n {
161 (lo_i, one)
162 } else {
163 (lo_i, zero)
164 }
165 }
166 QuantileMethod::Nearest => {
167 let vidx = q * T::from(n - 1).unwrap();
168 let lo = vidx.floor();
169 let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
170 let frac = vidx - lo;
171 if frac < half {
172 (lo_i, zero)
173 } else if frac > half {
174 if lo_i + 1 < n {
175 (lo_i, one)
176 } else {
177 (lo_i, zero)
178 }
179 } else {
180 if lo_i % 2 == 0 || lo_i + 1 >= n {
182 (lo_i, zero)
183 } else {
184 (lo_i, one)
185 }
186 }
187 }
188 QuantileMethod::Midpoint => {
189 let vidx = q * T::from(n - 1).unwrap();
190 let lo = vidx.floor();
191 let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
192 let frac = vidx - lo;
193 if frac > zero && lo_i + 1 < n {
194 (lo_i, half)
195 } else {
196 (lo_i, zero)
197 }
198 }
199
200 QuantileMethod::InvertedCdf => {
202 let nq = nf * q;
204 let k = if nq <= zero {
205 0
206 } else {
207 nq.ceil()
208 .to_usize()
209 .unwrap_or(0)
210 .saturating_sub(1)
211 .min(n - 1)
212 };
213 (k, zero)
214 }
215 QuantileMethod::AveragedInvertedCdf => {
216 let nq = nf * q;
219 let floor_nq = nq.floor();
220 let is_integer = nq == floor_nq;
221 if is_integer && nq > zero && nq < nf {
222 let k = floor_nq.to_usize().unwrap_or(0);
225 let lo_i = k.saturating_sub(1).min(n - 1);
226 if lo_i + 1 < n {
227 (lo_i, half)
228 } else {
229 (lo_i, zero)
230 }
231 } else {
232 let k = if nq <= zero {
234 0
235 } else {
236 nq.ceil()
237 .to_usize()
238 .unwrap_or(0)
239 .saturating_sub(1)
240 .min(n - 1)
241 };
242 (k, zero)
243 }
244 }
245 QuantileMethod::ClosestObservation => {
246 let nq = nf * q;
248 let adj = nq - half;
249 let rounded = round_half_to_even(adj);
250 let k = if rounded < zero {
251 0
252 } else {
253 rounded.to_usize().unwrap_or(0).min(n - 1)
254 };
255 (k, zero)
256 }
257 }
258}
259
260fn quantile_select<T: Float>(mut data: Vec<T>, q: T, method: QuantileMethod) -> T {
272 let n = data.len();
273 if n == 0 {
274 return T::nan();
275 }
276 if n == 1 {
277 return data[0];
278 }
279
280 let cmp = |a: &T, b: &T| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
281 let (lo_i, gamma) = method_index_and_gamma(n, q, method);
282
283 data.select_nth_unstable_by(lo_i, cmp);
285 let lo_val = data[lo_i];
286
287 if gamma == T::zero() || lo_i >= n - 1 {
290 return lo_val;
291 }
292
293 let hi_val = data[lo_i + 1..]
298 .iter()
299 .copied()
300 .reduce(|a, b| match cmp(&a, &b) {
301 std::cmp::Ordering::Less | std::cmp::Ordering::Equal => a,
302 std::cmp::Ordering::Greater => b,
303 })
304 .unwrap_or(lo_val);
305
306 (T::one() - gamma) * lo_val + gamma * hi_val
307}
308
309fn lane_quantile_with_method<T: Float>(lane: &[T], q: T, method: QuantileMethod) -> T {
311 quantile_select(lane.to_vec(), q, method)
312}
313
314fn lane_nanquantile<T: Float>(lane: &[T], q: T) -> T {
316 let filtered: Vec<T> = lane.iter().copied().filter(|x| !x.is_nan()).collect();
317 if filtered.is_empty() {
318 return T::nan();
319 }
320 quantile_select(filtered, q, QuantileMethod::Linear)
321}
322
323pub fn quantile<T, D>(a: &Array<T, D>, q: T, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
333where
334 T: Element + Float,
335 D: Dimension,
336{
337 quantile_with_method(a, q, axis, QuantileMethod::Linear)
338}
339
340pub fn quantile_with_method<T, D>(
352 a: &Array<T, D>,
353 q: T,
354 axis: Option<usize>,
355 method: QuantileMethod,
356) -> FerrayResult<Array<T, IxDyn>>
357where
358 T: Element + Float,
359 D: Dimension,
360{
361 if q < <T as Element>::zero() || q > <T as Element>::one() {
362 return Err(FerrayError::invalid_value("quantile q must be in [0, 1]"));
363 }
364 if a.is_empty() {
365 return Err(FerrayError::invalid_value(
366 "cannot compute quantile of empty array",
367 ));
368 }
369 let data = collect_data(a);
370 match axis {
371 None => {
372 let val = lane_quantile_with_method(&data, q, method);
373 make_result(&[], vec![val])
374 }
375 Some(ax) => {
376 validate_axis(ax, a.ndim())?;
377 let shape = a.shape();
378 let out_s = output_shape(shape, ax);
379 let result = reduce_axis_general(&data, shape, ax, |lane| {
380 lane_quantile_with_method(lane, q, method)
381 });
382 make_result(&out_s, result)
383 }
384 }
385}
386
387pub fn percentile<T, D>(a: &Array<T, D>, q: T, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
399where
400 T: Element + Float,
401 D: Dimension,
402{
403 percentile_with_method(a, q, axis, QuantileMethod::Linear)
404}
405
406pub fn percentile_with_method<T, D>(
413 a: &Array<T, D>,
414 q: T,
415 axis: Option<usize>,
416 method: QuantileMethod,
417) -> FerrayResult<Array<T, IxDyn>>
418where
419 T: Element + Float,
420 D: Dimension,
421{
422 let hundred = T::from(100.0).unwrap();
423 if q < <T as Element>::zero() || q > hundred {
424 return Err(FerrayError::invalid_value(
425 "percentile q must be in [0, 100]",
426 ));
427 }
428 quantile_with_method(a, q / hundred, axis, method)
429}
430
431pub fn median<T, D>(a: &Array<T, D>, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
439where
440 T: Element + Float,
441 D: Dimension,
442{
443 let half = T::from(0.5).unwrap();
444 quantile(a, half, axis)
445}
446
447pub fn nanmedian<T, D>(a: &Array<T, D>, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
455where
456 T: Element + Float,
457 D: Dimension,
458{
459 let half = T::from(0.5).unwrap();
460 nanquantile(a, half, axis)
461}
462
463pub fn nanpercentile<T, D>(
467 a: &Array<T, D>,
468 q: T,
469 axis: Option<usize>,
470) -> FerrayResult<Array<T, IxDyn>>
471where
472 T: Element + Float,
473 D: Dimension,
474{
475 let hundred = T::from(100.0).unwrap();
476 if q < <T as Element>::zero() || q > hundred {
477 return Err(FerrayError::invalid_value(
478 "nanpercentile q must be in [0, 100]",
479 ));
480 }
481 nanquantile(a, q / hundred, axis)
482}
483
484pub fn nanquantile<T, D>(
488 a: &Array<T, D>,
489 q: T,
490 axis: Option<usize>,
491) -> FerrayResult<Array<T, IxDyn>>
492where
493 T: Element + Float,
494 D: Dimension,
495{
496 if q < <T as Element>::zero() || q > <T as Element>::one() {
497 return Err(FerrayError::invalid_value("quantile q must be in [0, 1]"));
498 }
499 if a.is_empty() {
500 return Err(FerrayError::invalid_value(
501 "cannot compute nanquantile of empty array",
502 ));
503 }
504 let data = collect_data(a);
505 match axis {
506 None => {
507 let val = lane_nanquantile(&data, q);
508 make_result(&[], vec![val])
509 }
510 Some(ax) => {
511 validate_axis(ax, a.ndim())?;
512 let shape = a.shape();
513 let out_s = output_shape(shape, ax);
514 let result = reduce_axis_general(&data, shape, ax, |lane| lane_nanquantile(lane, q));
515 make_result(&out_s, result)
516 }
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 use ferray_core::Ix1;
524
525 #[test]
526 fn test_median_odd() {
527 let a = Array::<f64, Ix1>::from_vec(Ix1::new([5]), vec![5.0, 1.0, 3.0, 2.0, 4.0]).unwrap();
528 let m = median(&a, None).unwrap();
529 assert!((m.iter().next().unwrap() - 3.0).abs() < 1e-12);
530 }
531
532 #[test]
533 fn test_median_even() {
534 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![4.0, 1.0, 3.0, 2.0]).unwrap();
535 let m = median(&a, None).unwrap();
536 assert!((m.iter().next().unwrap() - 2.5).abs() < 1e-12);
537 }
538
539 #[test]
540 fn test_percentile_0_50_100() {
541 let a = Array::<f64, Ix1>::from_vec(Ix1::new([5]), vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
542 let p0 = percentile(&a, 0.0, None).unwrap();
543 let p50 = percentile(&a, 50.0, None).unwrap();
544 let p100 = percentile(&a, 100.0, None).unwrap();
545 assert!((p0.iter().next().unwrap() - 1.0).abs() < 1e-12);
546 assert!((p50.iter().next().unwrap() - 3.0).abs() < 1e-12);
547 assert!((p100.iter().next().unwrap() - 5.0).abs() < 1e-12);
548 }
549
550 #[test]
551 fn test_quantile_bounds() {
552 let a = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
553 assert!(quantile(&a, -0.1, None).is_err());
554 assert!(quantile(&a, 1.1, None).is_err());
555 }
556
557 #[test]
558 fn test_quantile_interpolation() {
559 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
560 let q = quantile(&a, 0.25, None).unwrap();
561 assert!((q.iter().next().unwrap() - 1.75).abs() < 1e-12);
563 }
564
565 #[test]
566 fn test_nanmedian() {
567 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, f64::NAN, 3.0, 5.0]).unwrap();
568 let m = nanmedian(&a, None).unwrap();
569 assert!((m.iter().next().unwrap() - 3.0).abs() < 1e-12);
571 }
572
573 #[test]
574 fn test_nanpercentile() {
575 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, f64::NAN, 3.0, 5.0]).unwrap();
576 let p = nanpercentile(&a, 50.0, None).unwrap();
577 assert!((p.iter().next().unwrap() - 3.0).abs() < 1e-12);
578 }
579
580 #[test]
581 fn test_nanmedian_all_nan() {
582 let a = Array::<f64, Ix1>::from_vec(Ix1::new([2]), vec![f64::NAN, f64::NAN]).unwrap();
583 let m = nanmedian(&a, None).unwrap();
584 assert!(m.iter().next().unwrap().is_nan());
585 }
586
587 fn arr_1_5() -> Array<f64, Ix1> {
590 Array::<f64, Ix1>::from_vec(Ix1::new([5]), vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap()
591 }
592
593 #[test]
594 fn test_quantile_method_linear_matches_legacy() {
595 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
597 let legacy = quantile(&a, 0.25, None).unwrap();
598 let with_flag = quantile_with_method(&a, 0.25, None, QuantileMethod::Linear).unwrap();
599 assert_eq!(
600 legacy.iter().next().unwrap(),
601 with_flag.iter().next().unwrap()
602 );
603 }
604
605 #[test]
606 fn test_quantile_method_lower() {
607 let a = arr_1_5();
610 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Lower).unwrap();
611 assert!((q.iter().next().unwrap() - 2.0).abs() < 1e-12);
612
613 let a4 = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
615 let q = quantile_with_method(&a4, 0.25, None, QuantileMethod::Lower).unwrap();
616 assert!((q.iter().next().unwrap() - 1.0).abs() < 1e-12);
617 }
618
619 #[test]
620 fn test_quantile_method_higher() {
621 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
623 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Higher).unwrap();
624 assert!((q.iter().next().unwrap() - 2.0).abs() < 1e-12);
625 }
626
627 #[test]
628 fn test_quantile_method_nearest_round_down() {
629 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
631 let q = quantile_with_method(&a, 0.2, None, QuantileMethod::Nearest).unwrap();
632 assert!((q.iter().next().unwrap() - 2.0).abs() < 1e-12);
633
634 let q2 = quantile_with_method(&a, 0.1, None, QuantileMethod::Nearest).unwrap();
636 assert!((q2.iter().next().unwrap() - 1.0).abs() < 1e-12);
637 }
638
639 #[test]
640 fn test_quantile_method_nearest_tie_even() {
641 let a = arr_1_5();
643 let q = quantile_with_method(&a, 0.125, None, QuantileMethod::Nearest).unwrap();
644 assert!((q.iter().next().unwrap() - 1.0).abs() < 1e-12);
645
646 let q2 = quantile_with_method(&a, 0.375, None, QuantileMethod::Nearest).unwrap();
648 assert!((q2.iter().next().unwrap() - 3.0).abs() < 1e-12);
649 }
650
651 #[test]
652 fn test_quantile_method_midpoint() {
653 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
655 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Midpoint).unwrap();
656 assert!((q.iter().next().unwrap() - 1.5).abs() < 1e-12);
657
658 let q2 = quantile_with_method(&a, 0.75, None, QuantileMethod::Midpoint).unwrap();
660 assert!((q2.iter().next().unwrap() - 3.5).abs() < 1e-12);
661 }
662
663 #[test]
664 fn test_quantile_method_integer_index_all_agree() {
665 let a = arr_1_5();
668 let linear = quantile_with_method(&a, 0.5, None, QuantileMethod::Linear).unwrap();
669 let lower = quantile_with_method(&a, 0.5, None, QuantileMethod::Lower).unwrap();
670 let higher = quantile_with_method(&a, 0.5, None, QuantileMethod::Higher).unwrap();
671 let nearest = quantile_with_method(&a, 0.5, None, QuantileMethod::Nearest).unwrap();
672 let midpoint = quantile_with_method(&a, 0.5, None, QuantileMethod::Midpoint).unwrap();
673 let expected = 3.0;
674 assert!((linear.iter().next().unwrap() - expected).abs() < 1e-12);
675 assert!((lower.iter().next().unwrap() - expected).abs() < 1e-12);
676 assert!((higher.iter().next().unwrap() - expected).abs() < 1e-12);
677 assert!((nearest.iter().next().unwrap() - expected).abs() < 1e-12);
678 assert!((midpoint.iter().next().unwrap() - expected).abs() < 1e-12);
679 }
680
681 #[test]
682 fn test_quantile_method_axis_variant() {
683 use ferray_core::Ix2;
685 let a = Array::<f64, Ix2>::from_vec(
686 Ix2::new([2, 4]),
687 vec![1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0],
688 )
689 .unwrap();
690 let r = quantile_with_method(&a, 0.25, Some(1), QuantileMethod::Lower).unwrap();
692 assert_eq!(r.as_slice().unwrap(), &[1.0, 10.0]);
693 }
694
695 #[test]
696 fn test_percentile_with_method_50() {
697 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
698 let lin = percentile_with_method(&a, 50.0, None, QuantileMethod::Linear).unwrap();
701 let lo = percentile_with_method(&a, 50.0, None, QuantileMethod::Lower).unwrap();
702 let hi = percentile_with_method(&a, 50.0, None, QuantileMethod::Higher).unwrap();
703 let nr = percentile_with_method(&a, 50.0, None, QuantileMethod::Nearest).unwrap();
704 let mp = percentile_with_method(&a, 50.0, None, QuantileMethod::Midpoint).unwrap();
705 assert!((lin.iter().next().unwrap() - 2.5).abs() < 1e-12);
706 assert!((lo.iter().next().unwrap() - 2.0).abs() < 1e-12);
707 assert!((hi.iter().next().unwrap() - 3.0).abs() < 1e-12);
708 assert!((nr.iter().next().unwrap() - 3.0).abs() < 1e-12);
709 assert!((mp.iter().next().unwrap() - 2.5).abs() < 1e-12);
710 }
711
712 #[test]
713 fn test_percentile_with_method_rejects_out_of_range() {
714 let a = arr_1_5();
715 assert!(percentile_with_method(&a, -1.0, None, QuantileMethod::Linear).is_err());
716 assert!(percentile_with_method(&a, 101.0, None, QuantileMethod::Linear).is_err());
717 }
718
719 fn arr_1_4() -> Array<f64, Ix1> {
728 Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap()
729 }
730
731 #[test]
732 fn test_quantile_weibull_q_half() {
733 let a = arr_1_4();
737 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::Weibull).unwrap();
738 assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
739 }
740
741 #[test]
742 fn test_quantile_weibull_q_quarter() {
743 let a = arr_1_4();
747 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Weibull).unwrap();
748 assert!((q.iter().next().copied().unwrap() - 1.25).abs() < 1e-12);
749 }
750
751 #[test]
752 fn test_quantile_hazen_q_quarter() {
753 let a = arr_1_4();
757 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Hazen).unwrap();
758 assert!((q.iter().next().copied().unwrap() - 1.5).abs() < 1e-12);
759 }
760
761 #[test]
762 fn test_quantile_median_unbiased_q_half() {
763 let a = arr_1_4();
770 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::MedianUnbiased).unwrap();
771 assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
772 }
773
774 #[test]
775 fn test_quantile_normal_unbiased_q_half() {
776 let a = arr_1_4();
783 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::NormalUnbiased).unwrap();
784 assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
785 }
786
787 #[test]
788 fn test_quantile_interpolated_inverted_cdf_q_half() {
789 let a = arr_1_4();
795 let q =
796 quantile_with_method(&a, 0.5, None, QuantileMethod::InterpolatedInvertedCdf).unwrap();
797 assert!((q.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
798 }
799
800 #[test]
801 fn test_quantile_inverted_cdf_q_half() {
802 let a = arr_1_4();
804 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::InvertedCdf).unwrap();
805 assert!((q.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
806 }
807
808 #[test]
809 fn test_quantile_inverted_cdf_step_function() {
810 let a = arr_1_5();
814 let q1 = quantile_with_method(&a, 0.19, None, QuantileMethod::InvertedCdf).unwrap();
815 assert!((q1.iter().next().copied().unwrap() - 1.0).abs() < 1e-12);
816 let q2 = quantile_with_method(&a, 0.21, None, QuantileMethod::InvertedCdf).unwrap();
817 assert!((q2.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
818 }
819
820 #[test]
821 fn test_quantile_averaged_inverted_cdf_integer_nq_averages() {
822 let a = arr_1_4();
825 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::AveragedInvertedCdf).unwrap();
826 assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
827 }
828
829 #[test]
830 fn test_quantile_averaged_inverted_cdf_non_integer_nq_matches_inverted_cdf() {
831 let a = arr_1_5();
834 let q1 = quantile_with_method(&a, 0.3, None, QuantileMethod::AveragedInvertedCdf).unwrap();
835 let q2 = quantile_with_method(&a, 0.3, None, QuantileMethod::InvertedCdf).unwrap();
836 assert_eq!(
837 q1.iter().next().copied().unwrap(),
838 q2.iter().next().copied().unwrap()
839 );
840 assert!((q1.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
841 }
842
843 #[test]
844 fn test_quantile_closest_observation_half_to_even() {
845 let a = arr_1_4();
849 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::ClosestObservation).unwrap();
850 assert!((q.iter().next().copied().unwrap() - 3.0).abs() < 1e-12);
851
852 let q2 = quantile_with_method(&a, 0.125, None, QuantileMethod::ClosestObservation).unwrap();
855 assert!((q2.iter().next().copied().unwrap() - 1.0).abs() < 1e-12);
856 }
857
858 #[test]
859 fn test_quantile_closest_observation_nq_0_875_rounds_up() {
860 let a = arr_1_4();
863 let q = quantile_with_method(&a, 0.875, None, QuantileMethod::ClosestObservation).unwrap();
864 assert!((q.iter().next().copied().unwrap() - 4.0).abs() < 1e-12);
865 }
866
867 #[test]
868 fn test_quantile_continuous_methods_agree_at_q_0_and_q_1() {
869 let a = arr_1_5();
873 let methods = [
874 QuantileMethod::Linear,
875 QuantileMethod::Weibull,
876 QuantileMethod::Hazen,
877 QuantileMethod::InterpolatedInvertedCdf,
878 QuantileMethod::MedianUnbiased,
879 QuantileMethod::NormalUnbiased,
880 ];
881 for &m in &methods {
882 let q0 = quantile_with_method(&a, 0.0, None, m).unwrap();
883 let q1 = quantile_with_method(&a, 1.0, None, m).unwrap();
884 assert!(
885 (q0.iter().next().copied().unwrap() - 1.0).abs() < 1e-12,
886 "method {m:?} at q=0 should be min"
887 );
888 assert!(
889 (q1.iter().next().copied().unwrap() - 5.0).abs() < 1e-12,
890 "method {m:?} at q=1 should be max"
891 );
892 }
893 }
894
895 #[test]
896 fn test_quantile_discrete_methods_agree_at_q_1() {
897 let a = arr_1_5();
899 let methods = [
900 QuantileMethod::InvertedCdf,
901 QuantileMethod::AveragedInvertedCdf,
902 QuantileMethod::ClosestObservation,
903 ];
904 for &m in &methods {
905 let q = quantile_with_method(&a, 1.0, None, m).unwrap();
906 assert!(
907 (q.iter().next().copied().unwrap() - 5.0).abs() < 1e-12,
908 "method {m:?} at q=1 should be max"
909 );
910 }
911 }
912
913 #[test]
914 fn test_quantile_all_13_methods_at_integer_index_agree() {
915 let a = arr_1_5();
921 let all_methods = [
922 QuantileMethod::Linear,
923 QuantileMethod::Lower,
924 QuantileMethod::Higher,
925 QuantileMethod::Nearest,
926 QuantileMethod::Midpoint,
927 QuantileMethod::Weibull,
928 QuantileMethod::Hazen,
929 QuantileMethod::MedianUnbiased,
930 QuantileMethod::NormalUnbiased,
931 ];
934 for &m in &all_methods {
935 let r = quantile_with_method(&a, 0.5, None, m).unwrap();
936 assert!(
937 (r.iter().next().copied().unwrap() - 3.0).abs() < 1e-12,
938 "method {m:?} at odd n, q=0.5 should be 3.0"
939 );
940 }
941 }
942
943 #[test]
944 fn test_quantile_method_axis_variant_weibull() {
945 use ferray_core::Ix2;
946 let a = Array::<f64, Ix2>::from_vec(
949 Ix2::new([2, 4]),
950 vec![1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0],
951 )
952 .unwrap();
953 let r = quantile_with_method(&a, 0.5, Some(1), QuantileMethod::Weibull).unwrap();
954 assert_eq!(r.shape(), &[2]);
955 let s = r.as_slice().unwrap();
956 assert!((s[0] - 2.5).abs() < 1e-12);
957 assert!((s[1] - 25.0).abs() < 1e-12);
958 }
959}