polars_core/chunked_array/
arg_min_max.rs1use polars_arrow::array::Array;
2use polars_utils::arg_min_max::ArgMinMax;
3use polars_utils::min_max::{MaxIgnoreNan, MinIgnoreNan, MinMaxPolicy};
4
5use crate::chunked_array::ChunkedArray;
6use crate::chunked_array::ops::float_sorted_arg_max::{
7 float_arg_max_sorted_ascending, float_arg_max_sorted_descending,
8};
9use crate::datatypes::{
10 BinaryChunked, BinaryOffsetChunked, BooleanChunked, PolarsDataType, PolarsNumericType,
11 StringChunked,
12};
13#[cfg(feature = "dtype-categorical")]
14use crate::datatypes::{CategoricalChunked, PolarsCategoricalType};
15use crate::series::IsSorted;
16
17pub fn arg_min_opt_iter<T, I>(iter: I) -> Option<usize>
18where
19 I: IntoIterator<Item = Option<T>>,
20 T: Ord,
21{
22 iter.into_iter()
23 .enumerate()
24 .flat_map(|(idx, val)| Some((idx, val?)))
25 .min_by(|x, y| Ord::cmp(&x.1, &y.1))
26 .map(|x| x.0)
27}
28
29pub fn arg_max_opt_iter<T, I>(iter: I) -> Option<usize>
30where
31 I: IntoIterator<Item = Option<T>>,
32 T: Ord,
33{
34 iter.into_iter()
35 .enumerate()
36 .flat_map(|(idx, val)| Some((idx, val?)))
37 .max_by(|x, y| Ord::cmp(&x.1, &y.1))
38 .map(|x| x.0)
39}
40
41pub fn arg_min_numeric<T>(ca: &ChunkedArray<T>) -> Option<usize>
42where
43 T: PolarsNumericType,
44 for<'b> &'b [T::Native]: ArgMinMax,
45{
46 if ca.null_count() == ca.len() {
47 None
48 } else if let Ok(vals) = ca.cont_slice() {
49 arg_min_numeric_slice(vals, ca.is_sorted_flag())
50 } else {
51 arg_min_numeric_chunked(ca)
52 }
53}
54
55pub fn arg_max_numeric<T>(ca: &ChunkedArray<T>) -> Option<usize>
56where
57 T: PolarsNumericType,
58 for<'b> &'b [T::Native]: ArgMinMax,
59{
60 if ca.null_count() == ca.len() {
61 None
62 } else if T::get_static_dtype().is_float() && !matches!(ca.is_sorted_flag(), IsSorted::Not) {
63 arg_max_float_sorted(ca)
64 } else if let Ok(vals) = ca.cont_slice() {
65 arg_max_numeric_slice(vals, ca.is_sorted_flag())
66 } else {
67 arg_max_numeric_chunked(ca)
68 }
69}
70
71fn arg_max_float_sorted<T>(ca: &ChunkedArray<T>) -> Option<usize>
74where
75 T: PolarsNumericType,
76{
77 let out = match ca.is_sorted_flag() {
78 IsSorted::Ascending => float_arg_max_sorted_ascending(ca),
79 IsSorted::Descending => float_arg_max_sorted_descending(ca),
80 _ => unreachable!(),
81 };
82 Some(out)
83}
84
85#[cfg(feature = "dtype-categorical")]
86pub fn arg_min_cat<T: PolarsCategoricalType>(ca: &CategoricalChunked<T>) -> Option<usize> {
87 if ca.null_count() == ca.len() {
88 return None;
89 }
90 arg_min_opt_iter(ca.iter_str())
91}
92
93#[cfg(feature = "dtype-categorical")]
94pub fn arg_max_cat<T: PolarsCategoricalType>(ca: &CategoricalChunked<T>) -> Option<usize> {
95 if ca.null_count() == ca.len() {
96 return None;
97 }
98 arg_max_opt_iter(ca.iter_str())
99}
100
101pub fn arg_min_bool(ca: &BooleanChunked) -> Option<usize> {
102 ca.first_false_idx().or_else(|| ca.first_true_idx())
103}
104
105pub fn arg_max_bool(ca: &BooleanChunked) -> Option<usize> {
106 ca.first_true_idx().or_else(|| ca.first_false_idx())
107}
108
109pub fn arg_min_str(ca: &StringChunked) -> Option<usize> {
110 arg_min_physical_generic(ca)
111}
112
113pub fn arg_max_str(ca: &StringChunked) -> Option<usize> {
114 arg_max_physical_generic(ca)
115}
116
117pub fn arg_min_binary(ca: &BinaryChunked) -> Option<usize> {
118 arg_min_physical_generic(ca)
119}
120
121pub fn arg_max_binary(ca: &BinaryChunked) -> Option<usize> {
122 arg_max_physical_generic(ca)
123}
124
125pub fn arg_min_binary_offset(ca: &BinaryOffsetChunked) -> Option<usize> {
126 arg_min_physical_generic(ca)
127}
128
129pub fn arg_max_binary_offset(ca: &BinaryOffsetChunked) -> Option<usize> {
130 arg_max_physical_generic(ca)
131}
132
133fn arg_min_physical_generic<T>(ca: &ChunkedArray<T>) -> Option<usize>
134where
135 T: PolarsDataType,
136 for<'a> T::Physical<'a>: Ord,
137{
138 if ca.null_count() == ca.len() {
139 return None;
140 }
141 match ca.is_sorted_flag() {
142 IsSorted::Ascending => ca.first_non_null(),
143 IsSorted::Descending => ca.last_non_null(),
144 IsSorted::Not => arg_min_opt_iter(ca.iter()),
145 }
146}
147
148fn arg_max_physical_generic<T>(ca: &ChunkedArray<T>) -> Option<usize>
149where
150 T: PolarsDataType,
151 for<'a> T::Physical<'a>: Ord,
152{
153 if ca.null_count() == ca.len() {
154 return None;
155 }
156 match ca.is_sorted_flag() {
157 IsSorted::Ascending => ca.last_non_null(),
158 IsSorted::Descending => ca.first_non_null(),
159 IsSorted::Not => arg_max_opt_iter(ca.iter()),
160 }
161}
162
163fn arg_min_numeric_chunked<'a, T>(ca: &'a ChunkedArray<T>) -> Option<usize>
164where
165 T: PolarsNumericType,
166 for<'b> &'b [T::Native]: ArgMinMax,
167{
168 match ca.is_sorted_flag() {
169 IsSorted::Ascending => ca.first_non_null(),
170 IsSorted::Descending => ca.last_non_null(),
171 IsSorted::Not => {
172 let mut chunk_start_offset = 0;
173 let mut min_idx: Option<usize> = None;
174 let mut min_val: Option<T::Native> = None;
175 for arr in ca.downcast_iter() {
176 if arr.len() == arr.null_count() {
177 chunk_start_offset += arr.len();
178 continue;
179 }
180
181 let chunk_min: Option<(usize, T::Native)> = if arr.null_count() > 0 {
182 let (validity, offset, _) = arr.validity().unwrap().as_slice();
183 arr.values()
184 .as_slice()
185 .argmin_masked(validity, offset)
186 .map(|idx| (idx, arr.value(idx)))
187 } else {
188 let min_idx: usize = arr.values().as_slice().argmin();
190 Some((min_idx, arr.value(min_idx)))
191 };
192
193 if let Some((chunk_min_idx, chunk_min_val)) = chunk_min {
194 if min_val.is_none()
195 || MinIgnoreNan::is_better(&chunk_min_val, &min_val.unwrap())
196 {
197 min_val = Some(chunk_min_val);
198 min_idx = Some(chunk_start_offset + chunk_min_idx);
199 }
200 }
201 chunk_start_offset += arr.len();
202 }
203 min_idx
204 },
205 }
206}
207
208fn arg_max_numeric_chunked<'a, T>(ca: &'a ChunkedArray<T>) -> Option<usize>
209where
210 T: PolarsNumericType,
211 for<'b> &'b [T::Native]: ArgMinMax,
212{
213 match ca.is_sorted_flag() {
214 IsSorted::Ascending => ca.last_non_null(),
215 IsSorted::Descending => ca.first_non_null(),
216 IsSorted::Not => {
217 let mut chunk_start_offset = 0;
218 let mut max_idx: Option<usize> = None;
219 let mut max_val: Option<T::Native> = None;
220 for arr in ca.downcast_iter() {
221 if arr.len() == arr.null_count() {
222 chunk_start_offset += arr.len();
223 continue;
224 }
225
226 let chunk_max: Option<(usize, T::Native)> = if arr.null_count() > 0 {
227 let (validity, offset, _) = arr.validity().unwrap().as_slice();
228 arr.values()
229 .as_slice()
230 .argmax_masked(validity, offset)
231 .map(|idx| (idx, arr.value(idx)))
232 } else {
233 let max_idx: usize = arr.values().as_slice().argmax();
235 Some((max_idx, arr.value(max_idx)))
236 };
237
238 if let Some((chunk_max_idx, chunk_max_val)) = chunk_max {
239 if max_val.is_none()
240 || MaxIgnoreNan::is_better(&chunk_max_val, &max_val.unwrap())
241 {
242 max_val = Some(chunk_max_val);
243 max_idx = Some(chunk_start_offset + chunk_max_idx);
244 }
245 }
246 chunk_start_offset += arr.len();
247 }
248 max_idx
249 },
250 }
251}
252
253fn arg_min_numeric_slice<T>(vals: &[T], is_sorted: IsSorted) -> Option<usize>
254where
255 for<'a> &'a [T]: ArgMinMax,
256{
257 match is_sorted {
258 IsSorted::Ascending => Some(0),
260 IsSorted::Descending => Some(vals.len() - 1),
262 IsSorted::Not => Some(vals.argmin()), }
264}
265
266fn arg_max_numeric_slice<T>(vals: &[T], is_sorted: IsSorted) -> Option<usize>
267where
268 for<'a> &'a [T]: ArgMinMax,
269{
270 match is_sorted {
271 IsSorted::Ascending => Some(vals.len() - 1),
273 IsSorted::Descending => Some(0),
275 IsSorted::Not => Some(vals.argmax()), }
277}