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
120#[allow(clippy::too_many_lines)]
130fn method_index_and_gamma<T: Float>(n: usize, q: T, method: QuantileMethod) -> (usize, T) {
131 let zero = T::zero();
132 let one = T::one();
133 let half = T::from(0.5).unwrap();
134 let nf = T::from(n).unwrap();
135
136 match method {
137 QuantileMethod::Linear => continuous_vidx(n, q, one, one),
139 QuantileMethod::Weibull => continuous_vidx(n, q, zero, zero),
140 QuantileMethod::Hazen => continuous_vidx(n, q, half, half),
141 QuantileMethod::InterpolatedInvertedCdf => continuous_vidx(n, q, zero, one),
142 QuantileMethod::MedianUnbiased => {
143 let third = T::from(1.0 / 3.0).unwrap();
144 continuous_vidx(n, q, third, third)
145 }
146 QuantileMethod::NormalUnbiased => {
147 let ae = T::from(3.0 / 8.0).unwrap();
148 continuous_vidx(n, q, ae, ae)
149 }
150
151 QuantileMethod::Lower => {
154 let vidx = q * T::from(n - 1).unwrap();
155 let lo_i = vidx.floor().to_usize().unwrap_or(0).min(n - 1);
156 (lo_i, zero)
157 }
158 QuantileMethod::Higher => {
159 let vidx = q * T::from(n - 1).unwrap();
160 let lo = vidx.floor();
161 let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
162 let frac = vidx - lo;
163 if frac > zero && lo_i + 1 < n {
166 (lo_i, one)
167 } else {
168 (lo_i, zero)
169 }
170 }
171 QuantileMethod::Nearest => {
172 let vidx = q * T::from(n - 1).unwrap();
173 let lo = vidx.floor();
174 let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
175 let frac = vidx - lo;
176 if frac < half {
177 (lo_i, zero)
178 } else if frac > half {
179 if lo_i + 1 < n {
180 (lo_i, one)
181 } else {
182 (lo_i, zero)
183 }
184 } else {
185 if lo_i % 2 == 0 || lo_i + 1 >= n {
187 (lo_i, zero)
188 } else {
189 (lo_i, one)
190 }
191 }
192 }
193 QuantileMethod::Midpoint => {
194 let vidx = q * T::from(n - 1).unwrap();
195 let lo = vidx.floor();
196 let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
197 let frac = vidx - lo;
198 if frac > zero && lo_i + 1 < n {
199 (lo_i, half)
200 } else {
201 (lo_i, zero)
202 }
203 }
204
205 QuantileMethod::InvertedCdf => {
207 let nq = nf * q;
209 let k = if nq <= zero {
210 0
211 } else {
212 nq.ceil()
213 .to_usize()
214 .unwrap_or(0)
215 .saturating_sub(1)
216 .min(n - 1)
217 };
218 (k, zero)
219 }
220 QuantileMethod::AveragedInvertedCdf => {
221 let nq = nf * q;
224 let floor_nq = nq.floor();
225 let is_integer = nq == floor_nq;
226 if is_integer && nq > zero && nq < nf {
227 let k = floor_nq.to_usize().unwrap_or(0);
230 let lo_i = k.saturating_sub(1).min(n - 1);
231 if lo_i + 1 < n {
232 (lo_i, half)
233 } else {
234 (lo_i, zero)
235 }
236 } else {
237 let k = if nq <= zero {
239 0
240 } else {
241 nq.ceil()
242 .to_usize()
243 .unwrap_or(0)
244 .saturating_sub(1)
245 .min(n - 1)
246 };
247 (k, zero)
248 }
249 }
250 QuantileMethod::ClosestObservation => {
251 let nq = nf * q;
253 let adj = nq - half;
254 let rounded = round_half_to_even(adj);
255 let k = if rounded < zero {
256 0
257 } else {
258 rounded.to_usize().unwrap_or(0).min(n - 1)
259 };
260 (k, zero)
261 }
262 }
263}
264
265fn quantile_select<T: Float>(mut data: Vec<T>, q: T, method: QuantileMethod) -> T {
277 let n = data.len();
278 if n == 0 {
279 return T::nan();
280 }
281 if n == 1 {
282 return data[0];
283 }
284
285 let cmp = |a: &T, b: &T| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
286 let (lo_i, gamma) = method_index_and_gamma(n, q, method);
287
288 data.select_nth_unstable_by(lo_i, cmp);
290 let lo_val = data[lo_i];
291
292 if gamma == T::zero() || lo_i >= n - 1 {
295 return lo_val;
296 }
297
298 let hi_val = data[lo_i + 1..]
303 .iter()
304 .copied()
305 .reduce(|a, b| match cmp(&a, &b) {
306 std::cmp::Ordering::Less | std::cmp::Ordering::Equal => a,
307 std::cmp::Ordering::Greater => b,
308 })
309 .unwrap_or(lo_val);
310
311 (T::one() - gamma) * lo_val + gamma * hi_val
312}
313
314fn lane_quantile_with_method<T: Float>(lane: &[T], q: T, method: QuantileMethod) -> T {
316 quantile_select(lane.to_vec(), q, method)
317}
318
319fn lane_nanquantile<T: Float>(lane: &[T], q: T) -> T {
321 let filtered: Vec<T> = lane.iter().copied().filter(|x| !x.is_nan()).collect();
322 if filtered.is_empty() {
323 return T::nan();
324 }
325 quantile_select(filtered, q, QuantileMethod::Linear)
326}
327
328pub fn quantile<T, D>(a: &Array<T, D>, q: T, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
338where
339 T: Element + Float,
340 D: Dimension,
341{
342 quantile_with_method(a, q, axis, QuantileMethod::Linear)
343}
344
345pub fn quantile_with_method<T, D>(
357 a: &Array<T, D>,
358 q: T,
359 axis: Option<usize>,
360 method: QuantileMethod,
361) -> FerrayResult<Array<T, IxDyn>>
362where
363 T: Element + Float,
364 D: Dimension,
365{
366 if q < <T as Element>::zero() || q > <T as Element>::one() {
367 return Err(FerrayError::invalid_value("quantile q must be in [0, 1]"));
368 }
369 if a.is_empty() {
370 return Err(FerrayError::invalid_value(
371 "cannot compute quantile of empty array",
372 ));
373 }
374 let data = collect_data(a);
375 match axis {
376 None => {
377 let val = lane_quantile_with_method(&data, q, method);
378 make_result(&[], vec![val])
379 }
380 Some(ax) => {
381 validate_axis(ax, a.ndim())?;
382 let shape = a.shape();
383 let out_s = output_shape(shape, ax);
384 let result = reduce_axis_general(&data, shape, ax, |lane| {
385 lane_quantile_with_method(lane, q, method)
386 });
387 make_result(&out_s, result)
388 }
389 }
390}
391
392pub fn percentile<T, D>(a: &Array<T, D>, q: T, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
404where
405 T: Element + Float,
406 D: Dimension,
407{
408 percentile_with_method(a, q, axis, QuantileMethod::Linear)
409}
410
411pub fn percentile_with_method<T, D>(
418 a: &Array<T, D>,
419 q: T,
420 axis: Option<usize>,
421 method: QuantileMethod,
422) -> FerrayResult<Array<T, IxDyn>>
423where
424 T: Element + Float,
425 D: Dimension,
426{
427 let hundred = T::from(100.0).unwrap();
428 if q < <T as Element>::zero() || q > hundred {
429 return Err(FerrayError::invalid_value(
430 "percentile q must be in [0, 100]",
431 ));
432 }
433 quantile_with_method(a, q / hundred, axis, method)
434}
435
436pub fn median<T, D>(a: &Array<T, D>, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
444where
445 T: Element + Float,
446 D: Dimension,
447{
448 let half = T::from(0.5).unwrap();
449 quantile(a, half, axis)
450}
451
452pub fn nanmedian<T, D>(a: &Array<T, D>, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
460where
461 T: Element + Float,
462 D: Dimension,
463{
464 let half = T::from(0.5).unwrap();
465 nanquantile(a, half, axis)
466}
467
468pub fn nanpercentile<T, D>(
472 a: &Array<T, D>,
473 q: T,
474 axis: Option<usize>,
475) -> FerrayResult<Array<T, IxDyn>>
476where
477 T: Element + Float,
478 D: Dimension,
479{
480 let hundred = T::from(100.0).unwrap();
481 if q < <T as Element>::zero() || q > hundred {
482 return Err(FerrayError::invalid_value(
483 "nanpercentile q must be in [0, 100]",
484 ));
485 }
486 nanquantile(a, q / hundred, axis)
487}
488
489pub fn nanquantile<T, D>(
493 a: &Array<T, D>,
494 q: T,
495 axis: Option<usize>,
496) -> FerrayResult<Array<T, IxDyn>>
497where
498 T: Element + Float,
499 D: Dimension,
500{
501 if q < <T as Element>::zero() || q > <T as Element>::one() {
502 return Err(FerrayError::invalid_value("quantile q must be in [0, 1]"));
503 }
504 if a.is_empty() {
505 return Err(FerrayError::invalid_value(
506 "cannot compute nanquantile of empty array",
507 ));
508 }
509 let data = collect_data(a);
510 match axis {
511 None => {
512 let val = lane_nanquantile(&data, q);
513 make_result(&[], vec![val])
514 }
515 Some(ax) => {
516 validate_axis(ax, a.ndim())?;
517 let shape = a.shape();
518 let out_s = output_shape(shape, ax);
519 let result = reduce_axis_general(&data, shape, ax, |lane| lane_nanquantile(lane, q));
520 make_result(&out_s, result)
521 }
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use ferray_core::Ix1;
529
530 #[test]
531 fn test_median_odd() {
532 let a = Array::<f64, Ix1>::from_vec(Ix1::new([5]), vec![5.0, 1.0, 3.0, 2.0, 4.0]).unwrap();
533 let m = median(&a, None).unwrap();
534 assert!((m.iter().next().unwrap() - 3.0).abs() < 1e-12);
535 }
536
537 #[test]
538 fn test_median_even() {
539 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![4.0, 1.0, 3.0, 2.0]).unwrap();
540 let m = median(&a, None).unwrap();
541 assert!((m.iter().next().unwrap() - 2.5).abs() < 1e-12);
542 }
543
544 #[test]
545 fn test_percentile_0_50_100() {
546 let a = Array::<f64, Ix1>::from_vec(Ix1::new([5]), vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
547 let p0 = percentile(&a, 0.0, None).unwrap();
548 let p50 = percentile(&a, 50.0, None).unwrap();
549 let p100 = percentile(&a, 100.0, None).unwrap();
550 assert!((p0.iter().next().unwrap() - 1.0).abs() < 1e-12);
551 assert!((p50.iter().next().unwrap() - 3.0).abs() < 1e-12);
552 assert!((p100.iter().next().unwrap() - 5.0).abs() < 1e-12);
553 }
554
555 #[test]
556 fn test_quantile_bounds() {
557 let a = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
558 assert!(quantile(&a, -0.1, None).is_err());
559 assert!(quantile(&a, 1.1, None).is_err());
560 }
561
562 #[test]
563 fn test_quantile_interpolation() {
564 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
565 let q = quantile(&a, 0.25, None).unwrap();
566 assert!((q.iter().next().unwrap() - 1.75).abs() < 1e-12);
568 }
569
570 #[test]
571 fn test_nanmedian() {
572 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, f64::NAN, 3.0, 5.0]).unwrap();
573 let m = nanmedian(&a, None).unwrap();
574 assert!((m.iter().next().unwrap() - 3.0).abs() < 1e-12);
576 }
577
578 #[test]
579 fn test_nanpercentile() {
580 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, f64::NAN, 3.0, 5.0]).unwrap();
581 let p = nanpercentile(&a, 50.0, None).unwrap();
582 assert!((p.iter().next().unwrap() - 3.0).abs() < 1e-12);
583 }
584
585 #[test]
586 fn test_nanmedian_all_nan() {
587 let a = Array::<f64, Ix1>::from_vec(Ix1::new([2]), vec![f64::NAN, f64::NAN]).unwrap();
588 let m = nanmedian(&a, None).unwrap();
589 assert!(m.iter().next().unwrap().is_nan());
590 }
591
592 fn arr_1_5() -> Array<f64, Ix1> {
595 Array::<f64, Ix1>::from_vec(Ix1::new([5]), vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap()
596 }
597
598 #[test]
599 fn test_quantile_method_linear_matches_legacy() {
600 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
602 let legacy = quantile(&a, 0.25, None).unwrap();
603 let with_flag = quantile_with_method(&a, 0.25, None, QuantileMethod::Linear).unwrap();
604 assert_eq!(
605 legacy.iter().next().unwrap(),
606 with_flag.iter().next().unwrap()
607 );
608 }
609
610 #[test]
611 fn test_quantile_method_lower() {
612 let a = arr_1_5();
615 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Lower).unwrap();
616 assert!((q.iter().next().unwrap() - 2.0).abs() < 1e-12);
617
618 let a4 = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
620 let q = quantile_with_method(&a4, 0.25, None, QuantileMethod::Lower).unwrap();
621 assert!((q.iter().next().unwrap() - 1.0).abs() < 1e-12);
622 }
623
624 #[test]
625 fn test_quantile_method_higher() {
626 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
628 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Higher).unwrap();
629 assert!((q.iter().next().unwrap() - 2.0).abs() < 1e-12);
630 }
631
632 #[test]
633 fn test_quantile_method_nearest_round_down() {
634 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
636 let q = quantile_with_method(&a, 0.2, None, QuantileMethod::Nearest).unwrap();
637 assert!((q.iter().next().unwrap() - 2.0).abs() < 1e-12);
638
639 let q2 = quantile_with_method(&a, 0.1, None, QuantileMethod::Nearest).unwrap();
641 assert!((q2.iter().next().unwrap() - 1.0).abs() < 1e-12);
642 }
643
644 #[test]
645 fn test_quantile_method_nearest_tie_even() {
646 let a = arr_1_5();
648 let q = quantile_with_method(&a, 0.125, None, QuantileMethod::Nearest).unwrap();
649 assert!((q.iter().next().unwrap() - 1.0).abs() < 1e-12);
650
651 let q2 = quantile_with_method(&a, 0.375, None, QuantileMethod::Nearest).unwrap();
653 assert!((q2.iter().next().unwrap() - 3.0).abs() < 1e-12);
654 }
655
656 #[test]
657 fn test_quantile_method_midpoint() {
658 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
660 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Midpoint).unwrap();
661 assert!((q.iter().next().unwrap() - 1.5).abs() < 1e-12);
662
663 let q2 = quantile_with_method(&a, 0.75, None, QuantileMethod::Midpoint).unwrap();
665 assert!((q2.iter().next().unwrap() - 3.5).abs() < 1e-12);
666 }
667
668 #[test]
669 fn test_quantile_method_integer_index_all_agree() {
670 let a = arr_1_5();
673 let linear = quantile_with_method(&a, 0.5, None, QuantileMethod::Linear).unwrap();
674 let lower = quantile_with_method(&a, 0.5, None, QuantileMethod::Lower).unwrap();
675 let higher = quantile_with_method(&a, 0.5, None, QuantileMethod::Higher).unwrap();
676 let nearest = quantile_with_method(&a, 0.5, None, QuantileMethod::Nearest).unwrap();
677 let midpoint = quantile_with_method(&a, 0.5, None, QuantileMethod::Midpoint).unwrap();
678 let expected = 3.0;
679 assert!((linear.iter().next().unwrap() - expected).abs() < 1e-12);
680 assert!((lower.iter().next().unwrap() - expected).abs() < 1e-12);
681 assert!((higher.iter().next().unwrap() - expected).abs() < 1e-12);
682 assert!((nearest.iter().next().unwrap() - expected).abs() < 1e-12);
683 assert!((midpoint.iter().next().unwrap() - expected).abs() < 1e-12);
684 }
685
686 #[test]
687 fn test_quantile_method_axis_variant() {
688 use ferray_core::Ix2;
690 let a = Array::<f64, Ix2>::from_vec(
691 Ix2::new([2, 4]),
692 vec![1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0],
693 )
694 .unwrap();
695 let r = quantile_with_method(&a, 0.25, Some(1), QuantileMethod::Lower).unwrap();
697 assert_eq!(r.as_slice().unwrap(), &[1.0, 10.0]);
698 }
699
700 #[test]
701 fn test_percentile_with_method_50() {
702 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
703 let lin = percentile_with_method(&a, 50.0, None, QuantileMethod::Linear).unwrap();
706 let lo = percentile_with_method(&a, 50.0, None, QuantileMethod::Lower).unwrap();
707 let hi = percentile_with_method(&a, 50.0, None, QuantileMethod::Higher).unwrap();
708 let nr = percentile_with_method(&a, 50.0, None, QuantileMethod::Nearest).unwrap();
709 let mp = percentile_with_method(&a, 50.0, None, QuantileMethod::Midpoint).unwrap();
710 assert!((lin.iter().next().unwrap() - 2.5).abs() < 1e-12);
711 assert!((lo.iter().next().unwrap() - 2.0).abs() < 1e-12);
712 assert!((hi.iter().next().unwrap() - 3.0).abs() < 1e-12);
713 assert!((nr.iter().next().unwrap() - 3.0).abs() < 1e-12);
714 assert!((mp.iter().next().unwrap() - 2.5).abs() < 1e-12);
715 }
716
717 #[test]
718 fn test_percentile_with_method_rejects_out_of_range() {
719 let a = arr_1_5();
720 assert!(percentile_with_method(&a, -1.0, None, QuantileMethod::Linear).is_err());
721 assert!(percentile_with_method(&a, 101.0, None, QuantileMethod::Linear).is_err());
722 }
723
724 fn arr_1_4() -> Array<f64, Ix1> {
733 Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap()
734 }
735
736 #[test]
737 fn test_quantile_weibull_q_half() {
738 let a = arr_1_4();
742 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::Weibull).unwrap();
743 assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
744 }
745
746 #[test]
747 fn test_quantile_weibull_q_quarter() {
748 let a = arr_1_4();
752 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Weibull).unwrap();
753 assert!((q.iter().next().copied().unwrap() - 1.25).abs() < 1e-12);
754 }
755
756 #[test]
757 fn test_quantile_hazen_q_quarter() {
758 let a = arr_1_4();
762 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Hazen).unwrap();
763 assert!((q.iter().next().copied().unwrap() - 1.5).abs() < 1e-12);
764 }
765
766 #[test]
767 fn test_quantile_median_unbiased_q_half() {
768 let a = arr_1_4();
775 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::MedianUnbiased).unwrap();
776 assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
777 }
778
779 #[test]
780 fn test_quantile_normal_unbiased_q_half() {
781 let a = arr_1_4();
788 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::NormalUnbiased).unwrap();
789 assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
790 }
791
792 #[test]
793 fn test_quantile_interpolated_inverted_cdf_q_half() {
794 let a = arr_1_4();
800 let q =
801 quantile_with_method(&a, 0.5, None, QuantileMethod::InterpolatedInvertedCdf).unwrap();
802 assert!((q.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
803 }
804
805 #[test]
806 fn test_quantile_inverted_cdf_q_half() {
807 let a = arr_1_4();
809 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::InvertedCdf).unwrap();
810 assert!((q.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
811 }
812
813 #[test]
814 fn test_quantile_inverted_cdf_step_function() {
815 let a = arr_1_5();
819 let q1 = quantile_with_method(&a, 0.19, None, QuantileMethod::InvertedCdf).unwrap();
820 assert!((q1.iter().next().copied().unwrap() - 1.0).abs() < 1e-12);
821 let q2 = quantile_with_method(&a, 0.21, None, QuantileMethod::InvertedCdf).unwrap();
822 assert!((q2.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
823 }
824
825 #[test]
826 fn test_quantile_averaged_inverted_cdf_integer_nq_averages() {
827 let a = arr_1_4();
830 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::AveragedInvertedCdf).unwrap();
831 assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
832 }
833
834 #[test]
835 fn test_quantile_averaged_inverted_cdf_non_integer_nq_matches_inverted_cdf() {
836 let a = arr_1_5();
839 let q1 = quantile_with_method(&a, 0.3, None, QuantileMethod::AveragedInvertedCdf).unwrap();
840 let q2 = quantile_with_method(&a, 0.3, None, QuantileMethod::InvertedCdf).unwrap();
841 assert_eq!(
842 q1.iter().next().copied().unwrap(),
843 q2.iter().next().copied().unwrap()
844 );
845 assert!((q1.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
846 }
847
848 #[test]
849 fn test_quantile_closest_observation_half_to_even() {
850 let a = arr_1_4();
854 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::ClosestObservation).unwrap();
855 assert!((q.iter().next().copied().unwrap() - 3.0).abs() < 1e-12);
856
857 let q2 = quantile_with_method(&a, 0.125, None, QuantileMethod::ClosestObservation).unwrap();
860 assert!((q2.iter().next().copied().unwrap() - 1.0).abs() < 1e-12);
861 }
862
863 #[test]
864 fn test_quantile_closest_observation_nq_0_875_rounds_up() {
865 let a = arr_1_4();
868 let q = quantile_with_method(&a, 0.875, None, QuantileMethod::ClosestObservation).unwrap();
869 assert!((q.iter().next().copied().unwrap() - 4.0).abs() < 1e-12);
870 }
871
872 #[test]
873 fn test_quantile_continuous_methods_agree_at_q_0_and_q_1() {
874 let a = arr_1_5();
878 let methods = [
879 QuantileMethod::Linear,
880 QuantileMethod::Weibull,
881 QuantileMethod::Hazen,
882 QuantileMethod::InterpolatedInvertedCdf,
883 QuantileMethod::MedianUnbiased,
884 QuantileMethod::NormalUnbiased,
885 ];
886 for &m in &methods {
887 let q0 = quantile_with_method(&a, 0.0, None, m).unwrap();
888 let q1 = quantile_with_method(&a, 1.0, None, m).unwrap();
889 assert!(
890 (q0.iter().next().copied().unwrap() - 1.0).abs() < 1e-12,
891 "method {m:?} at q=0 should be min"
892 );
893 assert!(
894 (q1.iter().next().copied().unwrap() - 5.0).abs() < 1e-12,
895 "method {m:?} at q=1 should be max"
896 );
897 }
898 }
899
900 #[test]
901 fn test_quantile_discrete_methods_agree_at_q_1() {
902 let a = arr_1_5();
904 let methods = [
905 QuantileMethod::InvertedCdf,
906 QuantileMethod::AveragedInvertedCdf,
907 QuantileMethod::ClosestObservation,
908 ];
909 for &m in &methods {
910 let q = quantile_with_method(&a, 1.0, None, m).unwrap();
911 assert!(
912 (q.iter().next().copied().unwrap() - 5.0).abs() < 1e-12,
913 "method {m:?} at q=1 should be max"
914 );
915 }
916 }
917
918 #[test]
919 fn test_quantile_all_13_methods_at_integer_index_agree() {
920 let a = arr_1_5();
926 let all_methods = [
927 QuantileMethod::Linear,
928 QuantileMethod::Lower,
929 QuantileMethod::Higher,
930 QuantileMethod::Nearest,
931 QuantileMethod::Midpoint,
932 QuantileMethod::Weibull,
933 QuantileMethod::Hazen,
934 QuantileMethod::MedianUnbiased,
935 QuantileMethod::NormalUnbiased,
936 ];
939 for &m in &all_methods {
940 let r = quantile_with_method(&a, 0.5, None, m).unwrap();
941 assert!(
942 (r.iter().next().copied().unwrap() - 3.0).abs() < 1e-12,
943 "method {m:?} at odd n, q=0.5 should be 3.0"
944 );
945 }
946 }
947
948 #[test]
949 fn test_quantile_method_axis_variant_weibull() {
950 use ferray_core::Ix2;
951 let a = Array::<f64, Ix2>::from_vec(
954 Ix2::new([2, 4]),
955 vec![1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0],
956 )
957 .unwrap();
958 let r = quantile_with_method(&a, 0.5, Some(1), QuantileMethod::Weibull).unwrap();
959 assert_eq!(r.shape(), &[2]);
960 let s = r.as_slice().unwrap();
961 assert!((s[0] - 2.5).abs() < 1e-12);
962 assert!((s[1] - 25.0).abs() < 1e-12);
963 }
964}