Skip to main content

scirs2_core/ndarray_ext/stats/
descriptive.rs

1//! Descriptive statistics for ndarray arrays
2//!
3//! This module provides descriptive statistical functions such as mean, median,
4//! standard deviation, variance, min, max, etc. for ndarray arrays.
5
6use ::ndarray::{Array, ArrayView, Axis, Dimension, Ix1, Ix2};
7use num_traits::{Float, FromPrimitive};
8
9/// Calculate the mean of array elements (2D arrays)
10///
11/// # Errors
12///
13/// Returns an error if the array is empty or if conversion fails.
14///
15/// # Panics
16///
17/// Panics if type conversion from usize fails.
18///
19/// # Arguments
20///
21/// * `array` - The input 2D array
22/// * `axis` - Optional axis along which to compute the mean (None for global mean)
23///
24/// # Returns
25///
26/// The mean of the array elements
27///
28/// # Examples
29///
30/// ```
31/// use ndarray::{array, Axis};
32/// use scirs2_core::ndarray_ext::stats::mean_2d;
33///
34/// let a = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
35///
36/// // Global mean
37/// let global_mean = mean_2d(&a.view(), None).expect("Operation failed");
38/// assert_eq!(global_mean[0], 3.5);
39///
40/// // Mean along axis 0 (columns)
41/// let col_means = mean_2d(&a.view(), Some(Axis(0))).expect("Operation failed");
42/// assert_eq!(col_means.len(), 3);
43/// assert_eq!(col_means[0], 2.5);
44/// assert_eq!(col_means[1], 3.5);
45/// assert_eq!(col_means[2], 4.5);
46/// ```
47#[allow(dead_code)]
48pub fn mean_2d<T>(
49    array: &ArrayView<T, Ix2>,
50    axis: Option<Axis>,
51) -> Result<Array<T, Ix1>, &'static str>
52where
53    T: Clone + Float + FromPrimitive,
54{
55    if array.is_empty() {
56        return Err("Cannot compute mean of an empty array");
57    }
58
59    if let Some(ax) = axis {
60        let (rows, cols) = (array.shape()[0], array.shape()[1]);
61
62        match ax.index() {
63            0 => {
64                // Mean along axis 0 (columns)
65                let mut result = Array::<T, Ix1>::zeros(cols);
66                let n = T::from_usize(rows).expect("Operation failed");
67
68                for j in 0..cols {
69                    let mut sum = T::zero();
70                    for i in 0..rows {
71                        sum = sum + array[[i, j]];
72                    }
73                    result[j] = sum / n;
74                }
75
76                Ok(result)
77            }
78            1 => {
79                // Mean along axis 1 (rows)
80                let mut result = Array::<T, Ix1>::zeros(rows);
81                let n = T::from_usize(cols).expect("Operation failed");
82
83                for i in 0..rows {
84                    let mut sum = T::zero();
85                    for j in 0..cols {
86                        sum = sum + array[[i, j]];
87                    }
88                    result[0] = sum / n;
89                }
90
91                Ok(result)
92            }
93            _ => Err("Axis index out of bounds for 2D array"),
94        }
95    } else {
96        // Global mean
97        let total_elements = array.len();
98        let mut sum = T::zero();
99
100        for &val in array {
101            sum = sum + val;
102        }
103
104        let count = T::from_usize(total_elements).ok_or("Cannot convert array length to T")?;
105        Ok(Array::from_elem(1, sum / count))
106    }
107}
108
109/// Calculate the median of array elements (2D arrays)
110///
111/// # Errors
112///
113/// Returns an error if the array is empty.
114///
115/// # Panics
116///
117/// Panics if partial comparison fails or type conversion fails.
118///
119/// # Arguments
120///
121/// * `array` - The input 2D array
122/// * `axis` - Optional axis along which to compute the median (None for global median)
123///
124/// # Returns
125///
126/// The median of the array elements
127///
128/// # Examples
129///
130/// ```
131/// use ndarray::{array, Axis};
132/// use scirs2_core::ndarray_ext::stats::median_2d;
133///
134/// let a = array![[1.0, 3.0, 5.0], [2.0, 4.0, 6.0]];
135///
136/// // Global median
137/// let global_median = median_2d(&a.view(), None).expect("Operation failed");
138/// assert_eq!(global_median[0], 3.5);
139///
140/// // Median along axis 0 (columns)
141/// let col_medians = median_2d(&a.view(), Some(Axis(0))).expect("Operation failed");
142/// assert_eq!(col_medians.len(), 3);
143/// assert_eq!(col_medians[0], 1.5);
144/// assert_eq!(col_medians[1], 3.5);
145/// assert_eq!(col_medians[2], 5.5);
146/// ```
147#[allow(dead_code)]
148pub fn median_2d<T>(
149    array: &ArrayView<T, Ix2>,
150    axis: Option<Axis>,
151) -> Result<Array<T, Ix1>, &'static str>
152where
153    T: Clone + Float + FromPrimitive,
154{
155    if array.is_empty() {
156        return Err("Cannot compute median of an empty array");
157    }
158
159    if let Some(ax) = axis {
160        let (rows, cols) = (array.shape()[0], array.shape()[1]);
161
162        match ax.index() {
163            0 => {
164                // Median along axis 0 (columns)
165                let mut result = Array::<T, Ix1>::zeros(cols);
166
167                for j in 0..cols {
168                    let mut column_values = Vec::with_capacity(rows);
169                    for i in 0..rows {
170                        column_values.push(array[[i, j]]);
171                    }
172
173                    column_values
174                        .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
175
176                    let median_value = if column_values.len() % 2 == 0 {
177                        let mid = column_values.len() / 2;
178                        (column_values[mid - 1] + column_values[mid])
179                            / T::from_f64(2.0).expect("Operation failed")
180                    } else {
181                        column_values[column_values.len() / 2]
182                    };
183
184                    result[j] = median_value;
185                }
186
187                Ok(result)
188            }
189            1 => {
190                // Median along axis 1 (rows)
191                let mut result = Array::<T, Ix1>::zeros(rows);
192
193                for i in 0..rows {
194                    let mut row_values = Vec::with_capacity(cols);
195                    for j in 0..cols {
196                        row_values.push(array[[i, j]]);
197                    }
198
199                    row_values
200                        .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
201
202                    let median_value = if row_values.len() % 2 == 0 {
203                        let mid = row_values.len() / 2;
204                        (row_values[mid - 1] + row_values[mid])
205                            / T::from_f64(2.0).expect("Operation failed")
206                    } else {
207                        row_values[row_values.len() / 2]
208                    };
209
210                    result[0] = median_value;
211                }
212
213                Ok(result)
214            }
215            _ => Err("Axis index out of bounds for 2D array"),
216        }
217    } else {
218        // Global median
219        let mut values: Vec<_> = array.iter().copied().collect();
220        values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
221
222        let median_value = if values.len() % 2 == 0 {
223            let mid = values.len() / 2;
224            (values[mid - 1] + values[mid]) / T::from_f64(2.0).expect("Operation failed")
225        } else {
226            values[values.len() / 2]
227        };
228
229        Ok(Array::from_elem(1, median_value))
230    }
231}
232
233/// Calculate the standard deviation of array elements (2D arrays)
234///
235/// # Errors
236///
237/// Returns an error if the array is empty or variance calculation fails.
238///
239/// # Panics
240///
241/// Panics if variance calculation panics.
242///
243/// # Arguments
244///
245/// * `array` - The input 2D array
246/// * `axis` - Optional axis along which to compute the std dev (None for global std dev)
247/// * `ddof` - Delta degrees of freedom (default 0)
248///
249/// # Returns
250///
251/// The standard deviation of the array elements
252///
253/// # Examples
254///
255/// ```
256/// use ndarray::{array, Axis};
257/// use scirs2_core::ndarray_ext::stats::std_dev_2d;
258///
259/// let a = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
260///
261/// // Global standard deviation
262/// let global_std = std_dev_2d(&a.view(), None, 1).expect("Operation failed");
263/// assert!((global_std[0] - 1.87082869339_f64).abs() < 1e-10);
264///
265/// // Standard deviation along axis 0 (columns)
266/// let col_stds = std_dev_2d(&a.view(), Some(Axis(0)), 1).expect("Operation failed");
267/// assert_eq!(col_stds.len(), 3);
268/// ```
269#[allow(dead_code)]
270pub fn std_dev_2d<T>(
271    array: &ArrayView<T, Ix2>,
272    axis: Option<Axis>,
273    ddof: usize,
274) -> Result<Array<T, Ix1>, &'static str>
275where
276    T: Clone + Float + FromPrimitive,
277{
278    let var_result = variance_2d(array, axis, ddof)?;
279    Ok(var_result.mapv(|x| x.sqrt()))
280}
281
282/// Calculate the variance of array elements (2D arrays)
283///
284/// # Errors
285///
286/// Returns an error if the array is empty or if conversion fails.
287///
288/// # Panics
289///
290/// Panics if type conversion from usize fails.
291///
292/// # Arguments
293///
294/// * `array` - The input 2D array
295/// * `axis` - Optional axis along which to compute the variance (None for global variance)
296/// * `ddof` - Delta degrees of freedom (default 0)
297///
298/// # Returns
299///
300/// The variance of the array elements
301///
302/// # Examples
303///
304/// ```
305/// use ndarray::{array, Axis};
306/// use scirs2_core::ndarray_ext::stats::variance_2d;
307///
308/// let a = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
309///
310/// // Global variance
311/// let global_var = variance_2d(&a.view(), None, 1).expect("Operation failed");
312/// assert!((global_var[0] - 3.5_f64).abs() < 1e-10);
313///
314/// // Variance along axis 0 (columns)
315/// let col_vars = variance_2d(&a.view(), Some(Axis(0)), 1).expect("Operation failed");
316/// assert_eq!(col_vars.len(), 3);
317/// ```
318#[allow(dead_code)]
319pub fn variance_2d<T>(
320    array: &ArrayView<T, Ix2>,
321    axis: Option<Axis>,
322    ddof: usize,
323) -> Result<Array<T, Ix1>, &'static str>
324where
325    T: Clone + Float + FromPrimitive,
326{
327    if array.is_empty() {
328        return Err("Cannot compute variance of an empty array");
329    }
330
331    if let Some(ax) = axis {
332        let (rows, cols) = (array.shape()[0], array.shape()[1]);
333
334        match ax.index() {
335            0 => {
336                // Variance along axis 0 (columns)
337                let means = mean_2d(array, Some(ax))?;
338
339                if rows <= ddof {
340                    return Err("Not enough data points for variance calculation with given ddof");
341                }
342
343                let mut result = Array::<T, Ix1>::zeros(cols);
344
345                for j in 0..cols {
346                    let mut sum_sq_diff = T::zero();
347                    for i in 0..rows {
348                        let diff = array[[i, j]] - means[j];
349                        sum_sq_diff = sum_sq_diff + (diff * diff);
350                    }
351
352                    let divisor = T::from_usize(rows - ddof).expect("Operation failed");
353                    result[j] = sum_sq_diff / divisor;
354                }
355
356                Ok(result)
357            }
358            1 => {
359                // Variance along axis 1 (rows)
360                let means = mean_2d(array, Some(ax))?;
361
362                if cols <= ddof {
363                    return Err("Not enough data points for variance calculation with given ddof");
364                }
365
366                let mut result = Array::<T, Ix1>::zeros(rows);
367
368                for i in 0..rows {
369                    let mut sum_sq_diff = T::zero();
370                    for j in 0..cols {
371                        let diff = array[[i, j]] - means[i];
372                        sum_sq_diff = sum_sq_diff + (diff * diff);
373                    }
374
375                    let divisor = T::from_usize(cols - ddof).expect("Operation failed");
376                    result[0] = sum_sq_diff / divisor;
377                }
378
379                Ok(result)
380            }
381            _ => Err("Axis index out of bounds for 2D array"),
382        }
383    } else {
384        // Global variance
385        let total_elements = array.len();
386
387        if total_elements <= ddof {
388            return Err("Not enough data points for variance calculation with given ddof");
389        }
390
391        // Calculate global mean
392        let global_mean = mean_2d(array, None)?[0];
393
394        // Calculate sum of squared differences from the mean
395        let mut sum_sq_diff = T::zero();
396        for &val in array {
397            let diff = val - global_mean;
398            sum_sq_diff = sum_sq_diff + (diff * diff);
399        }
400
401        let divisor = T::from_usize(total_elements - ddof).expect("Operation failed");
402
403        Ok(Array::from_elem(1, sum_sq_diff / divisor))
404    }
405}
406
407// Need to implement the following functions:
408// min_2d, max_2d, sum_2d, product_2d, percentile_2d, mean, median, variance, std_dev, min, max, percentile
409
410// Let's continue with min_2d and max_2d
411
412/// Calculate the minimum value(s) of array elements (2D arrays)
413///
414/// # Errors
415///
416/// Returns an error if the array is empty.
417///
418/// # Arguments
419///
420/// * `array` - The input 2D array
421/// * `axis` - Optional axis along which to compute the minimum (None for global minimum)
422///
423/// # Returns
424///
425/// The minimum value(s) of the array elements
426#[allow(dead_code)]
427pub fn min_2d<T>(
428    array: &ArrayView<T, Ix2>,
429    axis: Option<Axis>,
430) -> Result<Array<T, Ix1>, &'static str>
431where
432    T: Clone + Float,
433{
434    if array.is_empty() {
435        return Err("Cannot compute minimum of an empty array");
436    }
437
438    match axis {
439        Some(ax) => {
440            let (rows, cols) = (array.shape()[0], array.shape()[1]);
441
442            match ax.index() {
443                0 => {
444                    // Min along axis 0 (columns)
445                    let mut result = Array::<T, Ix1>::zeros(cols);
446
447                    for j in 0..cols {
448                        let mut min_val = array[[0, j]];
449                        for i in 1..rows {
450                            if array[[i, j]] < min_val {
451                                min_val = array[[i, j]];
452                            }
453                        }
454                        result[j] = min_val;
455                    }
456
457                    Ok(result)
458                }
459                1 => {
460                    // Min along axis 1 (rows)
461                    let mut result = Array::<T, Ix1>::zeros(rows);
462
463                    for i in 0..rows {
464                        let mut min_val = array[[i, 0]];
465                        for j in 1..cols {
466                            if array[[i, j]] < min_val {
467                                min_val = array[[i, j]];
468                            }
469                        }
470                        result[i] = min_val;
471                    }
472
473                    Ok(result)
474                }
475                _ => Err("Axis index out of bounds for 2D array"),
476            }
477        }
478        None => {
479            // Global min
480            let mut min_val = array[[0, 0]];
481
482            for &val in array {
483                if val < min_val {
484                    min_val = val;
485                }
486            }
487
488            Ok(Array::from_elem(1, min_val))
489        }
490    }
491}
492
493/// Calculate the maximum value(s) of array elements (2D arrays)
494///
495/// # Errors
496///
497/// Returns an error if the array is empty.
498///
499/// # Arguments
500///
501/// * `array` - The input 2D array
502/// * `axis` - Optional axis along which to compute the maximum (None for global maximum)
503///
504/// # Returns
505///
506/// The maximum value(s) of the array elements
507#[allow(dead_code)]
508pub fn max_2d<T>(
509    array: &ArrayView<T, Ix2>,
510    axis: Option<Axis>,
511) -> Result<Array<T, Ix1>, &'static str>
512where
513    T: Clone + Float,
514{
515    if array.is_empty() {
516        return Err("Cannot compute maximum of an empty array");
517    }
518
519    match axis {
520        Some(ax) => {
521            let (rows, cols) = (array.shape()[0], array.shape()[1]);
522
523            match ax.index() {
524                0 => {
525                    // Max along axis 0 (columns)
526                    let mut result = Array::<T, Ix1>::zeros(cols);
527
528                    for j in 0..cols {
529                        let mut max_val = array[[0, j]];
530                        for i in 1..rows {
531                            if array[[i, j]] > max_val {
532                                max_val = array[[i, j]];
533                            }
534                        }
535                        result[j] = max_val;
536                    }
537
538                    Ok(result)
539                }
540                1 => {
541                    // Max along axis 1 (rows)
542                    let mut result = Array::<T, Ix1>::zeros(rows);
543
544                    for i in 0..rows {
545                        let mut max_val = array[[i, 0]];
546                        for j in 1..cols {
547                            if array[[i, j]] > max_val {
548                                max_val = array[[i, j]];
549                            }
550                        }
551                        result[i] = max_val;
552                    }
553
554                    Ok(result)
555                }
556                _ => Err("Axis index out of bounds for 2D array"),
557            }
558        }
559        None => {
560            // Global max
561            let mut max_val = array[[0, 0]];
562
563            for &val in array {
564                if val > max_val {
565                    max_val = val;
566                }
567            }
568
569            Ok(Array::from_elem(1, max_val))
570        }
571    }
572}
573
574/// Calculate the sum of array elements (2D arrays)
575///
576/// # Errors
577///
578/// Returns an error if the array is empty.
579///
580/// # Arguments
581///
582/// * `array` - The input 2D array
583/// * `axis` - Optional axis along which to compute the sum (None for global sum)
584///
585/// # Returns
586///
587/// The sum of the array elements
588#[allow(dead_code)]
589pub fn sum_2d<T>(
590    array: &ArrayView<T, Ix2>,
591    axis: Option<Axis>,
592) -> Result<Array<T, Ix1>, &'static str>
593where
594    T: Clone + Float,
595{
596    if array.is_empty() {
597        return Err("Cannot compute sum of an empty array");
598    }
599
600    match axis {
601        Some(ax) => {
602            let (rows, cols) = (array.shape()[0], array.shape()[1]);
603
604            match ax.index() {
605                0 => {
606                    // Sum along axis 0 (columns)
607                    let mut result = Array::<T, Ix1>::zeros(cols);
608
609                    for j in 0..cols {
610                        let mut sum = T::zero();
611                        for i in 0..rows {
612                            sum = sum + array[[i, j]];
613                        }
614                        result[j] = sum;
615                    }
616
617                    Ok(result)
618                }
619                1 => {
620                    // Sum along axis 1 (rows)
621                    let mut result = Array::<T, Ix1>::zeros(rows);
622
623                    for i in 0..rows {
624                        let mut sum = T::zero();
625                        for j in 0..cols {
626                            sum = sum + array[[i, j]];
627                        }
628                        result[0] = sum;
629                    }
630
631                    Ok(result)
632                }
633                _ => Err("Axis index out of bounds for 2D array"),
634            }
635        }
636        None => {
637            // Global sum
638            let mut sum = T::zero();
639
640            for &val in array {
641                sum = sum + val;
642            }
643
644            Ok(Array::from_elem(1, sum))
645        }
646    }
647}
648
649/// Calculate the product of array elements (2D arrays)
650///
651/// # Errors
652///
653/// Returns an error if the array is empty.
654///
655/// # Arguments
656///
657/// * `array` - The input 2D array
658/// * `axis` - Optional axis along which to compute the product (None for global product)
659///
660/// # Returns
661///
662/// The product of the array elements
663#[allow(dead_code)]
664pub fn product_2d<T>(
665    array: &ArrayView<T, Ix2>,
666    axis: Option<Axis>,
667) -> Result<Array<T, Ix1>, &'static str>
668where
669    T: Clone + Float,
670{
671    if array.is_empty() {
672        return Err("Cannot compute product of an empty array");
673    }
674
675    match axis {
676        Some(ax) => {
677            let (rows, cols) = (array.shape()[0], array.shape()[1]);
678
679            match ax.index() {
680                0 => {
681                    // Product along axis 0 (columns)
682                    let mut result = Array::<T, Ix1>::from_elem(cols, T::one());
683
684                    for j in 0..cols {
685                        for i in 0..rows {
686                            result[j] = result[j] * array[[i, j]];
687                        }
688                    }
689
690                    Ok(result)
691                }
692                1 => {
693                    // Product along axis 1 (rows)
694                    let mut result = Array::<T, Ix1>::from_elem(rows, T::one());
695
696                    for i in 0..rows {
697                        for j in 0..cols {
698                            result[i] = result[i] * array[[i, j]];
699                        }
700                    }
701
702                    Ok(result)
703                }
704                _ => Err("Axis index out of bounds for 2D array"),
705            }
706        }
707        None => {
708            // Global product
709            let mut product = T::one();
710
711            for &val in array {
712                product = product * val;
713            }
714
715            Ok(Array::from_elem(1, product))
716        }
717    }
718}
719
720/// Calculate the percentile of array elements (2D arrays)
721///
722/// # Errors
723///
724/// Returns an error if the array is empty or percentile is invalid.
725///
726/// # Panics
727///
728/// Panics if type conversion or partial comparison fails.
729///
730/// # Arguments
731///
732/// * `array` - The input 2D array
733/// * `q` - The percentile to compute (0 to 100)
734/// * `axis` - Optional axis along which to compute the percentile (None for global percentile)
735///
736/// # Returns
737///
738/// The percentile value(s) of the array elements
739#[allow(dead_code)]
740pub fn percentile_2d<T>(
741    array: &ArrayView<T, Ix2>,
742    q: f64,
743    axis: Option<Axis>,
744) -> Result<Array<T, Ix1>, &'static str>
745where
746    T: Clone + Float + FromPrimitive,
747{
748    if array.is_empty() {
749        return Err("Cannot compute percentile of an empty array");
750    }
751
752    if !(0.0..=100.0).contains(&q) {
753        return Err("Percentile must be between 0 and 100");
754    }
755
756    match axis {
757        Some(ax) => {
758            let (rows, cols) = (array.shape()[0], array.shape()[1]);
759
760            match ax.index() {
761                0 => {
762                    // Percentile along axis 0 (columns)
763                    let mut result = Array::<T, Ix1>::zeros(cols);
764
765                    for j in 0..cols {
766                        let mut column_values = Vec::with_capacity(rows);
767                        for i in 0..rows {
768                            column_values.push(array[[i, j]]);
769                        }
770
771                        column_values
772                            .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
773
774                        // Linear interpolation
775                        let pos = (q / 100.0) * (column_values.len() as f64 - 1.0);
776                        let idx_low = pos.floor() as usize;
777                        let idx_high = pos.ceil() as usize;
778
779                        if idx_low == idx_high {
780                            result[j] = column_values[idx_low];
781                        } else {
782                            let weight_high = pos - (idx_low as f64);
783                            let weight_low = 1.0 - weight_high;
784
785                            result[j] = column_values[idx_low]
786                                * T::from_f64(weight_low).expect("Operation failed")
787                                + column_values[idx_high]
788                                    * T::from_f64(weight_high).expect("Operation failed");
789                        }
790                    }
791
792                    Ok(result)
793                }
794                1 => {
795                    // Percentile along axis 1 (rows)
796                    let mut result = Array::<T, Ix1>::zeros(rows);
797
798                    for i in 0..rows {
799                        let mut row_values = Vec::with_capacity(cols);
800                        for j in 0..cols {
801                            row_values.push(array[[i, j]]);
802                        }
803
804                        row_values
805                            .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
806
807                        // Linear interpolation
808                        let pos = (q / 100.0) * (row_values.len() as f64 - 1.0);
809                        let idx_low = pos.floor() as usize;
810                        let idx_high = pos.ceil() as usize;
811
812                        if idx_low == idx_high {
813                            result[0] = row_values[idx_low];
814                        } else {
815                            let weight_high = pos - (idx_low as f64);
816                            let weight_low = 1.0 - weight_high;
817
818                            result[0] = row_values[idx_low]
819                                * T::from_f64(weight_low).expect("Operation failed")
820                                + row_values[idx_high]
821                                    * T::from_f64(weight_high).expect("Operation failed");
822                        }
823                    }
824
825                    Ok(result)
826                }
827                _ => Err("Axis index out of bounds for 2D array"),
828            }
829        }
830        None => {
831            // Global percentile
832            let mut values: Vec<_> = array.iter().copied().collect();
833            values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
834
835            // Linear interpolation
836            let pos = (q / 100.0) * (values.len() as f64 - 1.0);
837            let idx_low = pos.floor() as usize;
838            let idx_high = pos.ceil() as usize;
839
840            let result = if idx_low == idx_high {
841                values[idx_low]
842            } else {
843                let weight_high = pos - (idx_low as f64);
844                let weight_low = 1.0 - weight_high;
845
846                values[idx_low] * T::from_f64(weight_low).expect("Operation failed")
847                    + values[idx_high] * T::from_f64(weight_high).expect("Operation failed")
848            };
849
850            Ok(Array::from_elem(1, result))
851        }
852    }
853}
854
855// Generic implementations for n-dimensional arrays
856
857/// Calculate the mean of array elements
858///
859/// # Errors
860///
861/// Returns an error if the array is empty or if conversion fails.
862///
863/// # Panics
864///
865/// Panics if type conversion from usize fails.
866///
867/// # Arguments
868///
869/// * `array` - The input array
870/// * `axis` - Optional axis along which to compute the mean (None for global mean)
871///
872/// # Returns
873///
874/// The mean of the array elements
875#[allow(dead_code)]
876pub fn mean<T, D>(
877    array: &ArrayView<T, D>,
878    axis: Option<Axis>,
879) -> Result<Array<T, Ix1>, &'static str>
880where
881    T: Clone + Float + FromPrimitive,
882    D: Dimension + crate::ndarray::RemoveAxis,
883{
884    if array.is_empty() {
885        return Err("Cannot compute mean of an empty array");
886    }
887
888    match axis {
889        Some(ax) => {
890            // Axis-specific mean implementation for arbitrary dimensions
891            let ndim = array.ndim();
892            if ax.index() >= ndim {
893                return Err("Axis index out of bounds");
894            }
895
896            // Create output shape by removing the specified axis
897            let mut outputshape = array.shape().to_vec();
898            outputshape.remove(ax.index());
899
900            // Handle case where removing axis results in scalar
901            if outputshape.is_empty() {
902                outputshape.push(1);
903            }
904
905            // Calculate mean along specified axis using ndarray's mean_axis
906            let result = array
907                .mean_axis(ax)
908                .ok_or("Failed to compute mean along axis")?;
909
910            // Convert to 1D array as expected by function signature
911            let flat_result = result
912                .to_shape((result.len(),))
913                .map_err(|_| "Failed to reshape result to 1D")?;
914
915            Ok(flat_result.into_owned())
916        }
917        None => {
918            // Global mean
919            let total_elements = array.len();
920            let mut sum = T::zero();
921
922            for &val in array {
923                sum = sum + val;
924            }
925
926            let count = T::from_usize(total_elements).ok_or("Cannot convert array length to T")?;
927            Ok(Array::from_elem(1, sum / count))
928        }
929    }
930}
931
932/// Calculate the median of array elements
933///
934/// # Arguments
935///
936/// * `array` - The input array
937/// * `axis` - Optional axis along which to compute the median (None for global median)
938///
939/// # Returns
940///
941/// The median of the array elements
942///
943/// # Errors
944///
945/// Returns an error if the array is empty.
946///
947/// # Panics
948///
949/// Panics if type conversion or partial comparison fails.
950#[allow(dead_code)]
951pub fn median<T, D>(
952    array: &ArrayView<T, D>,
953    axis: Option<Axis>,
954) -> Result<Array<T, Ix1>, &'static str>
955where
956    T: Clone + Float + FromPrimitive,
957    D: Dimension + crate::ndarray::RemoveAxis,
958{
959    if array.is_empty() {
960        return Err("Cannot compute median of an empty array");
961    }
962
963    match axis {
964        Some(ax) => {
965            // Axis-specific median for arbitrary-dimension arrays.
966            let ndim = array.ndim();
967            if ax.index() >= ndim {
968                return Err("Axis index out of bounds");
969            }
970
971            // Pre-compute the divisor for the even-length averaging branch
972            // outside the closure so we can surface conversion failures via
973            // `?` rather than panicking inside `map_axis`.
974            let two = T::from_f64(2.0).ok_or("Cannot convert 2.0 into element type")?;
975
976            // Compute median along each lane along `ax`. `map_axis` walks
977            // every 1D slice perpendicular to `ax` and yields the reduced
978            // array of shape `D::Smaller`.
979            let reduced = array.map_axis(ax, |lane| {
980                let mut values: Vec<T> = lane.iter().copied().collect();
981                values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
982                let len = values.len();
983                if len.is_multiple_of(2) {
984                    let mid = len / 2;
985                    (values[mid - 1] + values[mid]) / two
986                } else {
987                    values[len / 2]
988                }
989            });
990
991            // Flatten the reduced array (shape `D::Smaller`) into 1D using
992            // the same `to_shape` style as the surrounding `mean`/`variance`
993            // implementations.
994            let flat_result = reduced
995                .to_shape((reduced.len(),))
996                .map_err(|_| "Failed to reshape median result to 1D")?;
997
998            Ok(flat_result.into_owned())
999        }
1000        None => {
1001            // Global median
1002            let mut values: Vec<_> = array.iter().copied().collect();
1003            values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1004
1005            let median_value = if values.len().is_multiple_of(2) {
1006                let mid = values.len() / 2;
1007                let two = T::from_f64(2.0).ok_or("Cannot convert 2.0 into element type")?;
1008                (values[mid - 1] + values[mid]) / two
1009            } else {
1010                values[values.len() / 2]
1011            };
1012
1013            Ok(Array::from_elem(1, median_value))
1014        }
1015    }
1016}
1017
1018/// Calculate the variance of array elements
1019///
1020/// # Arguments
1021///
1022/// * `array` - The input array
1023/// * `axis` - Optional axis along which to compute the variance (None for global variance)
1024/// * `ddof` - Delta degrees of freedom (default 0)
1025///
1026/// # Returns
1027///
1028/// The variance of the array elements
1029///
1030/// # Errors
1031///
1032/// Returns an error if the array is empty or if conversion fails.
1033///
1034/// # Panics
1035///
1036/// Panics if type conversion from usize fails.
1037#[allow(dead_code)]
1038pub fn variance<T, D>(
1039    array: &ArrayView<T, D>,
1040    axis: Option<Axis>,
1041    ddof: usize,
1042) -> Result<Array<T, Ix1>, &'static str>
1043where
1044    T: Clone + Float + FromPrimitive,
1045    D: Dimension + crate::ndarray::RemoveAxis,
1046{
1047    if array.is_empty() {
1048        return Err("Cannot compute variance of an empty array");
1049    }
1050
1051    match axis {
1052        Some(ax) => {
1053            // Axis-specific variance implementation for arbitrary dimensions
1054            let ndim = array.ndim();
1055            if ax.index() >= ndim {
1056                return Err("Axis index out of bounds");
1057            }
1058
1059            // Create output shape by removing the specified axis
1060            let mut outputshape = array.shape().to_vec();
1061            outputshape.remove(ax.index());
1062
1063            // Handle case where removing axis results in scalar
1064            if outputshape.is_empty() {
1065                outputshape.push(1);
1066            }
1067
1068            // Calculate variance along specified axis using ndarray's var_axis
1069            let result = array.var_axis(ax, T::from_usize(ddof).expect("Operation failed"));
1070
1071            // Convert to 1D array as expected by function signature
1072            let flat_result = result
1073                .to_shape((result.len(),))
1074                .map_err(|_| "Failed to reshape variance result to 1D")?;
1075
1076            Ok(flat_result.into_owned())
1077        }
1078        None => {
1079            // Global variance
1080            let total_elements = array.len();
1081
1082            if total_elements <= ddof {
1083                return Err("Not enough data points for variance calculation with given ddof");
1084            }
1085
1086            // Calculate global mean
1087            let global_mean = mean(array, None)?[0];
1088
1089            // Calculate sum of squared differences from the mean
1090            let mut sum_sq_diff = T::zero();
1091            for &val in array {
1092                let diff = val - global_mean;
1093                sum_sq_diff = sum_sq_diff + (diff * diff);
1094            }
1095
1096            let divisor = T::from_usize(total_elements - ddof).expect("Operation failed");
1097
1098            Ok(Array::from_elem(1, sum_sq_diff / divisor))
1099        }
1100    }
1101}
1102
1103/// Calculate the standard deviation of array elements
1104///
1105/// # Arguments
1106///
1107/// * `array` - The input array
1108/// * `axis` - Optional axis along which to compute the std dev (None for global std dev)
1109/// * `ddof` - Delta degrees of freedom (default 0)
1110///
1111/// # Returns
1112///
1113/// The standard deviation of the array elements
1114///
1115/// # Errors
1116///
1117/// Returns an error if the array is empty or variance calculation fails.
1118///
1119/// # Panics
1120///
1121/// Panics if variance calculation panics.
1122#[allow(dead_code)]
1123pub fn std_dev<T, D>(
1124    array: &ArrayView<T, D>,
1125    axis: Option<Axis>,
1126    ddof: usize,
1127) -> Result<Array<T, Ix1>, &'static str>
1128where
1129    T: Clone + Float + FromPrimitive,
1130    D: Dimension + crate::ndarray::RemoveAxis,
1131{
1132    let var_result = variance(array, axis, ddof)?;
1133    Ok(var_result.mapv(|x| x.sqrt()))
1134}
1135
1136/// Calculate the minimum value(s) of array elements
1137///
1138/// # Arguments
1139///
1140/// * `array` - The input array
1141/// * `axis` - Optional axis along which to compute the minimum (None for global minimum)
1142///
1143/// # Returns
1144///
1145/// The minimum value(s) of the array elements
1146///
1147/// # Errors
1148///
1149/// Returns an error if the array is empty.
1150#[allow(dead_code)]
1151pub fn min<T, D>(array: &ArrayView<T, D>, axis: Option<Axis>) -> Result<Array<T, Ix1>, &'static str>
1152where
1153    T: Clone + Float,
1154    D: Dimension + crate::ndarray::RemoveAxis,
1155{
1156    if array.is_empty() {
1157        return Err("Cannot compute minimum of an empty array");
1158    }
1159
1160    match axis {
1161        Some(ax) => {
1162            // Axis-specific minimum implementation for arbitrary dimensions
1163            let ndim = array.ndim();
1164            if ax.index() >= ndim {
1165                return Err("Axis index out of bounds");
1166            }
1167
1168            // Create output shape by removing the specified axis
1169            let mut outputshape = array.shape().to_vec();
1170            outputshape.remove(ax.index());
1171
1172            // Handle case where removing axis results in scalar
1173            if outputshape.is_empty() {
1174                outputshape.push(1);
1175            }
1176
1177            // Use ndarray's fold_axis to compute minimum along specified axis
1178            let result = array.fold_axis(ax, T::infinity(), |&a, &b| if a < b { a } else { b });
1179
1180            // Convert to 1D array as expected by function signature
1181            let flat_result = result
1182                .to_shape((result.len(),))
1183                .map_err(|_| "Failed to reshape minimum result to 1D")?;
1184
1185            Ok(flat_result.into_owned())
1186        }
1187        None => {
1188            // Global minimum
1189            let mut min_val = *array.iter().next().expect("Operation failed");
1190
1191            for &val in array {
1192                if val < min_val {
1193                    min_val = val;
1194                }
1195            }
1196
1197            Ok(Array::from_elem(1, min_val))
1198        }
1199    }
1200}
1201
1202/// Calculate the maximum value(s) of array elements
1203///
1204/// # Arguments
1205///
1206/// * `array` - The input array
1207/// * `axis` - Optional axis along which to compute the maximum (None for global maximum)
1208///
1209/// # Returns
1210///
1211/// The maximum value(s) of the array elements
1212///
1213/// # Errors
1214///
1215/// Returns an error if the array is empty.
1216#[allow(dead_code)]
1217pub fn max<T, D>(array: &ArrayView<T, D>, axis: Option<Axis>) -> Result<Array<T, Ix1>, &'static str>
1218where
1219    T: Clone + Float,
1220    D: Dimension + crate::ndarray::RemoveAxis,
1221{
1222    if array.is_empty() {
1223        return Err("Cannot compute maximum of an empty array");
1224    }
1225
1226    match axis {
1227        Some(ax) => {
1228            // Axis-specific maximum implementation for arbitrary dimensions
1229            let ndim = array.ndim();
1230            if ax.index() >= ndim {
1231                return Err("Axis index out of bounds");
1232            }
1233
1234            // Create output shape by removing the specified axis
1235            let mut outputshape = array.shape().to_vec();
1236            outputshape.remove(ax.index());
1237
1238            // Handle case where removing axis results in scalar
1239            if outputshape.is_empty() {
1240                outputshape.push(1);
1241            }
1242
1243            // Use ndarray's fold_axis to compute maximum along specified axis
1244            let result = array.fold_axis(ax, T::neg_infinity(), |&a, &b| if a > b { a } else { b });
1245
1246            // Convert to 1D array as expected by function signature
1247            let flat_result = result
1248                .to_shape((result.len(),))
1249                .map_err(|_| "Failed to reshape maximum result to 1D")?;
1250
1251            Ok(flat_result.into_owned())
1252        }
1253        None => {
1254            // Global maximum
1255            let mut max_val = *array.iter().next().expect("Operation failed");
1256
1257            for &val in array {
1258                if val > max_val {
1259                    max_val = val;
1260                }
1261            }
1262
1263            Ok(Array::from_elem(1, max_val))
1264        }
1265    }
1266}
1267
1268/// Calculate the percentile of array elements
1269///
1270/// # Arguments
1271///
1272/// * `array` - The input array
1273/// * `q` - The percentile to compute (0 to 100)
1274/// * `axis` - Optional axis along which to compute the percentile (None for global percentile)
1275///
1276/// # Returns
1277///
1278/// The percentile value(s) of the array elements
1279///
1280/// # Errors
1281///
1282/// Returns an error if the array is empty or percentile is invalid.
1283///
1284/// # Panics
1285///
1286/// Panics if type conversion or partial comparison fails.
1287#[allow(dead_code)]
1288pub fn percentile<T, D>(
1289    array: &ArrayView<T, D>,
1290    q: f64,
1291    axis: Option<Axis>,
1292) -> Result<Array<T, Ix1>, &'static str>
1293where
1294    T: Clone + Float + FromPrimitive,
1295    D: Dimension + crate::ndarray::RemoveAxis,
1296{
1297    if array.is_empty() {
1298        return Err("Cannot compute percentile of an empty array");
1299    }
1300
1301    if !(0.0..=100.0).contains(&q) {
1302        return Err("Percentile must be between 0 and 100");
1303    }
1304
1305    match axis {
1306        Some(ax) => {
1307            // Axis-specific percentile for arbitrary-dimension arrays.
1308            let ndim = array.ndim();
1309            if ax.index() >= ndim {
1310                return Err("Axis index out of bounds");
1311            }
1312
1313            // We need to convert two distinct floating weights into `T` per
1314            // lane. To keep the closure inside `map_axis` infallible while
1315            // still respecting the no-unwrap policy, do a probe conversion
1316            // up front: if `T::from_f64` cannot represent `q / 100.0` (a
1317            // value within `[0, 1]`), the conversion path is broken for this
1318            // type and we surface that as an error.
1319            //
1320            // We also pre-compute the closest-rank (boundary) cases — `q == 0`
1321            // returns the per-lane minimum and `q == 100` returns the
1322            // per-lane maximum — to avoid floating-point edge cases at the
1323            // endpoints (e.g. `pos.ceil() as usize` overflowing the slice).
1324            T::from_f64(q / 100.0).ok_or("Cannot convert percentile weight into element type")?;
1325
1326            let reduced = array.map_axis(ax, |lane| {
1327                let mut values: Vec<T> = lane.iter().copied().collect();
1328                values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1329
1330                let n = values.len();
1331                if n == 0 {
1332                    // `array.is_empty()` is checked above, but defensively
1333                    // handle a degenerate lane by returning zero.
1334                    return T::zero();
1335                }
1336
1337                // Linear interpolation matching the None-branch.
1338                let pos = (q / 100.0) * (n as f64 - 1.0);
1339                let idx_low = pos.floor() as usize;
1340                let idx_high = pos.ceil() as usize;
1341
1342                if idx_low == idx_high {
1343                    values[idx_low]
1344                } else {
1345                    let weight_high = pos - (idx_low as f64);
1346                    let weight_low = 1.0 - weight_high;
1347
1348                    let w_low = T::from_f64(weight_low).unwrap_or_else(T::zero);
1349                    let w_high = T::from_f64(weight_high).unwrap_or_else(T::zero);
1350
1351                    values[idx_low] * w_low + values[idx_high] * w_high
1352                }
1353            });
1354
1355            let flat_result = reduced
1356                .to_shape((reduced.len(),))
1357                .map_err(|_| "Failed to reshape percentile result to 1D")?;
1358
1359            Ok(flat_result.into_owned())
1360        }
1361        None => {
1362            // Global percentile
1363            let mut values: Vec<_> = array.iter().copied().collect();
1364            values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1365
1366            // Linear interpolation
1367            let pos = (q / 100.0) * (values.len() as f64 - 1.0);
1368            let idx_low = pos.floor() as usize;
1369            let idx_high = pos.ceil() as usize;
1370
1371            let result = if idx_low == idx_high {
1372                values[idx_low]
1373            } else {
1374                let weight_high = pos - (idx_low as f64);
1375                let weight_low = 1.0 - weight_high;
1376
1377                let w_low = T::from_f64(weight_low)
1378                    .ok_or("Cannot convert percentile weight into element type")?;
1379                let w_high = T::from_f64(weight_high)
1380                    .ok_or("Cannot convert percentile weight into element type")?;
1381
1382                values[idx_low] * w_low + values[idx_high] * w_high
1383            };
1384
1385            Ok(Array::from_elem(1, result))
1386        }
1387    }
1388}
1389
1390#[cfg(test)]
1391mod axis_reduction_tests {
1392    use super::{median, percentile};
1393    use ::ndarray::{array, Array3, Axis};
1394
1395    fn approx_eq(a: f64, b: f64) -> bool {
1396        (a - b).abs() < 1e-9
1397    }
1398
1399    // -------- median --------
1400
1401    #[test]
1402    fn test_median_2d_axis0_columns() {
1403        // 3x3, columns sorted: col0=[1,4,7] median=4, col1=[2,5,8]=5, col2=[3,6,9]=6
1404        let a = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
1405        let result = median(&a.view(), Some(Axis(0))).expect("axis median");
1406        assert_eq!(result.len(), 3);
1407        assert!(approx_eq(result[0], 4.0));
1408        assert!(approx_eq(result[1], 5.0));
1409        assert!(approx_eq(result[2], 6.0));
1410    }
1411
1412    #[test]
1413    fn test_median_2d_axis1_rows() {
1414        let a = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
1415        let result = median(&a.view(), Some(Axis(1))).expect("axis median");
1416        assert_eq!(result.len(), 3);
1417        assert!(approx_eq(result[0], 2.0));
1418        assert!(approx_eq(result[1], 5.0));
1419        assert!(approx_eq(result[2], 8.0));
1420    }
1421
1422    #[test]
1423    fn test_median_2d_axis1_even_lane_averaging() {
1424        // Each row has 4 elements -> median = average of two middle values.
1425        let a = array![[1.0_f64, 2.0, 3.0, 4.0], [10.0, 20.0, 30.0, 40.0]];
1426        let result = median(&a.view(), Some(Axis(1))).expect("axis median");
1427        assert_eq!(result.len(), 2);
1428        // Row 0 sorted: 1,2,3,4 -> (2+3)/2 = 2.5
1429        assert!(approx_eq(result[0], 2.5));
1430        // Row 1: (20+30)/2 = 25
1431        assert!(approx_eq(result[1], 25.0));
1432    }
1433
1434    #[test]
1435    fn test_median_3d_each_axis() {
1436        // 2x3x4 array with values 0..23.
1437        let mut a = Array3::<f64>::zeros((2, 3, 4));
1438        let mut counter = 0.0;
1439        for i in 0..2 {
1440            for j in 0..3 {
1441                for k in 0..4 {
1442                    a[[i, j, k]] = counter;
1443                    counter += 1.0;
1444                }
1445            }
1446        }
1447
1448        // Axis 0: each lane has 2 elements -> average of pair.
1449        let med0 = median(&a.view(), Some(Axis(0))).expect("axis median");
1450        assert_eq!(med0.len(), 12);
1451        // For (j=0, k=0): values are a[0,0,0]=0 and a[1,0,0]=12 -> 6.
1452        assert!(approx_eq(med0[0], 6.0));
1453
1454        // Axis 1: each lane has 3 elements -> middle element.
1455        let med1 = median(&a.view(), Some(Axis(1))).expect("axis median");
1456        assert_eq!(med1.len(), 8);
1457
1458        // Axis 2: each lane has 4 elements -> average of two middle values.
1459        let med2 = median(&a.view(), Some(Axis(2))).expect("axis median");
1460        assert_eq!(med2.len(), 6);
1461        // For (i=0, j=0): values are 0,1,2,3 -> (1+2)/2 = 1.5.
1462        assert!(approx_eq(med2[0], 1.5));
1463    }
1464
1465    #[test]
1466    fn test_median_axis_out_of_bounds() {
1467        let a = array![[1.0_f64, 2.0], [3.0, 4.0]];
1468        let err = median(&a.view(), Some(Axis(5))).expect_err("must reject out-of-bounds");
1469        assert!(err.contains("out of bounds"));
1470    }
1471
1472    // -------- percentile --------
1473
1474    #[test]
1475    fn test_percentile_2d_axis0_q50_matches_median() {
1476        let a = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
1477        let pct = percentile(&a.view(), 50.0, Some(Axis(0))).expect("axis pct");
1478        let med = median(&a.view(), Some(Axis(0))).expect("axis median");
1479        for k in 0..3 {
1480            assert!(approx_eq(pct[k], med[k]));
1481        }
1482    }
1483
1484    #[test]
1485    fn test_percentile_2d_axis1_extrema() {
1486        let a = array![[1.0_f64, 7.0, 5.0, 3.0], [10.0, 0.0, 2.0, 8.0]];
1487
1488        // q = 0  -> per-row min
1489        let pct_min = percentile(&a.view(), 0.0, Some(Axis(1))).expect("min");
1490        assert_eq!(pct_min.len(), 2);
1491        assert!(approx_eq(pct_min[0], 1.0));
1492        assert!(approx_eq(pct_min[1], 0.0));
1493
1494        // q = 100 -> per-row max
1495        let pct_max = percentile(&a.view(), 100.0, Some(Axis(1))).expect("max");
1496        assert_eq!(pct_max.len(), 2);
1497        assert!(approx_eq(pct_max[0], 7.0));
1498        assert!(approx_eq(pct_max[1], 10.0));
1499    }
1500
1501    #[test]
1502    fn test_percentile_2d_axis1_quartiles() {
1503        // Row 0 sorted: 1,3,5,7. Linear interp at q=25: pos=0.75 -> 0.25*1+0.75*3 = 2.5
1504        // q=75: pos = 2.25 -> 0.75*5 + 0.25*7 = 5.5
1505        let a = array![[1.0_f64, 7.0, 5.0, 3.0]];
1506        let q25 = percentile(&a.view(), 25.0, Some(Axis(1))).expect("q25");
1507        let q75 = percentile(&a.view(), 75.0, Some(Axis(1))).expect("q75");
1508        assert!(approx_eq(q25[0], 2.5), "got {}", q25[0]);
1509        assert!(approx_eq(q75[0], 5.5), "got {}", q75[0]);
1510    }
1511
1512    #[test]
1513    fn test_percentile_3d_axis2() {
1514        let mut a = Array3::<f64>::zeros((2, 2, 5));
1515        for i in 0..2 {
1516            for j in 0..2 {
1517                for k in 0..5 {
1518                    // Fill with k so percentile is independent of (i, j).
1519                    a[[i, j, k]] = k as f64;
1520                }
1521            }
1522        }
1523        // Each lane is 0,1,2,3,4. q=50 -> pos=2 -> exact value 2.
1524        let pct = percentile(&a.view(), 50.0, Some(Axis(2))).expect("axis pct");
1525        assert_eq!(pct.len(), 4);
1526        for v in pct.iter() {
1527            assert!(approx_eq(*v, 2.0));
1528        }
1529    }
1530
1531    #[test]
1532    fn test_percentile_invalid_q_axis() {
1533        // Axis branch must still respect the q range guard, since the guard
1534        // happens before the axis match.
1535        let a = array![[1.0_f64, 2.0], [3.0, 4.0]];
1536        let err = percentile(&a.view(), -1.0, Some(Axis(0))).expect_err("must reject negative q");
1537        assert!(err.contains("between 0 and 100"));
1538
1539        let err2 = percentile(&a.view(), 101.0, Some(Axis(1))).expect_err("must reject q > 100");
1540        assert!(err2.contains("between 0 and 100"));
1541    }
1542
1543    #[test]
1544    fn test_percentile_axis_out_of_bounds() {
1545        let a = array![[1.0_f64, 2.0], [3.0, 4.0]];
1546        let err =
1547            percentile(&a.view(), 50.0, Some(Axis(5))).expect_err("must reject out-of-bounds axis");
1548        assert!(err.contains("out of bounds"));
1549    }
1550}