use ferray_core::error::{FerrayError, FerrayResult};
use ferray_core::{Array, Dimension, Element, IxDyn};
use num_traits::Float;
use super::{collect_data, make_result, output_shape, reduce_axis_general, validate_axis};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuantileMethod {
Linear,
Lower,
Higher,
Nearest,
Midpoint,
InvertedCdf,
AveragedInvertedCdf,
ClosestObservation,
InterpolatedInvertedCdf,
Hazen,
Weibull,
MedianUnbiased,
NormalUnbiased,
}
#[inline]
fn continuous_vidx<T: Float>(n: usize, q: T, alpha: T, beta: T) -> (usize, T) {
let nf = T::from(n).unwrap();
let zero = T::zero();
let one = T::one();
let n_minus_1 = T::from(n - 1).unwrap();
let vidx = nf * q + alpha + q * (one - alpha - beta) - one;
let vidx_clamped = if vidx < zero {
zero
} else if vidx > n_minus_1 {
n_minus_1
} else {
vidx
};
let lo = vidx_clamped.floor();
let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
let gamma = vidx_clamped - lo;
(lo_i, gamma)
}
#[inline]
fn round_half_to_even<T: Float>(x: T) -> T {
let floor = x.floor();
let frac = x - floor;
let half = T::from(0.5).unwrap();
if frac < half {
floor
} else if frac > half {
floor + T::one()
} else {
let floor_i = floor.to_i64().unwrap_or(0);
if floor_i.rem_euclid(2) == 0 {
floor
} else {
floor + T::one()
}
}
}
#[allow(clippy::too_many_lines)]
fn method_index_and_gamma<T: Float>(n: usize, q: T, method: QuantileMethod) -> (usize, T) {
let zero = T::zero();
let one = T::one();
let half = T::from(0.5).unwrap();
let nf = T::from(n).unwrap();
match method {
QuantileMethod::Linear => continuous_vidx(n, q, one, one),
QuantileMethod::Weibull => continuous_vidx(n, q, zero, zero),
QuantileMethod::Hazen => continuous_vidx(n, q, half, half),
QuantileMethod::InterpolatedInvertedCdf => continuous_vidx(n, q, zero, one),
QuantileMethod::MedianUnbiased => {
let third = T::from(1.0 / 3.0).unwrap();
continuous_vidx(n, q, third, third)
}
QuantileMethod::NormalUnbiased => {
let ae = T::from(3.0 / 8.0).unwrap();
continuous_vidx(n, q, ae, ae)
}
QuantileMethod::Lower => {
let vidx = q * T::from(n - 1).unwrap();
let lo_i = vidx.floor().to_usize().unwrap_or(0).min(n - 1);
(lo_i, zero)
}
QuantileMethod::Higher => {
let vidx = q * T::from(n - 1).unwrap();
let lo = vidx.floor();
let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
let frac = vidx - lo;
if frac > zero && lo_i + 1 < n {
(lo_i, one)
} else {
(lo_i, zero)
}
}
QuantileMethod::Nearest => {
let vidx = q * T::from(n - 1).unwrap();
let lo = vidx.floor();
let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
let frac = vidx - lo;
if frac < half {
(lo_i, zero)
} else if frac > half {
if lo_i + 1 < n {
(lo_i, one)
} else {
(lo_i, zero)
}
} else {
if lo_i % 2 == 0 || lo_i + 1 >= n {
(lo_i, zero)
} else {
(lo_i, one)
}
}
}
QuantileMethod::Midpoint => {
let vidx = q * T::from(n - 1).unwrap();
let lo = vidx.floor();
let lo_i = lo.to_usize().unwrap_or(0).min(n - 1);
let frac = vidx - lo;
if frac > zero && lo_i + 1 < n {
(lo_i, half)
} else {
(lo_i, zero)
}
}
QuantileMethod::InvertedCdf => {
let nq = nf * q;
let k = if nq <= zero {
0
} else {
nq.ceil()
.to_usize()
.unwrap_or(0)
.saturating_sub(1)
.min(n - 1)
};
(k, zero)
}
QuantileMethod::AveragedInvertedCdf => {
let nq = nf * q;
let floor_nq = nq.floor();
let is_integer = nq == floor_nq;
if is_integer && nq > zero && nq < nf {
let k = floor_nq.to_usize().unwrap_or(0);
let lo_i = k.saturating_sub(1).min(n - 1);
if lo_i + 1 < n {
(lo_i, half)
} else {
(lo_i, zero)
}
} else {
let k = if nq <= zero {
0
} else {
nq.ceil()
.to_usize()
.unwrap_or(0)
.saturating_sub(1)
.min(n - 1)
};
(k, zero)
}
}
QuantileMethod::ClosestObservation => {
let nq = nf * q;
let adj = nq - half;
let rounded = round_half_to_even(adj);
let k = if rounded < zero {
0
} else {
rounded.to_usize().unwrap_or(0).min(n - 1)
};
(k, zero)
}
}
}
fn quantile_select<T: Float>(mut data: Vec<T>, q: T, method: QuantileMethod) -> T {
let n = data.len();
if n == 0 {
return T::nan();
}
if n == 1 {
return data[0];
}
let cmp = |a: &T, b: &T| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
let (lo_i, gamma) = method_index_and_gamma(n, q, method);
data.select_nth_unstable_by(lo_i, cmp);
let lo_val = data[lo_i];
if gamma == T::zero() || lo_i >= n - 1 {
return lo_val;
}
let hi_val = data[lo_i + 1..]
.iter()
.copied()
.reduce(|a, b| match cmp(&a, &b) {
std::cmp::Ordering::Less | std::cmp::Ordering::Equal => a,
std::cmp::Ordering::Greater => b,
})
.unwrap_or(lo_val);
(T::one() - gamma) * lo_val + gamma * hi_val
}
fn lane_quantile_with_method<T: Float>(lane: &[T], q: T, method: QuantileMethod) -> T {
quantile_select(lane.to_vec(), q, method)
}
fn lane_nanquantile<T: Float>(lane: &[T], q: T) -> T {
let filtered: Vec<T> = lane.iter().copied().filter(|x| !x.is_nan()).collect();
if filtered.is_empty() {
return T::nan();
}
quantile_select(filtered, q, QuantileMethod::Linear)
}
pub fn quantile<T, D>(a: &Array<T, D>, q: T, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
where
T: Element + Float,
D: Dimension,
{
quantile_with_method(a, q, axis, QuantileMethod::Linear)
}
pub fn quantile_with_method<T, D>(
a: &Array<T, D>,
q: T,
axis: Option<usize>,
method: QuantileMethod,
) -> FerrayResult<Array<T, IxDyn>>
where
T: Element + Float,
D: Dimension,
{
if q < <T as Element>::zero() || q > <T as Element>::one() {
return Err(FerrayError::invalid_value("quantile q must be in [0, 1]"));
}
if a.is_empty() {
return Err(FerrayError::invalid_value(
"cannot compute quantile of empty array",
));
}
let data = collect_data(a);
match axis {
None => {
let val = lane_quantile_with_method(&data, q, method);
make_result(&[], vec![val])
}
Some(ax) => {
validate_axis(ax, a.ndim())?;
let shape = a.shape();
let out_s = output_shape(shape, ax);
let result = reduce_axis_general(&data, shape, ax, |lane| {
lane_quantile_with_method(lane, q, method)
});
make_result(&out_s, result)
}
}
}
pub fn percentile<T, D>(a: &Array<T, D>, q: T, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
where
T: Element + Float,
D: Dimension,
{
percentile_with_method(a, q, axis, QuantileMethod::Linear)
}
pub fn percentile_with_method<T, D>(
a: &Array<T, D>,
q: T,
axis: Option<usize>,
method: QuantileMethod,
) -> FerrayResult<Array<T, IxDyn>>
where
T: Element + Float,
D: Dimension,
{
let hundred = T::from(100.0).unwrap();
if q < <T as Element>::zero() || q > hundred {
return Err(FerrayError::invalid_value(
"percentile q must be in [0, 100]",
));
}
quantile_with_method(a, q / hundred, axis, method)
}
pub fn median<T, D>(a: &Array<T, D>, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
where
T: Element + Float,
D: Dimension,
{
let half = T::from(0.5).unwrap();
quantile(a, half, axis)
}
pub fn nanmedian<T, D>(a: &Array<T, D>, axis: Option<usize>) -> FerrayResult<Array<T, IxDyn>>
where
T: Element + Float,
D: Dimension,
{
let half = T::from(0.5).unwrap();
nanquantile(a, half, axis)
}
pub fn nanpercentile<T, D>(
a: &Array<T, D>,
q: T,
axis: Option<usize>,
) -> FerrayResult<Array<T, IxDyn>>
where
T: Element + Float,
D: Dimension,
{
let hundred = T::from(100.0).unwrap();
if q < <T as Element>::zero() || q > hundred {
return Err(FerrayError::invalid_value(
"nanpercentile q must be in [0, 100]",
));
}
nanquantile(a, q / hundred, axis)
}
pub fn nanquantile<T, D>(
a: &Array<T, D>,
q: T,
axis: Option<usize>,
) -> FerrayResult<Array<T, IxDyn>>
where
T: Element + Float,
D: Dimension,
{
if q < <T as Element>::zero() || q > <T as Element>::one() {
return Err(FerrayError::invalid_value("quantile q must be in [0, 1]"));
}
if a.is_empty() {
return Err(FerrayError::invalid_value(
"cannot compute nanquantile of empty array",
));
}
let data = collect_data(a);
match axis {
None => {
let val = lane_nanquantile(&data, q);
make_result(&[], vec![val])
}
Some(ax) => {
validate_axis(ax, a.ndim())?;
let shape = a.shape();
let out_s = output_shape(shape, ax);
let result = reduce_axis_general(&data, shape, ax, |lane| lane_nanquantile(lane, q));
make_result(&out_s, result)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ferray_core::Ix1;
#[test]
fn test_median_odd() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([5]), vec![5.0, 1.0, 3.0, 2.0, 4.0]).unwrap();
let m = median(&a, None).unwrap();
assert!((m.iter().next().unwrap() - 3.0).abs() < 1e-12);
}
#[test]
fn test_median_even() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![4.0, 1.0, 3.0, 2.0]).unwrap();
let m = median(&a, None).unwrap();
assert!((m.iter().next().unwrap() - 2.5).abs() < 1e-12);
}
#[test]
fn test_percentile_0_50_100() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([5]), vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
let p0 = percentile(&a, 0.0, None).unwrap();
let p50 = percentile(&a, 50.0, None).unwrap();
let p100 = percentile(&a, 100.0, None).unwrap();
assert!((p0.iter().next().unwrap() - 1.0).abs() < 1e-12);
assert!((p50.iter().next().unwrap() - 3.0).abs() < 1e-12);
assert!((p100.iter().next().unwrap() - 5.0).abs() < 1e-12);
}
#[test]
fn test_quantile_bounds() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
assert!(quantile(&a, -0.1, None).is_err());
assert!(quantile(&a, 1.1, None).is_err());
}
#[test]
fn test_quantile_interpolation() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
let q = quantile(&a, 0.25, None).unwrap();
assert!((q.iter().next().unwrap() - 1.75).abs() < 1e-12);
}
#[test]
fn test_nanmedian() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, f64::NAN, 3.0, 5.0]).unwrap();
let m = nanmedian(&a, None).unwrap();
assert!((m.iter().next().unwrap() - 3.0).abs() < 1e-12);
}
#[test]
fn test_nanpercentile() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, f64::NAN, 3.0, 5.0]).unwrap();
let p = nanpercentile(&a, 50.0, None).unwrap();
assert!((p.iter().next().unwrap() - 3.0).abs() < 1e-12);
}
#[test]
fn test_nanmedian_all_nan() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([2]), vec![f64::NAN, f64::NAN]).unwrap();
let m = nanmedian(&a, None).unwrap();
assert!(m.iter().next().unwrap().is_nan());
}
fn arr_1_5() -> Array<f64, Ix1> {
Array::<f64, Ix1>::from_vec(Ix1::new([5]), vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap()
}
#[test]
fn test_quantile_method_linear_matches_legacy() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
let legacy = quantile(&a, 0.25, None).unwrap();
let with_flag = quantile_with_method(&a, 0.25, None, QuantileMethod::Linear).unwrap();
assert_eq!(
legacy.iter().next().unwrap(),
with_flag.iter().next().unwrap()
);
}
#[test]
fn test_quantile_method_lower() {
let a = arr_1_5();
let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Lower).unwrap();
assert!((q.iter().next().unwrap() - 2.0).abs() < 1e-12);
let a4 = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
let q = quantile_with_method(&a4, 0.25, None, QuantileMethod::Lower).unwrap();
assert!((q.iter().next().unwrap() - 1.0).abs() < 1e-12);
}
#[test]
fn test_quantile_method_higher() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Higher).unwrap();
assert!((q.iter().next().unwrap() - 2.0).abs() < 1e-12);
}
#[test]
fn test_quantile_method_nearest_round_down() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
let q = quantile_with_method(&a, 0.2, None, QuantileMethod::Nearest).unwrap();
assert!((q.iter().next().unwrap() - 2.0).abs() < 1e-12);
let q2 = quantile_with_method(&a, 0.1, None, QuantileMethod::Nearest).unwrap();
assert!((q2.iter().next().unwrap() - 1.0).abs() < 1e-12);
}
#[test]
fn test_quantile_method_nearest_tie_even() {
let a = arr_1_5();
let q = quantile_with_method(&a, 0.125, None, QuantileMethod::Nearest).unwrap();
assert!((q.iter().next().unwrap() - 1.0).abs() < 1e-12);
let q2 = quantile_with_method(&a, 0.375, None, QuantileMethod::Nearest).unwrap();
assert!((q2.iter().next().unwrap() - 3.0).abs() < 1e-12);
}
#[test]
fn test_quantile_method_midpoint() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Midpoint).unwrap();
assert!((q.iter().next().unwrap() - 1.5).abs() < 1e-12);
let q2 = quantile_with_method(&a, 0.75, None, QuantileMethod::Midpoint).unwrap();
assert!((q2.iter().next().unwrap() - 3.5).abs() < 1e-12);
}
#[test]
fn test_quantile_method_integer_index_all_agree() {
let a = arr_1_5();
let linear = quantile_with_method(&a, 0.5, None, QuantileMethod::Linear).unwrap();
let lower = quantile_with_method(&a, 0.5, None, QuantileMethod::Lower).unwrap();
let higher = quantile_with_method(&a, 0.5, None, QuantileMethod::Higher).unwrap();
let nearest = quantile_with_method(&a, 0.5, None, QuantileMethod::Nearest).unwrap();
let midpoint = quantile_with_method(&a, 0.5, None, QuantileMethod::Midpoint).unwrap();
let expected = 3.0;
assert!((linear.iter().next().unwrap() - expected).abs() < 1e-12);
assert!((lower.iter().next().unwrap() - expected).abs() < 1e-12);
assert!((higher.iter().next().unwrap() - expected).abs() < 1e-12);
assert!((nearest.iter().next().unwrap() - expected).abs() < 1e-12);
assert!((midpoint.iter().next().unwrap() - expected).abs() < 1e-12);
}
#[test]
fn test_quantile_method_axis_variant() {
use ferray_core::Ix2;
let a = Array::<f64, Ix2>::from_vec(
Ix2::new([2, 4]),
vec![1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0],
)
.unwrap();
let r = quantile_with_method(&a, 0.25, Some(1), QuantileMethod::Lower).unwrap();
assert_eq!(r.as_slice().unwrap(), &[1.0, 10.0]);
}
#[test]
fn test_percentile_with_method_50() {
let a = Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
let lin = percentile_with_method(&a, 50.0, None, QuantileMethod::Linear).unwrap();
let lo = percentile_with_method(&a, 50.0, None, QuantileMethod::Lower).unwrap();
let hi = percentile_with_method(&a, 50.0, None, QuantileMethod::Higher).unwrap();
let nr = percentile_with_method(&a, 50.0, None, QuantileMethod::Nearest).unwrap();
let mp = percentile_with_method(&a, 50.0, None, QuantileMethod::Midpoint).unwrap();
assert!((lin.iter().next().unwrap() - 2.5).abs() < 1e-12);
assert!((lo.iter().next().unwrap() - 2.0).abs() < 1e-12);
assert!((hi.iter().next().unwrap() - 3.0).abs() < 1e-12);
assert!((nr.iter().next().unwrap() - 3.0).abs() < 1e-12);
assert!((mp.iter().next().unwrap() - 2.5).abs() < 1e-12);
}
#[test]
fn test_percentile_with_method_rejects_out_of_range() {
let a = arr_1_5();
assert!(percentile_with_method(&a, -1.0, None, QuantileMethod::Linear).is_err());
assert!(percentile_with_method(&a, 101.0, None, QuantileMethod::Linear).is_err());
}
fn arr_1_4() -> Array<f64, Ix1> {
Array::<f64, Ix1>::from_vec(Ix1::new([4]), vec![1.0, 2.0, 3.0, 4.0]).unwrap()
}
#[test]
fn test_quantile_weibull_q_half() {
let a = arr_1_4();
let q = quantile_with_method(&a, 0.5, None, QuantileMethod::Weibull).unwrap();
assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
}
#[test]
fn test_quantile_weibull_q_quarter() {
let a = arr_1_4();
let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Weibull).unwrap();
assert!((q.iter().next().copied().unwrap() - 1.25).abs() < 1e-12);
}
#[test]
fn test_quantile_hazen_q_quarter() {
let a = arr_1_4();
let q = quantile_with_method(&a, 0.25, None, QuantileMethod::Hazen).unwrap();
assert!((q.iter().next().copied().unwrap() - 1.5).abs() < 1e-12);
}
#[test]
fn test_quantile_median_unbiased_q_half() {
let a = arr_1_4();
let q = quantile_with_method(&a, 0.5, None, QuantileMethod::MedianUnbiased).unwrap();
assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
}
#[test]
fn test_quantile_normal_unbiased_q_half() {
let a = arr_1_4();
let q = quantile_with_method(&a, 0.5, None, QuantileMethod::NormalUnbiased).unwrap();
assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
}
#[test]
fn test_quantile_interpolated_inverted_cdf_q_half() {
let a = arr_1_4();
let q =
quantile_with_method(&a, 0.5, None, QuantileMethod::InterpolatedInvertedCdf).unwrap();
assert!((q.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
}
#[test]
fn test_quantile_inverted_cdf_q_half() {
let a = arr_1_4();
let q = quantile_with_method(&a, 0.5, None, QuantileMethod::InvertedCdf).unwrap();
assert!((q.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
}
#[test]
fn test_quantile_inverted_cdf_step_function() {
let a = arr_1_5();
let q1 = quantile_with_method(&a, 0.19, None, QuantileMethod::InvertedCdf).unwrap();
assert!((q1.iter().next().copied().unwrap() - 1.0).abs() < 1e-12);
let q2 = quantile_with_method(&a, 0.21, None, QuantileMethod::InvertedCdf).unwrap();
assert!((q2.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
}
#[test]
fn test_quantile_averaged_inverted_cdf_integer_nq_averages() {
let a = arr_1_4();
let q = quantile_with_method(&a, 0.5, None, QuantileMethod::AveragedInvertedCdf).unwrap();
assert!((q.iter().next().copied().unwrap() - 2.5).abs() < 1e-12);
}
#[test]
fn test_quantile_averaged_inverted_cdf_non_integer_nq_matches_inverted_cdf() {
let a = arr_1_5();
let q1 = quantile_with_method(&a, 0.3, None, QuantileMethod::AveragedInvertedCdf).unwrap();
let q2 = quantile_with_method(&a, 0.3, None, QuantileMethod::InvertedCdf).unwrap();
assert_eq!(
q1.iter().next().copied().unwrap(),
q2.iter().next().copied().unwrap()
);
assert!((q1.iter().next().copied().unwrap() - 2.0).abs() < 1e-12);
}
#[test]
fn test_quantile_closest_observation_half_to_even() {
let a = arr_1_4();
let q = quantile_with_method(&a, 0.5, None, QuantileMethod::ClosestObservation).unwrap();
assert!((q.iter().next().copied().unwrap() - 3.0).abs() < 1e-12);
let q2 = quantile_with_method(&a, 0.125, None, QuantileMethod::ClosestObservation).unwrap();
assert!((q2.iter().next().copied().unwrap() - 1.0).abs() < 1e-12);
}
#[test]
fn test_quantile_closest_observation_nq_0_875_rounds_up() {
let a = arr_1_4();
let q = quantile_with_method(&a, 0.875, None, QuantileMethod::ClosestObservation).unwrap();
assert!((q.iter().next().copied().unwrap() - 4.0).abs() < 1e-12);
}
#[test]
fn test_quantile_continuous_methods_agree_at_q_0_and_q_1() {
let a = arr_1_5();
let methods = [
QuantileMethod::Linear,
QuantileMethod::Weibull,
QuantileMethod::Hazen,
QuantileMethod::InterpolatedInvertedCdf,
QuantileMethod::MedianUnbiased,
QuantileMethod::NormalUnbiased,
];
for &m in &methods {
let q0 = quantile_with_method(&a, 0.0, None, m).unwrap();
let q1 = quantile_with_method(&a, 1.0, None, m).unwrap();
assert!(
(q0.iter().next().copied().unwrap() - 1.0).abs() < 1e-12,
"method {m:?} at q=0 should be min"
);
assert!(
(q1.iter().next().copied().unwrap() - 5.0).abs() < 1e-12,
"method {m:?} at q=1 should be max"
);
}
}
#[test]
fn test_quantile_discrete_methods_agree_at_q_1() {
let a = arr_1_5();
let methods = [
QuantileMethod::InvertedCdf,
QuantileMethod::AveragedInvertedCdf,
QuantileMethod::ClosestObservation,
];
for &m in &methods {
let q = quantile_with_method(&a, 1.0, None, m).unwrap();
assert!(
(q.iter().next().copied().unwrap() - 5.0).abs() < 1e-12,
"method {m:?} at q=1 should be max"
);
}
}
#[test]
fn test_quantile_all_13_methods_at_integer_index_agree() {
let a = arr_1_5();
let all_methods = [
QuantileMethod::Linear,
QuantileMethod::Lower,
QuantileMethod::Higher,
QuantileMethod::Nearest,
QuantileMethod::Midpoint,
QuantileMethod::Weibull,
QuantileMethod::Hazen,
QuantileMethod::MedianUnbiased,
QuantileMethod::NormalUnbiased,
];
for &m in &all_methods {
let r = quantile_with_method(&a, 0.5, None, m).unwrap();
assert!(
(r.iter().next().copied().unwrap() - 3.0).abs() < 1e-12,
"method {m:?} at odd n, q=0.5 should be 3.0"
);
}
}
#[test]
fn test_quantile_method_axis_variant_weibull() {
use ferray_core::Ix2;
let a = Array::<f64, Ix2>::from_vec(
Ix2::new([2, 4]),
vec![1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0],
)
.unwrap();
let r = quantile_with_method(&a, 0.5, Some(1), QuantileMethod::Weibull).unwrap();
assert_eq!(r.shape(), &[2]);
let s = r.as_slice().unwrap();
assert!((s[0] - 2.5).abs() < 1e-12);
assert!((s[1] - 25.0).abs() < 1e-12);
}
}