1use polars_core::chunked_array::ops::row_encode::_get_rows_encoded_ca;
2use polars_core::prelude::arity::unary_elementwise_values;
3use polars_core::prelude::*;
4use polars_core::series::IsSorted;
5use polars_core::with_match_physical_numeric_polars_type;
6#[cfg(feature = "hash")]
7use polars_utils::aliases::PlSeedableRandomStateQuality;
8use polars_utils::total_ord::TotalOrd;
9
10use crate::series::ops::SeriesSealed;
11
12pub trait SeriesMethods: SeriesSealed {
13 fn value_counts(
16 &self,
17 sort: bool,
18 parallel: bool,
19 name: PlSmallStr,
20 normalize: bool,
21 ) -> PolarsResult<DataFrame> {
22 let s = self.as_series();
23 polars_ensure!(
24 s.name() != &name,
25 Duplicate: "using `value_counts` on a column/series named '{}' would lead to duplicate \
26 column names; change `name` to fix", name,
27 );
28 let groups = s.group_tuples(parallel, sort)?;
29 let values = unsafe { s.agg_first(&groups) }
30 .with_name(s.name().clone())
31 .into();
32 let counts = groups.group_count().with_name(name.clone());
33
34 let counts = if normalize {
35 let len = s.len() as f64;
36 let counts: Float64Chunked =
37 unary_elementwise_values(&counts, |count| count as f64 / len);
38 counts.into_column()
39 } else {
40 counts.into_column()
41 };
42
43 let height = counts.len();
44 let cols = vec![values, counts];
45 let df = unsafe { DataFrame::new_unchecked(height, cols) };
46 if sort {
47 df.sort(
48 [name],
49 SortMultipleOptions::default()
50 .with_order_descending(true)
51 .with_multithreaded(parallel),
52 )
53 } else {
54 Ok(df)
55 }
56 }
57
58 #[cfg(feature = "hash")]
59 fn hash(&self, build_hasher: PlSeedableRandomStateQuality) -> UInt64Chunked {
60 let s = self.as_series();
61 let mut h = vec![];
62 s.0.vec_hash(build_hasher, &mut h).unwrap();
63 UInt64Chunked::from_vec(s.name().clone(), h)
64 }
65
66 fn ensure_sorted_arg(&self, operation: &str) -> PolarsResult<()> {
67 polars_ensure!(
68 self.is_sorted(SortOptions::default())?,
69 InvalidOperation: "argument in operation '{}' is not sorted, please sort the 'expr/series/column' first",
70 operation
71 );
72 Ok(())
73 }
74
75 fn is_sorted(&self, options: SortOptions) -> PolarsResult<bool> {
79 is_sorted_impl(self.as_series(), options)
80 }
81
82 fn is_sorted_any(
83 &self,
84 descending: Option<bool>,
85 nulls_last: Option<bool>,
86 ) -> PolarsResult<bool> {
87 let s = self.as_series();
88 let (descending, nulls_last) = resolve_sort_options(s, descending, nulls_last)?;
89 let options = SortOptions {
92 descending: descending.unwrap_or(false),
93 nulls_last: nulls_last.unwrap_or(false),
94 ..Default::default()
95 };
96 is_sorted_impl(s, options)
97 }
98}
99
100fn row_encode_nested(
104 s: &Series,
105 descending: bool,
106 nulls_last: bool,
107) -> PolarsResult<Option<Series>> {
108 match s.dtype() {
109 DataType::List(_) => {},
110 #[cfg(feature = "dtype-array")]
111 DataType::Array(..) => {},
112 #[cfg(feature = "dtype-struct")]
113 DataType::Struct(_) => {},
114 #[cfg(feature = "dtype-map")]
115 DataType::Map(..) => {},
116 _ => return Ok(None),
117 }
118 let encoded = _get_rows_encoded_ca(
119 PlSmallStr::EMPTY,
120 &[s.clone().into()],
121 &[descending],
122 &[nulls_last],
123 false,
124 )?;
125 Ok(Some(encoded.into_series()))
126}
127
128fn is_sorted_impl(s: &Series, options: SortOptions) -> PolarsResult<bool> {
129 let null_count = s.null_count();
130
131 if (options.descending
132 && (options.nulls_last || null_count == 0)
133 && matches!(s.is_sorted_flag(), IsSorted::Descending))
134 || (!options.descending
135 && (!options.nulls_last || null_count == 0)
136 && matches!(s.is_sorted_flag(), IsSorted::Ascending))
137 {
138 return Ok(true);
139 }
140
141 if let Some(encoded) = row_encode_nested(s, options.descending, options.nulls_last)? {
142 let options = SortOptions {
143 descending: false,
144 nulls_last: false,
145 ..options
146 };
147 return is_sorted_impl(&encoded, options);
148 }
149
150 let s_len = s.len();
151 if null_count == s_len {
152 return Ok(true);
154 }
155 if null_count > 0 {
157 if options.nulls_last {
158 if s.slice((s_len - null_count) as i64, null_count)
159 .null_count()
160 != null_count
161 {
162 return Ok(false);
163 }
164 } else if s.slice(0, null_count).null_count() != null_count {
165 return Ok(false);
166 }
167 }
168
169 if s.dtype().is_primitive_numeric() {
170 with_match_physical_numeric_polars_type!(s.dtype(), |$T| {
171 let ca: &ChunkedArray<$T> = s.as_ref().as_ref().as_ref();
172 return Ok(is_sorted_ca_num::<$T>(ca, options))
173 })
174 }
175
176 let non_null_len = s_len - null_count;
186 if non_null_len <= 1 {
187 return Ok(true);
188 }
189
190 let offset = (!options.nulls_last as i64) * (null_count as i64);
191 let non_null = s.slice(offset, non_null_len);
192 debug_assert_eq!(
193 non_null.null_count(),
194 0,
195 "internal error: `is_sorted` non-null slice contains nulls"
196 );
197
198 #[cfg(feature = "dtype-categorical")]
199 if matches!(non_null.dtype(), DataType::Categorical(_, _)) {
200 return is_sorted_categorical_lexical_adjacent(&non_null, options);
201 }
202
203 let phys = non_null.to_physical_repr();
204 let s_phys = phys.as_ref();
205 if s_phys.dtype().is_primitive_numeric() {
206 with_match_physical_numeric_polars_type!(s_phys.dtype(), |$T| {
207 let ca: &ChunkedArray<$T> = s_phys.as_ref().as_ref().as_ref();
208 return Ok(is_sorted_ca_num::<$T>(ca, options))
209 })
210 }
211
212 match s_phys.dtype() {
213 DataType::Boolean => {
214 let ca = s_phys.bool()?;
215 Ok(is_sorted_ca_bool(ca, options.descending))
216 },
217 DataType::String => {
218 let ca = s_phys.str()?;
219 Ok(is_sorted_adjacent_total_ord(
220 ca.no_null_iter(),
221 options.descending,
222 ))
223 },
224 DataType::Binary => {
225 let ca = s_phys.binary()?;
226 Ok(is_sorted_adjacent_total_ord(
227 ca.no_null_iter(),
228 options.descending,
229 ))
230 },
231 DataType::BinaryOffset => {
232 let ca = s_phys.binary_offset()?;
233 Ok(is_sorted_adjacent_total_ord(
234 ca.no_null_iter(),
235 options.descending,
236 ))
237 },
238 _ => {
239 let cmp_len = non_null_len - 1;
241 let s1 = non_null.slice(0, cmp_len);
242 let s2 = non_null.slice(1, cmp_len);
243 let cmp_op = if options.descending {
244 Series::gt_eq
245 } else {
246 Series::lt_eq
247 };
248 Ok(cmp_op(&s1, &s2)?.all())
249 },
250 }
251}
252
253fn is_sorted_adjacent_total_ord<T: TotalOrd>(
259 it: impl Iterator<Item = T>,
260 descending: bool,
261) -> bool {
262 let mut it = it;
263 let Some(mut prev) = it.next() else {
265 return true;
266 };
267 if descending {
268 for v in it {
269 if !prev.tot_ge(&v) {
270 return false;
271 }
272 prev = v;
273 }
274 } else {
275 for v in it {
276 if !prev.tot_le(&v) {
277 return false;
278 }
279 prev = v;
280 }
281 }
282 true
283}
284
285#[cfg(feature = "dtype-categorical")]
289fn is_sorted_categorical_lexical_adjacent(s: &Series, options: SortOptions) -> PolarsResult<bool> {
290 polars_ensure!(
291 matches!(s.dtype(), DataType::Categorical(_, _)),
292 ComputeError: "internal error: expected Categorical in lexical `is_sorted` path",
293 );
294
295 with_match_categorical_physical_type!(s.dtype().cat_physical().unwrap(), |$C| {
296 let ca = s.cat::<$C>()?;
297
298 Ok(is_sorted_adjacent_total_ord(
300 ca.iter_str().map(|opt| {
301 opt.expect(
302 "`iter_str` produced None while categorical null_count reported 0 (`is_sorted`)"
303 )
304 }),
305 options.descending,
306 ))
307 })
308}
309
310fn is_sorted_ca_bool(ca: &BooleanChunked, descending: bool) -> bool {
318 let len = ca.len();
319 if len <= 1 {
320 return true;
321 }
322 debug_assert_eq!(
323 ca.null_count(),
324 0,
325 "internal error: `is_sorted_ca_bool` expects a non-null boolean slice"
326 );
327 if descending {
328 let Some(idx) = ca.first_false_idx() else {
329 return true;
330 };
331 !ca.slice(idx as i64, ca.len() - idx).any()
332 } else {
333 let Some(idx) = ca.first_true_idx() else {
334 return true;
335 };
336 ca.slice(idx as i64, ca.len() - idx).all()
337 }
338}
339
340pub fn resolve_sort_options(
352 s: &Series,
353 descending: Option<bool>,
354 nulls_last: Option<bool>,
355) -> PolarsResult<(Option<bool>, Option<bool>)> {
356 let nulls_last = match nulls_last {
357 Some(n) => Some(n),
358 None => infer_nulls_last(s),
359 };
360
361 let descending = match descending {
362 Some(d) => Some(d),
363 None => infer_descending(s, nulls_last.unwrap_or(false))?,
364 };
365
366 Ok((descending, nulls_last))
367}
368
369fn infer_nulls_last(s: &Series) -> Option<bool> {
373 let null_count = s.null_count();
374 let s_len = s.len();
375
376 if null_count == 0 || null_count == s_len {
377 return None;
378 }
379
380 if s.slice((s_len - null_count) as i64, null_count)
381 .null_count()
382 == null_count
383 {
384 Some(true)
385 } else if s.slice(0, null_count).null_count() == null_count {
386 Some(false)
387 } else {
388 None
389 }
390}
391
392fn infer_descending(s: &Series, nulls_last: bool) -> PolarsResult<Option<bool>> {
393 let null_count = s.null_count();
394 let non_null_len = s.len() - null_count;
395 if non_null_len < 2 {
396 return Ok(None);
397 }
398
399 let non_null_start = if nulls_last { 0 } else { null_count };
400 let non_null = s.slice(non_null_start as i64, non_null_len);
401 let non_null = row_encode_nested(&non_null, false, nulls_last)?.unwrap_or(non_null);
402
403 let a = non_null.slice(0, non_null_len - 1);
404 let b = non_null.slice(1, non_null_len - 1);
405
406 let lt = a.lt(&b)?;
407 let gt = a.gt(&b)?;
408
409 let lt_first = lt.iter().position(|v| v == Some(true));
410 let gt_first = gt.iter().position(|v| v == Some(true));
411
412 Ok(match (lt_first, gt_first) {
413 (None, None) => None,
414 (Some(_), None) => Some(false),
415 (None, Some(_)) => Some(true),
416 (Some(l), Some(g)) => Some(g < l),
417 })
418}
419
420fn check_cmp<T: NumericNative, Cmp: Fn(&T, &T) -> bool>(
421 vals: &[T],
422 f: Cmp,
423 previous: &mut T,
424) -> bool {
425 let mut sorted = true;
426 for c in vals.chunks(1024) {
427 for v in c {
428 sorted &= f(previous, v);
429 *previous = *v;
430 }
431 if !sorted {
432 return false;
433 }
434 }
435 sorted
436}
437
438fn is_sorted_ca_num<T: PolarsNumericType>(ca: &ChunkedArray<T>, options: SortOptions) -> bool {
439 if let Ok(vals) = ca.cont_slice() {
440 let Some(mut previous) = vals.first().copied() else {
441 return true;
442 };
443 return if options.descending {
444 check_cmp(vals, |prev, c| prev.tot_ge(c), &mut previous)
445 } else {
446 check_cmp(vals, |prev, c| prev.tot_le(c), &mut previous)
447 };
448 };
449
450 if ca.null_count() == 0 {
451 let Some(mut previous) = ca
452 .downcast_iter()
453 .find_map(|arr| arr.values().first().copied())
454 else {
455 return true;
456 };
457 for arr in ca.downcast_iter() {
458 let vals = arr.values();
459 let sorted = if options.descending {
460 check_cmp(vals, |prev, c| prev.tot_ge(c), &mut previous)
461 } else {
462 check_cmp(vals, |prev, c| prev.tot_le(c), &mut previous)
463 };
464 if !sorted {
465 return false;
466 }
467 }
468 return true;
469 };
470
471 let null_count = ca.null_count();
472 if options.nulls_last {
473 let ca = ca.slice(0, ca.len() - null_count);
474 is_sorted_ca_num(&ca, options)
475 } else {
476 let ca = ca.slice(null_count as i64, ca.len() - null_count);
477 is_sorted_ca_num(&ca, options)
478 }
479}
480
481impl SeriesMethods for Series {}