1use ::ndarray::{Array, ArrayView, Axis, Dimension, Ix1, Ix2};
7use num_traits::{Float, FromPrimitive};
8
9#[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 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 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 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#[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 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 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 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#[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#[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 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 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 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 let global_mean = mean_2d(array, None)?[0];
393
394 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#[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 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 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 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#[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 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 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 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#[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 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 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 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#[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 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 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 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#[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 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 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 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 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 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 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#[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 let ndim = array.ndim();
892 if ax.index() >= ndim {
893 return Err("Axis index out of bounds");
894 }
895
896 let mut outputshape = array.shape().to_vec();
898 outputshape.remove(ax.index());
899
900 if outputshape.is_empty() {
902 outputshape.push(1);
903 }
904
905 let result = array
907 .mean_axis(ax)
908 .ok_or("Failed to compute mean along axis")?;
909
910 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 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#[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 let ndim = array.ndim();
967 if ax.index() >= ndim {
968 return Err("Axis index out of bounds");
969 }
970
971 let two = T::from_f64(2.0).ok_or("Cannot convert 2.0 into element type")?;
975
976 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 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 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#[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 let ndim = array.ndim();
1055 if ax.index() >= ndim {
1056 return Err("Axis index out of bounds");
1057 }
1058
1059 let mut outputshape = array.shape().to_vec();
1061 outputshape.remove(ax.index());
1062
1063 if outputshape.is_empty() {
1065 outputshape.push(1);
1066 }
1067
1068 let result = array.var_axis(ax, T::from_usize(ddof).expect("Operation failed"));
1070
1071 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 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 let global_mean = mean(array, None)?[0];
1088
1089 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#[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#[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 let ndim = array.ndim();
1164 if ax.index() >= ndim {
1165 return Err("Axis index out of bounds");
1166 }
1167
1168 let mut outputshape = array.shape().to_vec();
1170 outputshape.remove(ax.index());
1171
1172 if outputshape.is_empty() {
1174 outputshape.push(1);
1175 }
1176
1177 let result = array.fold_axis(ax, T::infinity(), |&a, &b| if a < b { a } else { b });
1179
1180 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 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#[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 let ndim = array.ndim();
1230 if ax.index() >= ndim {
1231 return Err("Axis index out of bounds");
1232 }
1233
1234 let mut outputshape = array.shape().to_vec();
1236 outputshape.remove(ax.index());
1237
1238 if outputshape.is_empty() {
1240 outputshape.push(1);
1241 }
1242
1243 let result = array.fold_axis(ax, T::neg_infinity(), |&a, &b| if a > b { a } else { b });
1245
1246 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 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#[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 let ndim = array.ndim();
1309 if ax.index() >= ndim {
1310 return Err("Axis index out of bounds");
1311 }
1312
1313 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 return T::zero();
1335 }
1336
1337 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 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 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 #[test]
1402 fn test_median_2d_axis0_columns() {
1403 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 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 assert!(approx_eq(result[0], 2.5));
1430 assert!(approx_eq(result[1], 25.0));
1432 }
1433
1434 #[test]
1435 fn test_median_3d_each_axis() {
1436 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 let med0 = median(&a.view(), Some(Axis(0))).expect("axis median");
1450 assert_eq!(med0.len(), 12);
1451 assert!(approx_eq(med0[0], 6.0));
1453
1454 let med1 = median(&a.view(), Some(Axis(1))).expect("axis median");
1456 assert_eq!(med1.len(), 8);
1457
1458 let med2 = median(&a.view(), Some(Axis(2))).expect("axis median");
1460 assert_eq!(med2.len(), 6);
1461 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 #[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 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 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 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 a[[i, j, k]] = k as f64;
1520 }
1521 }
1522 }
1523 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 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}