1use ferray_core::error::{FerrayError, FerrayResult};
31use ferray_core::{Array, Dimension, Element, IxDyn};
32use num_traits::Float;
33
34use super::{collect_data, make_result, output_shape, reduce_axis_general, validate_axis};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum QuantileMethod {
51 Linear,
54 Lower,
57 Higher,
60 Nearest,
64 Midpoint,
66 InvertedCdf,
70 AveragedInvertedCdf,
75 ClosestObservation,
81 InterpolatedInvertedCdf,
84 Hazen,
87 Weibull,
90 MedianUnbiased,
93 NormalUnbiased,
96}
97
98#[inline]
104fn continuous_vidx<T: Float>(n: usize, q: T, alpha: T, beta: T) -> (usize, T) {
105 let nf = T::from(n).unwrap();
106 let zero = T::zero();
107 let one = T::one();
108 let n_minus_1 = T::from(n - 1).unwrap();
109
110 let vidx = nf * q + alpha + q * (one - alpha - beta) - one;
111
112 let vidx_clamped = if vidx < zero {
114 zero
115 } else if vidx > n_minus_1 {
116 n_minus_1
117 } else {
118 vidx
119 };
120
121 let lo = vidx_clamped.floor();
122 let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
123 let gamma = vidx_clamped - lo;
124 (lo_i, gamma)
125}
126
127#[allow(clippy::too_many_lines)]
137fn method_index_and_gamma<T: Float>(n: usize, q: T, method: QuantileMethod) -> (usize, T) {
138 let zero = T::zero();
139 let one = T::one();
140 let half = T::from(0.5).unwrap();
141 let nf = T::from(n).unwrap();
142
143 match method {
144 QuantileMethod::Linear => continuous_vidx(n, q, one, one),
146 QuantileMethod::Weibull => continuous_vidx(n, q, zero, zero),
147 QuantileMethod::Hazen => continuous_vidx(n, q, half, half),
148 QuantileMethod::InterpolatedInvertedCdf => continuous_vidx(n, q, zero, one),
149 QuantileMethod::MedianUnbiased => {
150 let third = T::from(1.0 / 3.0).unwrap();
151 continuous_vidx(n, q, third, third)
152 }
153 QuantileMethod::NormalUnbiased => {
154 let ae = T::from(3.0 / 8.0).unwrap();
155 continuous_vidx(n, q, ae, ae)
156 }
157
158 QuantileMethod::Lower => {
161 let vidx = q * T::from(n - 1).unwrap();
162 let lo_i = vidx.floor().to_usize().unwrap_or(0).min(n - 1);
163 (lo_i, zero)
164 }
165 QuantileMethod::Higher => {
166 let vidx = q * T::from(n - 1).unwrap();
167 let lo = vidx.floor();
168 let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
169 let frac = vidx - lo;
170 if frac > zero && lo_i + 1 < n {
173 (lo_i, one)
174 } else {
175 (lo_i, zero)
176 }
177 }
178 QuantileMethod::Nearest => {
179 let vidx = q * T::from(n - 1).unwrap();
180 let lo = vidx.floor();
181 let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
182 let frac = vidx - lo;
183 if frac < half {
184 (lo_i, zero)
185 } else if frac > half {
186 if lo_i + 1 < n {
187 (lo_i, one)
188 } else {
189 (lo_i, zero)
190 }
191 } else {
192 if lo_i.is_multiple_of(2) || lo_i + 1 >= n {
194 (lo_i, zero)
195 } else {
196 (lo_i, one)
197 }
198 }
199 }
200 QuantileMethod::Midpoint => {
201 let vidx = q * T::from(n - 1).unwrap();
202 let lo = vidx.floor();
203 let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
204 let frac = vidx - lo;
205 if frac > zero && lo_i + 1 < n {
206 (lo_i, half)
207 } else {
208 (lo_i, zero)
209 }
210 }
211
212 QuantileMethod::InvertedCdf => {
214 let nq = nf * q;
216 let k = if nq <= zero {
217 0
218 } else {
219 nq.ceil()
220 .to_usize()
221 .unwrap_or(0)
222 .saturating_sub(1)
223 .min(n - 1)
224 };
225 (k, zero)
226 }
227 QuantileMethod::AveragedInvertedCdf => {
228 let nq = nf * q;
231 let floor_nq = nq.floor();
232 let is_integer = nq == floor_nq;
233 if is_integer && nq > zero && nq < nf {
234 let k = floor_nq.to_usize().unwrap_or(0);
237 let lo_i = k.saturating_sub(1).min(n - 1);
238 if lo_i + 1 < n {
239 (lo_i, half)
240 } else {
241 (lo_i, zero)
242 }
243 } else {
244 let k = if nq <= zero {
246 0
247 } else {
248 nq.ceil()
249 .to_usize()
250 .unwrap_or(0)
251 .saturating_sub(1)
252 .min(n - 1)
253 };
254 (k, zero)
255 }
256 }
257 QuantileMethod::ClosestObservation => {
258 let one_and_half = one + half;
269 let idx = nf * q - one_and_half;
270 let previous = idx.floor();
271 let gamma = idx - previous;
272 let previous_i = previous.to_i64().unwrap_or(0);
275 let chosen = if gamma == zero && previous_i.rem_euclid(2) == 1 {
279 previous_i
280 } else {
281 previous_i + 1
282 };
283 let k = if chosen < 0 {
286 0
287 } else {
288 usize::try_from(chosen).unwrap_or(0).min(n - 1)
289 };
290 (k, zero)
291 }
292 }
293}
294
295fn quantile_select<T: Float>(mut data: Vec<T>, q: T, method: QuantileMethod) -> T {
307 let n = data.len();
308 if n == 0 {
309 return T::nan();
310 }
311 if n == 1 {
312 return data[0];
313 }
314
315 let cmp = |a: &T, b: &T| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
316 let (lo_i, gamma) = method_index_and_gamma(n, q, method);
317
318 data.select_nth_unstable_by(lo_i, cmp);
320 let lo_val = data[lo_i];
321
322 if gamma == T::zero() || lo_i >= n - 1 {
325 return lo_val;
326 }
327
328 let hi_val = data[lo_i + 1..]
333 .iter()
334 .copied()
335 .reduce(|a, b| match cmp(&a, &b) {
336 std::cmp::Ordering::Less | std::cmp::Ordering::Equal => a,
337 std::cmp::Ordering::Greater => b,
338 })
339 .unwrap_or(lo_val);
340
341 (T::one() - gamma) * lo_val + gamma * hi_val
342}
343
344fn lane_quantile_with_method<T: Float>(lane: &[T], q: T, method: QuantileMethod) -> T {
346 quantile_select(lane.to_vec(), q, method)
347}
348
349fn lane_nanquantile<T: Float>(lane: &[T], q: T) -> T {
351 let filtered: Vec<T> = lane.iter().copied().filter(|x| !x.is_nan()).collect();
352 if filtered.is_empty() {
353 return T::nan();
354 }
355 quantile_select(filtered, q, QuantileMethod::Linear)
356}
357
358pub fn quantile<T, D>(a: &Array<T, D>, q: T, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
368where
369 T: Element + Float,
370 D: Dimension,
371{
372 quantile_with_method(a, q, axis, QuantileMethod::Linear)
373}
374
375pub fn quantile_with_method<T, D>(
387 a: &Array<T, D>,
388 q: T,
389 axis: Option<usize>,
390 method: QuantileMethod,
391) -> FerrayResult<Array<T, IxDyn>>
392where
393 T: Element + Float,
394 D: Dimension,
395{
396 if q < <T as Element>::zero() || q > <T as Element>::one() {
397 return Err(FerrayError::invalid_value("quantile q must be in [0, 1]"));
398 }
399 if a.is_empty() {
400 return Err(FerrayError::invalid_value(
401 "cannot compute quantile of empty array",
402 ));
403 }
404 let data = collect_data(a);
405 match axis {
406 None => {
407 let val = lane_quantile_with_method(&data, q, method);
408 make_result(&[], vec![val])
409 }
410 Some(ax) => {
411 validate_axis(ax, a.ndim())?;
412 let shape = a.shape();
413 let out_s = output_shape(shape, ax);
414 let result = reduce_axis_general(&data, shape, ax, |lane| {
415 lane_quantile_with_method(lane, q, method)
416 });
417 make_result(&out_s, result)
418 }
419 }
420}
421
422pub fn percentile<T, D>(a: &Array<T, D>, q: T, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
434where
435 T: Element + Float,
436 D: Dimension,
437{
438 percentile_with_method(a, q, axis, QuantileMethod::Linear)
439}
440
441pub fn percentile_with_method<T, D>(
448 a: &Array<T, D>,
449 q: T,
450 axis: Option<usize>,
451 method: QuantileMethod,
452) -> FerrayResult<Array<T, IxDyn>>
453where
454 T: Element + Float,
455 D: Dimension,
456{
457 let hundred = T::from(100.0).unwrap();
458 if q < <T as Element>::zero() || q > hundred {
459 return Err(FerrayError::invalid_value(
460 "percentile q must be in [0, 100]",
461 ));
462 }
463 quantile_with_method(a, q / hundred, axis, method)
464}
465
466pub fn median<T, D>(a: &Array<T, D>, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
474where
475 T: Element + Float,
476 D: Dimension,
477{
478 let half = T::from(0.5).unwrap();
479 quantile(a, half, axis)
480}
481
482pub fn nanmedian<T, D>(a: &Array<T, D>, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
490where
491 T: Element + Float,
492 D: Dimension,
493{
494 let half = T::from(0.5).unwrap();
495 nanquantile(a, half, axis)
496}
497
498pub fn nanpercentile<T, D>(
502 a: &Array<T, D>,
503 q: T,
504 axis: Option<usize>,
505) -> FerrayResult<Array<T, IxDyn>>
506where
507 T: Element + Float,
508 D: Dimension,
509{
510 let hundred = T::from(100.0).unwrap();
511 if q < <T as Element>::zero() || q > hundred {
512 return Err(FerrayError::invalid_value(
513 "nanpercentile q must be in [0, 100]",
514 ));
515 }
516 nanquantile(a, q / hundred, axis)
517}
518
519pub fn nanquantile<T, D>(
523 a: &Array<T, D>,
524 q: T,
525 axis: Option<usize>,
526) -> FerrayResult<Array<T, IxDyn>>
527where
528 T: Element + Float,
529 D: Dimension,
530{
531 if q < <T as Element>::zero() || q > <T as Element>::one() {
532 return Err(FerrayError::invalid_value("quantile q must be in [0, 1]"));
533 }
534 if a.is_empty() {
535 return Err(FerrayError::invalid_value(
536 "cannot compute nanquantile of empty array",
537 ));
538 }
539 let data = collect_data(a);
540 match axis {
541 None => {
542 let val = lane_nanquantile(&data, q);
543 make_result(&[], vec![val])
544 }
545 Some(ax) => {
546 validate_axis(ax, a.ndim())?;
547 let shape = a.shape();
548 let out_s = output_shape(shape, ax);
549 let result = reduce_axis_general(&data, shape, ax, |lane| lane_nanquantile(lane, q));
550 make_result(&out_s, result)
551 }
552 }
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558 use ferray_core::Ix1;
559
560 #[test]
561 fn test_median_odd() {
562 let a = Array::<f64, Ix1>::from_vec(Ix1::new([5]), vec![5.0, 1.0, 3.0, 2.0, 4.0]).unwrap();
563 let m = median(&a, None).unwrap();
564 assert!((m.iter().next().unwrap() - 3.0).abs() < 1e-12);
565 }
566
567 #[test]
568 fn test_median_even() {
569 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![4.0, 1.0, 3.0, 2.0]).unwrap();
570 let m = median(&a, None).unwrap();
571 assert!((m.iter().next().unwrap() - 2.5).abs() < 1e-12);
572 }
573
574 #[test]
575 fn test_percentile_0_50_100() {
576 let a = Array::<f64, Ix1>::from_vec(Ix1::new([5]), vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
577 let p0 = percentile(&a, 0.0, None).unwrap();
578 let p50 = percentile(&a, 50.0, None).unwrap();
579 let p100 = percentile(&a, 100.0, None).unwrap();
580 assert!((p0.iter().next().unwrap() - 1.0).abs() < 1e-12);
581 assert!((p50.iter().next().unwrap() - 3.0).abs() < 1e-12);
582 assert!((p100.iter().next().unwrap() - 5.0).abs() < 1e-12);
583 }
584
585 #[test]
586 fn test_quantile_bounds() {
587 let a = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
588 assert!(quantile(&a, -0.1, None).is_err());
589 assert!(quantile(&a, 1.1, None).is_err());
590 }
591
592 #[test]
593 fn test_quantile_interpolation() {
594 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
595 let q = quantile(&a, 0.25, None).unwrap();
596 assert!((q.iter().next().unwrap() - 1.75).abs() < 1e-12);
598 }
599
600 #[test]
601 fn test_nanmedian() {
602 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, f64::NAN, 3.0, 5.0]).unwrap();
603 let m = nanmedian(&a, None).unwrap();
604 assert!((m.iter().next().unwrap() - 3.0).abs() < 1e-12);
606 }
607
608 #[test]
609 fn test_nanpercentile() {
610 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, f64::NAN, 3.0, 5.0]).unwrap();
611 let p = nanpercentile(&a, 50.0, None).unwrap();
612 assert!((p.iter().next().unwrap() - 3.0).abs() < 1e-12);
613 }
614
615 #[test]
616 fn test_nanmedian_all_nan() {
617 let a = Array::<f64, Ix1>::from_vec(Ix1::new([2]), vec![f64::NAN, f64::NAN]).unwrap();
618 let m = nanmedian(&a, None).unwrap();
619 assert!(m.iter().next().unwrap().is_nan());
620 }
621
622 fn arr_1_5() -> Array<f64, Ix1> {
625 Array::<f64, Ix1>::from_vec(Ix1::new([5]), vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap()
626 }
627
628 #[test]
629 fn test_quantile_method_linear_matches_legacy() {
630 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
632 let legacy = quantile(&a, 0.25, None).unwrap();
633 let with_flag = quantile_with_method(&a, 0.25, None, QuantileMethod::Linear).unwrap();
634 assert_eq!(
635 legacy.iter().next().unwrap(),
636 with_flag.iter().next().unwrap()
637 );
638 }
639
640 #[test]
641 fn test_quantile_method_lower() {
642 let a = arr_1_5();
645 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Lower).unwrap();
646 assert!((q.iter().next().unwrap() - 2.0).abs() < 1e-12);
647
648 let a4 = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
650 let q = quantile_with_method(&a4, 0.25, None, QuantileMethod::Lower).unwrap();
651 assert!((q.iter().next().unwrap() - 1.0).abs() < 1e-12);
652 }
653
654 #[test]
655 fn test_quantile_method_higher() {
656 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
658 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Higher).unwrap();
659 assert!((q.iter().next().unwrap() - 2.0).abs() < 1e-12);
660 }
661
662 #[test]
663 fn test_quantile_method_nearest_round_down() {
664 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
666 let q = quantile_with_method(&a, 0.2, None, QuantileMethod::Nearest).unwrap();
667 assert!((q.iter().next().unwrap() - 2.0).abs() < 1e-12);
668
669 let q2 = quantile_with_method(&a, 0.1, None, QuantileMethod::Nearest).unwrap();
671 assert!((q2.iter().next().unwrap() - 1.0).abs() < 1e-12);
672 }
673
674 #[test]
675 fn test_quantile_method_nearest_tie_even() {
676 let a = arr_1_5();
678 let q = quantile_with_method(&a, 0.125, None, QuantileMethod::Nearest).unwrap();
679 assert!((q.iter().next().unwrap() - 1.0).abs() < 1e-12);
680
681 let q2 = quantile_with_method(&a, 0.375, None, QuantileMethod::Nearest).unwrap();
683 assert!((q2.iter().next().unwrap() - 3.0).abs() < 1e-12);
684 }
685
686 #[test]
687 fn test_quantile_method_midpoint() {
688 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
690 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Midpoint).unwrap();
691 assert!((q.iter().next().unwrap() - 1.5).abs() < 1e-12);
692
693 let q2 = quantile_with_method(&a, 0.75, None, QuantileMethod::Midpoint).unwrap();
695 assert!((q2.iter().next().unwrap() - 3.5).abs() < 1e-12);
696 }
697
698 #[test]
699 fn test_quantile_method_integer_index_all_agree() {
700 let a = arr_1_5();
703 let linear = quantile_with_method(&a, 0.5, None, QuantileMethod::Linear).unwrap();
704 let lower = quantile_with_method(&a, 0.5, None, QuantileMethod::Lower).unwrap();
705 let higher = quantile_with_method(&a, 0.5, None, QuantileMethod::Higher).unwrap();
706 let nearest = quantile_with_method(&a, 0.5, None, QuantileMethod::Nearest).unwrap();
707 let midpoint = quantile_with_method(&a, 0.5, None, QuantileMethod::Midpoint).unwrap();
708 let expected = 3.0;
709 assert!((linear.iter().next().unwrap() - expected).abs() < 1e-12);
710 assert!((lower.iter().next().unwrap() - expected).abs() < 1e-12);
711 assert!((higher.iter().next().unwrap() - expected).abs() < 1e-12);
712 assert!((nearest.iter().next().unwrap() - expected).abs() < 1e-12);
713 assert!((midpoint.iter().next().unwrap() - expected).abs() < 1e-12);
714 }
715
716 #[test]
717 fn test_quantile_method_axis_variant() {
718 use ferray_core::Ix2;
720 let a = Array::<f64, Ix2>::from_vec(
721 Ix2::new([2, 4]),
722 vec![1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0],
723 )
724 .unwrap();
725 let r = quantile_with_method(&a, 0.25, Some(1), QuantileMethod::Lower).unwrap();
727 assert_eq!(r.as_slice().unwrap(), &[1.0, 10.0]);
728 }
729
730 #[test]
731 fn test_percentile_with_method_50() {
732 let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
733 let lin = percentile_with_method(&a, 50.0, None, QuantileMethod::Linear).unwrap();
736 let lo = percentile_with_method(&a, 50.0, None, QuantileMethod::Lower).unwrap();
737 let hi = percentile_with_method(&a, 50.0, None, QuantileMethod::Higher).unwrap();
738 let nr = percentile_with_method(&a, 50.0, None, QuantileMethod::Nearest).unwrap();
739 let mp = percentile_with_method(&a, 50.0, None, QuantileMethod::Midpoint).unwrap();
740 assert!((lin.iter().next().unwrap() - 2.5).abs() < 1e-12);
741 assert!((lo.iter().next().unwrap() - 2.0).abs() < 1e-12);
742 assert!((hi.iter().next().unwrap() - 3.0).abs() < 1e-12);
743 assert!((nr.iter().next().unwrap() - 3.0).abs() < 1e-12);
744 assert!((mp.iter().next().unwrap() - 2.5).abs() < 1e-12);
745 }
746
747 #[test]
748 fn test_percentile_with_method_rejects_out_of_range() {
749 let a = arr_1_5();
750 assert!(percentile_with_method(&a, -1.0, None, QuantileMethod::Linear).is_err());
751 assert!(percentile_with_method(&a, 101.0, None, QuantileMethod::Linear).is_err());
752 }
753
754 fn arr_1_4() -> Array<f64, Ix1> {
763 Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap()
764 }
765
766 #[test]
767 fn test_quantile_weibull_q_half() {
768 let a = arr_1_4();
772 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::Weibull).unwrap();
773 assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
774 }
775
776 #[test]
777 fn test_quantile_weibull_q_quarter() {
778 let a = arr_1_4();
782 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Weibull).unwrap();
783 assert!((q.iter().next().copied().unwrap() - 1.25).abs() < 1e-12);
784 }
785
786 #[test]
787 fn test_quantile_hazen_q_quarter() {
788 let a = arr_1_4();
792 let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Hazen).unwrap();
793 assert!((q.iter().next().copied().unwrap() - 1.5).abs() < 1e-12);
794 }
795
796 #[test]
797 fn test_quantile_median_unbiased_q_half() {
798 let a = arr_1_4();
805 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::MedianUnbiased).unwrap();
806 assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
807 }
808
809 #[test]
810 fn test_quantile_normal_unbiased_q_half() {
811 let a = arr_1_4();
818 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::NormalUnbiased).unwrap();
819 assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
820 }
821
822 #[test]
823 fn test_quantile_interpolated_inverted_cdf_q_half() {
824 let a = arr_1_4();
830 let q =
831 quantile_with_method(&a, 0.5, None, QuantileMethod::InterpolatedInvertedCdf).unwrap();
832 assert!((q.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
833 }
834
835 #[test]
836 fn test_quantile_inverted_cdf_q_half() {
837 let a = arr_1_4();
839 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::InvertedCdf).unwrap();
840 assert!((q.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
841 }
842
843 #[test]
844 fn test_quantile_inverted_cdf_step_function() {
845 let a = arr_1_5();
849 let q1 = quantile_with_method(&a, 0.19, None, QuantileMethod::InvertedCdf).unwrap();
850 assert!((q1.iter().next().copied().unwrap() - 1.0).abs() < 1e-12);
851 let q2 = quantile_with_method(&a, 0.21, None, QuantileMethod::InvertedCdf).unwrap();
852 assert!((q2.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
853 }
854
855 #[test]
856 fn test_quantile_averaged_inverted_cdf_integer_nq_averages() {
857 let a = arr_1_4();
860 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::AveragedInvertedCdf).unwrap();
861 assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
862 }
863
864 #[test]
865 fn test_quantile_averaged_inverted_cdf_non_integer_nq_matches_inverted_cdf() {
866 let a = arr_1_5();
869 let q1 = quantile_with_method(&a, 0.3, None, QuantileMethod::AveragedInvertedCdf).unwrap();
870 let q2 = quantile_with_method(&a, 0.3, None, QuantileMethod::InvertedCdf).unwrap();
871 assert_eq!(
872 q1.iter().next().copied().unwrap(),
873 q2.iter().next().copied().unwrap()
874 );
875 assert!((q1.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
876 }
877
878 #[test]
879 fn test_quantile_closest_observation_half_to_even() {
880 let a = arr_1_4();
884 let q = quantile_with_method(&a, 0.5, None, QuantileMethod::ClosestObservation).unwrap();
885 assert!((q.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
886
887 let q2 = quantile_with_method(&a, 0.125, None, QuantileMethod::ClosestObservation).unwrap();
890 assert!((q2.iter().next().copied().unwrap() - 1.0).abs() < 1e-12);
891 }
892
893 #[test]
894 fn test_quantile_closest_observation_nq_0_875_rounds_up() {
895 let a = arr_1_4();
898 let q = quantile_with_method(&a, 0.875, None, QuantileMethod::ClosestObservation).unwrap();
899 assert!((q.iter().next().copied().unwrap() - 4.0).abs() < 1e-12);
900 }
901
902 #[test]
903 fn test_quantile_continuous_methods_agree_at_q_0_and_q_1() {
904 let a = arr_1_5();
908 let methods = [
909 QuantileMethod::Linear,
910 QuantileMethod::Weibull,
911 QuantileMethod::Hazen,
912 QuantileMethod::InterpolatedInvertedCdf,
913 QuantileMethod::MedianUnbiased,
914 QuantileMethod::NormalUnbiased,
915 ];
916 for &m in &methods {
917 let q0 = quantile_with_method(&a, 0.0, None, m).unwrap();
918 let q1 = quantile_with_method(&a, 1.0, None, m).unwrap();
919 assert!(
920 (q0.iter().next().copied().unwrap() - 1.0).abs() < 1e-12,
921 "method {m:?} at q=0 should be min"
922 );
923 assert!(
924 (q1.iter().next().copied().unwrap() - 5.0).abs() < 1e-12,
925 "method {m:?} at q=1 should be max"
926 );
927 }
928 }
929
930 #[test]
931 fn test_quantile_discrete_methods_agree_at_q_1() {
932 let a = arr_1_5();
934 let methods = [
935 QuantileMethod::InvertedCdf,
936 QuantileMethod::AveragedInvertedCdf,
937 QuantileMethod::ClosestObservation,
938 ];
939 for &m in &methods {
940 let q = quantile_with_method(&a, 1.0, None, m).unwrap();
941 assert!(
942 (q.iter().next().copied().unwrap() - 5.0).abs() < 1e-12,
943 "method {m:?} at q=1 should be max"
944 );
945 }
946 }
947
948 #[test]
949 fn test_quantile_all_13_methods_at_integer_index_agree() {
950 let a = arr_1_5();
956 let all_methods = [
957 QuantileMethod::Linear,
958 QuantileMethod::Lower,
959 QuantileMethod::Higher,
960 QuantileMethod::Nearest,
961 QuantileMethod::Midpoint,
962 QuantileMethod::Weibull,
963 QuantileMethod::Hazen,
964 QuantileMethod::MedianUnbiased,
965 QuantileMethod::NormalUnbiased,
966 ];
969 for &m in &all_methods {
970 let r = quantile_with_method(&a, 0.5, None, m).unwrap();
971 assert!(
972 (r.iter().next().copied().unwrap() - 3.0).abs() < 1e-12,
973 "method {m:?} at odd n, q=0.5 should be 3.0"
974 );
975 }
976 }
977
978 #[test]
979 fn test_quantile_method_axis_variant_weibull() {
980 use ferray_core::Ix2;
981 let a = Array::<f64, Ix2>::from_vec(
984 Ix2::new([2, 4]),
985 vec![1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0],
986 )
987 .unwrap();
988 let r = quantile_with_method(&a, 0.5, Some(1), QuantileMethod::Weibull).unwrap();
989 assert_eq!(r.shape(), &[2]);
990 let s = r.as_slice().unwrap();
991 assert!((s[0] - 2.5).abs() < 1e-12);
992 assert!((s[1] - 25.0).abs() < 1e-12);
993 }
994}