1mod agg_list;
2mod boolean;
3#[cfg(feature = "dtype-categorical")]
4mod categorical;
5mod dispatch;
6mod string;
7
8use std::borrow::Cow;
9
10pub use agg_list::*;
11use arrow::bitmap::{Bitmap, MutableBitmap};
12use arrow::legacy::kernels::take_agg::*;
13use arrow::legacy::trusted_len::TrustedLenPush;
14use arrow::types::NativeType;
15use num_traits::pow::Pow;
16use num_traits::{Bounded, Float, Num, NumCast, ToPrimitive, Zero};
17use polars_compute::rolling::no_nulls::{
18 MaxWindow, MinWindow, MomentWindow, QuantileWindow, RollingAggWindowNoNulls,
19};
20use polars_compute::rolling::nulls::{RollingAggWindowNulls, VarianceMoment};
21use polars_compute::rolling::quantile_filter::SealedRolling;
22use polars_compute::rolling::{
23 self, ArgMaxWindow, ArgMinWindow, MeanWindow, QuantileMethod, RollingFnParams,
24 RollingQuantileParams, RollingVarParams, SumWindow, quantile_filter, rolling_argmax_by,
25 rolling_argmin_by,
26};
27use polars_utils::arg_min_max::ArgMinMax;
28use polars_utils::float::IsFloat;
29#[cfg(feature = "dtype-f16")]
30use polars_utils::float16::pf16;
31use polars_utils::idx_vec::IdxVec;
32use polars_utils::kahan_sum::KahanSum;
33use polars_utils::min_max::MinMax;
34use rayon::prelude::*;
35
36use crate::chunked_array::cast::CastOptions;
37#[cfg(feature = "object")]
38use crate::chunked_array::object::extension::create_extension;
39use crate::chunked_array::{arg_max_numeric, arg_min_numeric};
40#[cfg(feature = "object")]
41use crate::frame::group_by::GroupsIndicator;
42use crate::prelude::*;
43use crate::runtime::RAYON;
44use crate::series::IsSorted;
45use crate::series::implementations::SeriesWrap;
46use crate::utils::NoNull;
47
48fn idx2usize(idx: &[IdxSize]) -> impl ExactSizeIterator<Item = usize> + '_ {
49 idx.iter().map(|i| *i as usize)
50}
51
52pub fn _use_rolling_kernels(
59 groups: &GroupsSlice,
60 overlapping: bool,
61 monotonic: bool,
62 chunks: &[ArrayRef],
63) -> bool {
64 match groups.len() {
65 0 | 1 => false,
66 _ => overlapping && monotonic && chunks.len() == 1,
67 }
68}
69
70pub fn rolling_numeric_minmax_by(by_col: &Column, slices: &GroupsSlice, is_max_by: bool) -> IdxCa {
75 let dtype = by_col.dtype();
76 let by_series = by_col.as_materialized_series().rechunk();
77 let by_phys = by_series.to_physical_repr();
78 let phys_dtype = by_phys.dtype();
79
80 assert!(
81 phys_dtype.is_primitive_numeric() && !dtype.is_categorical(),
82 "rolling_numeric_minmax_by requires a numeric by column, got {dtype}",
83 );
84
85 let starts: Vec<IdxSize> = slices.iter().map(|s| s[0]).collect();
86 let ends: Vec<IdxSize> = slices.iter().map(|s| s[0] + s[1]).collect();
87
88 let arr = with_match_physical_numeric_polars_type!(phys_dtype, |$T| {
89 let ca: &ChunkedArray<$T> = by_phys.as_ref().as_ref().as_ref();
90 let arr = ca.downcast_as_array();
91 let values = arr.values().as_slice();
92 let validity = arr.validity();
93
94 if is_max_by {
95 rolling_argmax_by(values, validity, &starts, &ends, 1)
96 } else {
97 rolling_argmin_by(values, validity, &starts, &ends, 1)
98 }
99 });
100
101 IdxCa::with_chunk(PlSmallStr::EMPTY, arr)
102}
103
104pub fn _rolling_apply_agg_window_nulls<Agg, T, O, Out>(
106 values: &[T],
107 validity: &Bitmap,
108 offsets: O,
109 params: Option<RollingFnParams>,
110) -> PrimitiveArray<Out>
111where
112 O: Iterator<Item = (IdxSize, IdxSize)> + TrustedLen,
113 Agg: RollingAggWindowNulls<T, Out>,
114 T: IsFloat + NativeType,
115 Out: NativeType,
116{
117 let output_len = offsets.size_hint().0;
120 let mut agg_window = Agg::new(values, validity, 0, 0, params, None);
122
123 let mut validity = MutableBitmap::with_capacity(output_len);
124 validity.extend_constant(output_len, true);
125
126 let out = offsets
127 .enumerate()
128 .map(|(idx, (start, len))| {
129 let end = start + len;
130
131 unsafe { agg_window.update(start as usize, end as usize) };
134 match agg_window.get_agg(idx) {
135 Some(val) => val,
136 None => {
137 unsafe { validity.set_unchecked(idx, false) };
139 Out::default()
140 },
141 }
142 })
143 .collect_trusted::<Vec<_>>();
144
145 PrimitiveArray::new(Out::PRIMITIVE.into(), out.into(), Some(validity.into()))
146}
147
148pub fn _rolling_apply_agg_window_no_nulls<Agg, T, O, Out>(
150 values: &[T],
151 offsets: O,
152 params: Option<RollingFnParams>,
153) -> PrimitiveArray<Out>
154where
155 Agg: RollingAggWindowNoNulls<T, Out>,
157 O: Iterator<Item = (IdxSize, IdxSize)> + TrustedLen,
158 T: IsFloat + NativeType,
159 Out: NativeType,
160{
161 let mut agg_window = Agg::new(values, 0, 0, params, None);
163
164 offsets
165 .enumerate()
166 .map(|(idx, (start, len))| {
167 let end = start + len;
168
169 unsafe { agg_window.update(start as usize, end as usize) };
171 agg_window.get_agg(idx)
172 })
173 .collect::<PrimitiveArray<Out>>()
174}
175
176pub fn _slice_from_offsets<T>(ca: &ChunkedArray<T>, first: IdxSize, len: IdxSize) -> ChunkedArray<T>
177where
178 T: PolarsDataType,
179{
180 ca.slice(first as i64, len as usize)
181}
182
183pub fn _agg_helper_idx<T, F>(groups: &GroupsIdx, f: F) -> Series
185where
186 F: Fn((IdxSize, &IdxVec)) -> Option<T::Native> + Send + Sync,
187 T: PolarsNumericType,
188{
189 let ca: ChunkedArray<T> = RAYON.install(|| groups.into_par_iter().map(f).collect());
190 ca.into_series()
191}
192
193pub fn _agg_helper_idx_no_null<T, F>(groups: &GroupsIdx, f: F) -> Series
195where
196 F: Fn((IdxSize, &IdxVec)) -> T::Native + Send + Sync,
197 T: PolarsNumericType,
198{
199 let ca: NoNull<ChunkedArray<T>> = RAYON.install(|| groups.into_par_iter().map(f).collect());
200 ca.into_inner().into_series()
201}
202
203fn agg_helper_idx_on_all<T, F>(groups: &GroupsIdx, f: F) -> Series
206where
207 F: Fn(&IdxVec) -> Option<T::Native> + Send + Sync,
208 T: PolarsNumericType,
209{
210 let ca: ChunkedArray<T> = RAYON.install(|| groups.all().into_par_iter().map(f).collect());
211 ca.into_series()
212}
213
214pub fn _agg_helper_slice<T, F>(groups: &[[IdxSize; 2]], f: F) -> Series
215where
216 F: Fn([IdxSize; 2]) -> Option<T::Native> + Send + Sync,
217 T: PolarsNumericType,
218{
219 let ca: ChunkedArray<T> = RAYON.install(|| groups.par_iter().copied().map(f).collect());
220 ca.into_series()
221}
222
223pub fn _agg_helper_idx_idx<'a, F>(groups: &'a GroupsIdx, f: F) -> Series
224where
225 F: Fn((IdxSize, &'a IdxVec)) -> Option<IdxSize> + Send + Sync,
226{
227 let ca: IdxCa = RAYON.install(|| groups.into_par_iter().map(f).collect());
228 ca.into_series()
229}
230
231pub fn _agg_helper_slice_idx<F>(groups: &[[IdxSize; 2]], f: F) -> Series
232where
233 F: Fn([IdxSize; 2]) -> Option<IdxSize> + Send + Sync,
234{
235 let ca: IdxCa = RAYON.install(|| groups.par_iter().copied().map(f).collect());
236 ca.into_series()
237}
238
239pub fn _agg_helper_slice_no_null<T, F>(groups: &[[IdxSize; 2]], f: F) -> Series
240where
241 F: Fn([IdxSize; 2]) -> T::Native + Send + Sync,
242 T: PolarsNumericType,
243{
244 let ca: NoNull<ChunkedArray<T>> = RAYON.install(|| groups.par_iter().copied().map(f).collect());
245 ca.into_inner().into_series()
246}
247
248trait QuantileDispatcher<K> {
252 fn _quantile(self, quantile: f64, method: QuantileMethod) -> PolarsResult<Option<K>>;
253
254 fn _median(self) -> Option<K>;
255}
256
257impl<T> QuantileDispatcher<f64> for ChunkedArray<T>
258where
259 T: PolarsIntegerType,
260 T::Native: Ord,
261{
262 fn _quantile(self, quantile: f64, method: QuantileMethod) -> PolarsResult<Option<f64>> {
263 self.quantile_faster(quantile, method)
264 }
265 fn _median(self) -> Option<f64> {
266 self.median_faster()
267 }
268}
269
270#[cfg(feature = "dtype-f16")]
271impl QuantileDispatcher<pf16> for Float16Chunked {
272 fn _quantile(self, quantile: f64, method: QuantileMethod) -> PolarsResult<Option<pf16>> {
273 self.quantile_faster(quantile, method)
274 }
275 fn _median(self) -> Option<pf16> {
276 self.median_faster()
277 }
278}
279
280impl QuantileDispatcher<f32> for Float32Chunked {
281 fn _quantile(self, quantile: f64, method: QuantileMethod) -> PolarsResult<Option<f32>> {
282 self.quantile_faster(quantile, method)
283 }
284 fn _median(self) -> Option<f32> {
285 self.median_faster()
286 }
287}
288impl QuantileDispatcher<f64> for Float64Chunked {
289 fn _quantile(self, quantile: f64, method: QuantileMethod) -> PolarsResult<Option<f64>> {
290 self.quantile_faster(quantile, method)
291 }
292 fn _median(self) -> Option<f64> {
293 self.median_faster()
294 }
295}
296
297unsafe fn agg_quantile_generic<T, K>(
298 ca: &ChunkedArray<T>,
299 groups: &GroupsType,
300 quantile: f64,
301 method: QuantileMethod,
302) -> Series
303where
304 T: PolarsNumericType,
305 ChunkedArray<T>: QuantileDispatcher<K::Native>,
306 K: PolarsNumericType,
307 <K as datatypes::PolarsNumericType>::Native: num_traits::Float + quantile_filter::SealedRolling,
308{
309 let invalid_quantile = !(0.0..=1.0).contains(&quantile);
310 if invalid_quantile {
311 return Series::full_null(ca.name().clone(), groups.len(), ca.dtype());
312 }
313 match groups {
314 GroupsType::Idx(groups) => {
315 let ca = ca.rechunk();
316 agg_helper_idx_on_all::<K, _>(groups, |idx| {
317 debug_assert!(idx.len() <= ca.len());
318 if idx.is_empty() {
319 return None;
320 }
321 let take = { ca.take_unchecked(idx) };
322 take._quantile(quantile, method).unwrap_unchecked()
324 })
325 },
326 GroupsType::Slice {
327 groups,
328 overlapping,
329 monotonic,
330 } => {
331 if _use_rolling_kernels(groups, *overlapping, *monotonic, ca.chunks()) {
332 let s = ca
334 .cast_with_options(&K::get_static_dtype(), CastOptions::Overflowing)
335 .unwrap();
336 let ca: &ChunkedArray<K> = s.as_ref().as_ref();
337 let arr = ca.downcast_iter().next().unwrap();
338 let values = arr.values().as_slice();
339 let offset_iter = groups.iter().map(|[first, len]| (*first, *len));
340 let arr = match arr.validity() {
341 None => _rolling_apply_agg_window_no_nulls::<QuantileWindow<_>, _, _, _>(
342 values,
343 offset_iter,
344 Some(RollingFnParams::Quantile(RollingQuantileParams {
345 prob: quantile,
346 method,
347 })),
348 ),
349 Some(validity) => {
350 _rolling_apply_agg_window_nulls::<rolling::nulls::QuantileWindow<_>, _, _, _>(
351 values,
352 validity,
353 offset_iter,
354 Some(RollingFnParams::Quantile(RollingQuantileParams {
355 prob: quantile,
356 method,
357 })),
358 )
359 },
360 };
361 ChunkedArray::<K>::with_chunk(PlSmallStr::EMPTY, arr).into_series()
364 } else {
365 _agg_helper_slice::<K, _>(groups, |[first, len]| {
366 debug_assert!(first + len <= ca.len() as IdxSize);
367 match len {
368 0 => None,
369 1 => ca.get(first as usize).map(|v| NumCast::from(v).unwrap()),
370 _ => {
371 let arr_group = _slice_from_offsets(ca, first, len);
372 arr_group
374 ._quantile(quantile, method)
375 .unwrap_unchecked()
376 .map(|flt| NumCast::from(flt).unwrap_unchecked())
377 },
378 }
379 })
380 }
381 },
382 }
383}
384
385unsafe fn agg_median_generic<T, K>(ca: &ChunkedArray<T>, groups: &GroupsType) -> Series
386where
387 T: PolarsNumericType,
388 ChunkedArray<T>: QuantileDispatcher<K::Native>,
389 K: PolarsNumericType,
390 <K as datatypes::PolarsNumericType>::Native: num_traits::Float + SealedRolling,
391{
392 match groups {
393 GroupsType::Idx(groups) => {
394 let ca = ca.rechunk();
395 agg_helper_idx_on_all::<K, _>(groups, |idx| {
396 debug_assert!(idx.len() <= ca.len());
397 if idx.is_empty() {
398 return None;
399 }
400 let take = { ca.take_unchecked(idx) };
401 take._median()
402 })
403 },
404 GroupsType::Slice { .. } => {
405 agg_quantile_generic::<T, K>(ca, groups, 0.5, QuantileMethod::Linear)
406 },
407 }
408}
409
410#[cfg(feature = "bitwise")]
414unsafe fn bitwise_agg<T: PolarsNumericType>(
415 ca: &ChunkedArray<T>,
416 groups: &GroupsType,
417 f: fn(&ChunkedArray<T>) -> Option<T::Native>,
418) -> Series
419where
420 ChunkedArray<T>: ChunkTakeUnchecked<[IdxSize]> + ChunkBitwiseReduce<Physical = T::Native>,
421{
422 let s = if groups.len() > 1 {
425 ca.rechunk()
426 } else {
427 Cow::Borrowed(ca)
428 };
429
430 match groups {
431 GroupsType::Idx(groups) => agg_helper_idx_on_all::<T, _>(groups, |idx| {
432 debug_assert!(idx.len() <= s.len());
433 if idx.is_empty() {
434 None
435 } else {
436 let take = unsafe { s.take_unchecked(idx) };
437 f(&take)
438 }
439 }),
440 GroupsType::Slice { groups, .. } => _agg_helper_slice::<T, _>(groups, |[first, len]| {
441 debug_assert!(len <= s.len() as IdxSize);
442 if len == 0 {
443 None
444 } else {
445 let take = _slice_from_offsets(&s, first, len);
446 f(&take)
447 }
448 }),
449 }
450}
451
452#[cfg(feature = "bitwise")]
453impl<T> ChunkedArray<T>
454where
455 T: PolarsNumericType,
456 ChunkedArray<T>: ChunkTakeUnchecked<[IdxSize]> + ChunkBitwiseReduce<Physical = T::Native>,
457{
458 pub(crate) unsafe fn agg_and(&self, groups: &GroupsType) -> Series {
462 unsafe { bitwise_agg(self, groups, ChunkBitwiseReduce::and_reduce) }
463 }
464
465 pub(crate) unsafe fn agg_or(&self, groups: &GroupsType) -> Series {
469 unsafe { bitwise_agg(self, groups, ChunkBitwiseReduce::or_reduce) }
470 }
471
472 pub(crate) unsafe fn agg_xor(&self, groups: &GroupsType) -> Series {
476 unsafe { bitwise_agg(self, groups, ChunkBitwiseReduce::xor_reduce) }
477 }
478}
479
480impl<T> ChunkedArray<T>
481where
482 T: PolarsNumericType + Sync,
483 T::Native: NativeType + PartialOrd + Num + NumCast + Zero + Bounded + std::iter::Sum<T::Native>,
484 ChunkedArray<T>: ChunkAgg<T::Native>,
485{
486 pub(crate) unsafe fn agg_min(&self, groups: &GroupsType) -> Series {
487 if !self.has_nulls() || matches!(groups, GroupsType::Slice { .. }) {
489 match self.is_sorted_flag() {
490 IsSorted::Ascending => {
491 return self.clone().into_series().agg_first_non_null(groups);
492 },
493 IsSorted::Descending => {
494 return self.clone().into_series().agg_last_non_null(groups);
495 },
496 _ => {},
497 }
498 }
499
500 match groups {
501 GroupsType::Idx(groups) => {
502 let ca = self.rechunk();
503 let arr = ca.downcast_iter().next().unwrap();
504 let no_nulls = arr.null_count() == 0;
505 _agg_helper_idx::<T, _>(groups, |(first, idx)| {
506 debug_assert!(idx.len() <= arr.len());
507 if idx.is_empty() {
508 None
509 } else if idx.len() == 1 {
510 arr.get(first as usize)
511 } else if no_nulls {
512 take_agg_no_null_primitive_iter_unchecked(arr, idx2usize(idx))
513 .reduce(|a, b| a.min_ignore_nan(b))
514 } else {
515 take_agg_primitive_iter_unchecked(arr, idx2usize(idx))
516 .reduce(|a, b| a.min_ignore_nan(b))
517 }
518 })
519 },
520 GroupsType::Slice {
521 groups: groups_slice,
522 overlapping,
523 monotonic,
524 } => {
525 if _use_rolling_kernels(groups_slice, *overlapping, *monotonic, self.chunks()) {
526 let arr = self.downcast_iter().next().unwrap();
527 let values = arr.values().as_slice();
528 let offset_iter = groups_slice.iter().map(|[first, len]| (*first, *len));
529 let arr = match arr.validity() {
530 None => _rolling_apply_agg_window_no_nulls::<MinWindow<_>, _, _, _>(
531 values,
532 offset_iter,
533 None,
534 ),
535 Some(validity) => {
536 _rolling_apply_agg_window_nulls::<rolling::nulls::MinWindow<_>, _, _, _>(
537 values,
538 validity,
539 offset_iter,
540 None,
541 )
542 },
543 };
544 Self::from(arr).into_series()
545 } else {
546 _agg_helper_slice::<T, _>(groups_slice, |[first, len]| {
547 debug_assert!(len <= self.len() as IdxSize);
548 match len {
549 0 => None,
550 1 => self.get(first as usize),
551 _ => {
552 let arr_group = _slice_from_offsets(self, first, len);
553 ChunkAgg::min(&arr_group)
554 },
555 }
556 })
557 }
558 },
559 }
560 }
561
562 pub(crate) unsafe fn agg_arg_min(&self, groups: &GroupsType) -> Series
563 where
564 for<'b> &'b [T::Native]: ArgMinMax,
565 {
566 if !self.has_nulls() || matches!(groups, GroupsType::Slice { .. }) {
567 match self.is_sorted_flag() {
568 IsSorted::Ascending => {
569 return self.clone().into_series().agg_arg_first_non_null(groups);
570 },
571 IsSorted::Descending => {
572 return self.clone().into_series().agg_arg_last_non_null(groups);
573 },
574 _ => {},
575 }
576 }
577
578 match groups {
579 GroupsType::Idx(groups) => {
580 let ca = self.rechunk();
581 let arr = ca.downcast_iter().next().unwrap();
582 let no_nulls = !arr.has_nulls();
583
584 agg_helper_idx_on_all::<IdxType, _>(groups, |idx| {
585 if idx.is_empty() {
586 return None;
587 }
588
589 if no_nulls {
590 let first_i = idx[0] as usize;
591 let mut best_pos: IdxSize = 0;
592 let mut best_val: T::Native = unsafe { arr.value_unchecked(first_i) };
593
594 for (pos, &i) in idx.iter().enumerate().skip(1) {
595 let v = unsafe { arr.value_unchecked(i as usize) };
596 if v.nan_max_lt(&best_val) {
597 best_val = v;
598 best_pos = pos as IdxSize;
599 }
600 }
601 Some(best_pos)
602 } else {
603 let (start_pos, mut best_val) = idx
604 .iter()
605 .enumerate()
606 .find_map(|(pos, &i)| arr.get(i as usize).map(|v| (pos, v)))?;
607
608 let mut best_pos: IdxSize = start_pos as IdxSize;
609
610 for (pos, &i) in idx.iter().enumerate().skip(start_pos + 1) {
611 if let Some(v) = arr.get(i as usize) {
612 if v.nan_max_lt(&best_val) {
613 best_val = v;
614 best_pos = pos as IdxSize;
615 }
616 }
617 }
618
619 Some(best_pos)
620 }
621 })
622 },
623 GroupsType::Slice {
624 groups: groups_slice,
625 overlapping,
626 monotonic,
627 } => {
628 if _use_rolling_kernels(groups_slice, *overlapping, *monotonic, self.chunks()) {
629 let arr = self.downcast_as_array();
630 let values = arr.values().as_slice();
631 let offset_iter = groups_slice.iter().map(|[first, len]| (*first, *len));
632 let idx_arr = match arr.validity() {
633 None => {
634 _rolling_apply_agg_window_no_nulls::<ArgMinWindow<_>, _, _, IdxSize>(
635 values,
636 offset_iter,
637 None,
638 )
639 },
640 Some(validity) => {
641 _rolling_apply_agg_window_nulls::<ArgMinWindow<_>, _, _, IdxSize>(
642 values,
643 validity,
644 offset_iter,
645 None,
646 )
647 },
648 };
649
650 IdxCa::from(idx_arr).into_series()
651 } else {
652 _agg_helper_slice::<IdxType, _>(groups_slice, |[first, len]| {
653 debug_assert!(len <= self.len() as IdxSize);
654 match len {
655 0 => None,
656 1 => Some(0 as IdxSize),
657 _ => {
658 let group_ca = _slice_from_offsets(self, first, len);
659 let pos_in_group: Option<usize> = arg_min_numeric(&group_ca);
660 pos_in_group.map(|p| p as IdxSize)
661 },
662 }
663 })
664 }
665 },
666 }
667 }
668
669 pub(crate) unsafe fn agg_max(&self, groups: &GroupsType) -> Series {
670 if (!self.has_nulls() || matches!(groups, GroupsType::Slice { .. }))
674 && !T::Native::is_float()
675 {
676 match self.is_sorted_flag() {
677 IsSorted::Ascending => return self.clone().into_series().agg_last_non_null(groups),
678 IsSorted::Descending => {
679 return self.clone().into_series().agg_first_non_null(groups);
680 },
681 _ => {},
682 }
683 }
684
685 match groups {
686 GroupsType::Idx(groups) => {
687 let ca = self.rechunk();
688 let arr = ca.downcast_iter().next().unwrap();
689 let no_nulls = arr.null_count() == 0;
690 _agg_helper_idx::<T, _>(groups, |(first, idx)| {
691 debug_assert!(idx.len() <= arr.len());
692 if idx.is_empty() {
693 None
694 } else if idx.len() == 1 {
695 arr.get(first as usize)
696 } else if no_nulls {
697 take_agg_no_null_primitive_iter_unchecked(arr, idx2usize(idx))
698 .reduce(|a, b| a.max_ignore_nan(b))
699 } else {
700 take_agg_primitive_iter_unchecked(arr, idx2usize(idx))
701 .reduce(|a, b| a.max_ignore_nan(b))
702 }
703 })
704 },
705 GroupsType::Slice {
706 groups: groups_slice,
707 overlapping,
708 monotonic,
709 } => {
710 if _use_rolling_kernels(groups_slice, *overlapping, *monotonic, self.chunks()) {
711 let arr = self.downcast_iter().next().unwrap();
712 let values = arr.values().as_slice();
713 let offset_iter = groups_slice.iter().map(|[first, len]| (*first, *len));
714 let arr = match arr.validity() {
715 None => _rolling_apply_agg_window_no_nulls::<MaxWindow<_>, _, _, _>(
716 values,
717 offset_iter,
718 None,
719 ),
720 Some(validity) => {
721 _rolling_apply_agg_window_nulls::<rolling::nulls::MaxWindow<_>, _, _, _>(
722 values,
723 validity,
724 offset_iter,
725 None,
726 )
727 },
728 };
729 Self::from(arr).into_series()
730 } else {
731 _agg_helper_slice::<T, _>(groups_slice, |[first, len]| {
732 debug_assert!(len <= self.len() as IdxSize);
733 match len {
734 0 => None,
735 1 => self.get(first as usize),
736 _ => {
737 let arr_group = _slice_from_offsets(self, first, len);
738 ChunkAgg::max(&arr_group)
739 },
740 }
741 })
742 }
743 },
744 }
745 }
746
747 pub(crate) unsafe fn agg_arg_max(&self, groups: &GroupsType) -> Series
748 where
749 for<'b> &'b [T::Native]: ArgMinMax,
750 {
751 if !self.has_nulls() || matches!(groups, GroupsType::Slice { .. }) {
752 match self.is_sorted_flag() {
753 IsSorted::Ascending => {
754 return self.clone().into_series().agg_arg_last_non_null(groups);
755 },
756 IsSorted::Descending => {
757 return self.clone().into_series().agg_arg_first_non_null(groups);
758 },
759 _ => {},
760 }
761 }
762 match groups {
763 GroupsType::Idx(groups) => {
764 let ca = self.rechunk();
765 let arr = ca.downcast_as_array();
766 let no_nulls = arr.null_count() == 0;
767
768 agg_helper_idx_on_all::<IdxType, _>(groups, |idx| {
769 if idx.is_empty() {
770 return None;
771 }
772
773 if no_nulls {
774 let first_i = idx[0] as usize;
775 let mut best_pos: IdxSize = 0;
776 let mut best_val: T::Native = unsafe { arr.value_unchecked(first_i) };
777
778 for (pos, &i) in idx.iter().enumerate().skip(1) {
779 let v = unsafe { arr.value_unchecked(i as usize) };
780
781 if v.nan_min_gt(&best_val) {
782 best_val = v;
783 best_pos = pos as IdxSize;
784 }
785 }
786
787 Some(best_pos)
788 } else {
789 let (start_pos, mut best_val) = idx
790 .iter()
791 .enumerate()
792 .find_map(|(pos, &i)| arr.get(i as usize).map(|v| (pos, v)))?;
793
794 let mut best_pos: IdxSize = start_pos as IdxSize;
795
796 for (pos, &i) in idx.iter().enumerate().skip(start_pos + 1) {
797 if let Some(v) = arr.get(i as usize) {
798 if v.nan_min_gt(&best_val) {
799 best_val = v;
800 best_pos = pos as IdxSize;
801 }
802 }
803 }
804
805 Some(best_pos)
806 }
807 })
808 },
809
810 GroupsType::Slice {
811 groups: groups_slice,
812 overlapping,
813 monotonic,
814 } => {
815 if _use_rolling_kernels(groups_slice, *overlapping, *monotonic, self.chunks()) {
816 let arr = self.downcast_iter().next().unwrap();
817 let values = arr.values().as_slice();
818 let offset_iter = groups_slice.iter().map(|[first, len]| (*first, *len));
819 let idx_arr = match arr.validity() {
820 None => {
821 _rolling_apply_agg_window_no_nulls::<ArgMaxWindow<_>, _, _, IdxSize>(
822 values,
823 offset_iter,
824 None,
825 )
826 },
827 Some(validity) => {
828 _rolling_apply_agg_window_nulls::<ArgMaxWindow<_>, _, _, IdxSize>(
829 values,
830 validity,
831 offset_iter,
832 None,
833 )
834 },
835 };
836 IdxCa::from(idx_arr).into_series()
837 } else {
838 _agg_helper_slice::<IdxType, _>(groups_slice, |[first, len]| {
839 debug_assert!(len <= self.len() as IdxSize);
840 match len {
841 0 => None,
842 1 => Some(0 as IdxSize),
843 _ => {
844 let group_ca = _slice_from_offsets(self, first, len);
845 let pos_in_group: Option<usize> = arg_max_numeric(&group_ca);
846 pos_in_group.map(|p| p as IdxSize)
847 },
848 }
849 })
850 }
851 },
852 }
853 }
854 pub(crate) unsafe fn agg_sum(&self, groups: &GroupsType) -> Series {
855 match groups {
856 GroupsType::Idx(groups) => {
857 let ca = self.rechunk();
858 let arr = ca.downcast_iter().next().unwrap();
859 let no_nulls = arr.null_count() == 0;
860 _agg_helper_idx_no_null::<T, _>(groups, |(first, idx)| {
861 debug_assert!(idx.len() <= self.len());
862 if idx.is_empty() {
863 T::Native::zero()
864 } else if idx.len() == 1 {
865 arr.get(first as usize).unwrap_or(T::Native::zero())
866 } else if no_nulls {
867 if T::Native::is_float() {
868 take_agg_no_null_primitive_iter_unchecked(arr, idx2usize(idx))
869 .fold(KahanSum::default(), |k, x| k + x)
870 .sum()
871 } else {
872 take_agg_no_null_primitive_iter_unchecked(arr, idx2usize(idx))
873 .fold(T::Native::zero(), |a, b| a + b)
874 }
875 } else if T::Native::is_float() {
876 take_agg_primitive_iter_unchecked(arr, idx2usize(idx))
877 .fold(KahanSum::default(), |k, x| k + x)
878 .sum()
879 } else {
880 take_agg_primitive_iter_unchecked(arr, idx2usize(idx))
881 .fold(T::Native::zero(), |a, b| a + b)
882 }
883 })
884 },
885 GroupsType::Slice {
886 groups,
887 overlapping,
888 monotonic,
889 } => {
890 if _use_rolling_kernels(groups, *overlapping, *monotonic, self.chunks()) {
891 let arr = self.downcast_iter().next().unwrap();
892 let values = arr.values().as_slice();
893 let offset_iter = groups.iter().map(|[first, len]| (*first, *len));
894 let arr = match arr.validity() {
895 None => _rolling_apply_agg_window_no_nulls::<
896 SumWindow<T::Native, T::Native>,
897 _,
898 _,
899 _,
900 >(values, offset_iter, None),
901 Some(validity) => {
902 _rolling_apply_agg_window_nulls::<
903 SumWindow<T::Native, T::Native>,
904 _,
905 _,
906 _,
907 >(values, validity, offset_iter, None)
908 },
909 };
910 Self::from(arr).into_series()
911 } else {
912 _agg_helper_slice_no_null::<T, _>(groups, |[first, len]| {
913 debug_assert!(len <= self.len() as IdxSize);
914 match len {
915 0 => T::Native::zero(),
916 1 => self.get(first as usize).unwrap_or(T::Native::zero()),
917 _ => {
918 let arr_group = _slice_from_offsets(self, first, len);
919 arr_group.sum().unwrap_or(T::Native::zero())
920 },
921 }
922 })
923 }
924 },
925 }
926 }
927}
928
929impl<T> SeriesWrap<ChunkedArray<T>>
930where
931 T: PolarsFloatType,
932 ChunkedArray<T>: ChunkVar
933 + VarAggSeries
934 + ChunkQuantile<T::Native>
935 + QuantileAggSeries
936 + ChunkAgg<T::Native>,
937 T::Native: Pow<T::Native, Output = T::Native>,
938{
939 pub(crate) unsafe fn agg_mean(&self, groups: &GroupsType) -> Series {
940 match groups {
941 GroupsType::Idx(groups) => {
942 let ca = self.rechunk();
943 let arr = ca.downcast_iter().next().unwrap();
944 let no_nulls = arr.null_count() == 0;
945 _agg_helper_idx::<T, _>(groups, |(first, idx)| {
946 debug_assert!(idx.len() <= self.len());
952 let out = if idx.is_empty() {
953 None
954 } else if idx.len() == 1 {
955 arr.get(first as usize).map(|sum| sum.to_f64().unwrap())
956 } else if no_nulls {
957 Some(
958 take_agg_no_null_primitive_iter_unchecked(arr, idx2usize(idx))
959 .fold(KahanSum::default(), |a, b| {
960 a + b.to_f64().unwrap_unchecked()
961 })
962 .sum()
963 / idx.len() as f64,
964 )
965 } else {
966 take_agg_primitive_iter_unchecked_count_nulls(
967 arr,
968 idx2usize(idx),
969 KahanSum::default(),
970 |a, b| a + b.to_f64().unwrap_unchecked(),
971 idx.len() as IdxSize,
972 )
973 .map(|(sum, null_count)| sum.sum() / (idx.len() as f64 - null_count as f64))
974 };
975 out.map(|flt| NumCast::from(flt).unwrap())
976 })
977 },
978 GroupsType::Slice {
979 groups,
980 overlapping,
981 monotonic,
982 } => {
983 if _use_rolling_kernels(groups, *overlapping, *monotonic, self.chunks()) {
984 let arr = self.downcast_iter().next().unwrap();
985 let values = arr.values().as_slice();
986 let offset_iter = groups.iter().map(|[first, len]| (*first, *len));
987 let arr = match arr.validity() {
988 None => _rolling_apply_agg_window_no_nulls::<MeanWindow<_>, _, _, _>(
989 values,
990 offset_iter,
991 None,
992 ),
993 Some(validity) => {
994 _rolling_apply_agg_window_nulls::<MeanWindow<_>, _, _, _>(
995 values,
996 validity,
997 offset_iter,
998 None,
999 )
1000 },
1001 };
1002 ChunkedArray::<T>::from(arr).into_series()
1003 } else {
1004 _agg_helper_slice::<T, _>(groups, |[first, len]| {
1005 debug_assert!(len <= self.len() as IdxSize);
1006 match len {
1007 0 => None,
1008 1 => self.get(first as usize),
1009 _ => {
1010 let arr_group = _slice_from_offsets(self, first, len);
1011 arr_group.mean().map(|flt| NumCast::from(flt).unwrap())
1012 },
1013 }
1014 })
1015 }
1016 },
1017 }
1018 }
1019
1020 pub(crate) unsafe fn agg_var(&self, groups: &GroupsType, ddof: u8) -> Series
1021 where
1022 <T as datatypes::PolarsNumericType>::Native: num_traits::Float,
1023 {
1024 let ca = &self.0.rechunk();
1025 match groups {
1026 GroupsType::Idx(groups) => {
1027 let ca = ca.rechunk();
1028 let arr = ca.downcast_iter().next().unwrap();
1029 let no_nulls = arr.null_count() == 0;
1030 agg_helper_idx_on_all::<T, _>(groups, |idx| {
1031 debug_assert!(idx.len() <= ca.len());
1032 if idx.is_empty() {
1033 return None;
1034 }
1035 let out = if no_nulls {
1036 take_var_no_null_primitive_iter_unchecked(arr, idx2usize(idx), ddof)
1037 } else {
1038 take_var_nulls_primitive_iter_unchecked(arr, idx2usize(idx), ddof)
1039 };
1040 out.map(|flt| NumCast::from(flt).unwrap())
1041 })
1042 },
1043 GroupsType::Slice {
1044 groups,
1045 overlapping,
1046 monotonic,
1047 } => {
1048 if _use_rolling_kernels(groups, *overlapping, *monotonic, self.chunks()) {
1049 let arr = self.downcast_iter().next().unwrap();
1050 let values = arr.values().as_slice();
1051 let offset_iter = groups.iter().map(|[first, len]| (*first, *len));
1052 let arr = match arr.validity() {
1053 None => _rolling_apply_agg_window_no_nulls::<
1054 MomentWindow<_, VarianceMoment>,
1055 _,
1056 _,
1057 _,
1058 >(
1059 values,
1060 offset_iter,
1061 Some(RollingFnParams::Var(RollingVarParams { ddof })),
1062 ),
1063 Some(validity) => _rolling_apply_agg_window_nulls::<
1064 rolling::nulls::MomentWindow<_, VarianceMoment>,
1065 _,
1066 _,
1067 _,
1068 >(
1069 values,
1070 validity,
1071 offset_iter,
1072 Some(RollingFnParams::Var(RollingVarParams { ddof })),
1073 ),
1074 };
1075 ChunkedArray::<T>::from(arr).into_series()
1076 } else {
1077 _agg_helper_slice::<T, _>(groups, |[first, len]| {
1078 debug_assert!(len <= self.len() as IdxSize);
1079 match len {
1080 0 => None,
1081 1 => {
1082 if ddof == 0 {
1083 NumCast::from(0)
1084 } else {
1085 None
1086 }
1087 },
1088 _ => {
1089 let arr_group = _slice_from_offsets(self, first, len);
1090 arr_group.var(ddof).map(|flt| NumCast::from(flt).unwrap())
1091 },
1092 }
1093 })
1094 }
1095 },
1096 }
1097 }
1098 pub(crate) unsafe fn agg_std(&self, groups: &GroupsType, ddof: u8) -> Series
1099 where
1100 <T as datatypes::PolarsNumericType>::Native: num_traits::Float,
1101 {
1102 let ca = &self.0.rechunk();
1103 match groups {
1104 GroupsType::Idx(groups) => {
1105 let arr = ca.downcast_iter().next().unwrap();
1106 let no_nulls = arr.null_count() == 0;
1107 agg_helper_idx_on_all::<T, _>(groups, |idx| {
1108 debug_assert!(idx.len() <= ca.len());
1109 if idx.is_empty() {
1110 return None;
1111 }
1112 let out = if no_nulls {
1113 take_var_no_null_primitive_iter_unchecked(arr, idx2usize(idx), ddof)
1114 } else {
1115 take_var_nulls_primitive_iter_unchecked(arr, idx2usize(idx), ddof)
1116 };
1117 out.map(|flt| NumCast::from(flt.sqrt()).unwrap())
1118 })
1119 },
1120 GroupsType::Slice {
1121 groups,
1122 overlapping,
1123 monotonic,
1124 } => {
1125 if _use_rolling_kernels(groups, *overlapping, *monotonic, self.chunks()) {
1126 let arr = ca.downcast_iter().next().unwrap();
1127 let values = arr.values().as_slice();
1128 let offset_iter = groups.iter().map(|[first, len]| (*first, *len));
1129 let arr = match arr.validity() {
1130 None => _rolling_apply_agg_window_no_nulls::<
1131 MomentWindow<_, VarianceMoment>,
1132 _,
1133 _,
1134 _,
1135 >(
1136 values,
1137 offset_iter,
1138 Some(RollingFnParams::Var(RollingVarParams { ddof })),
1139 ),
1140 Some(validity) => _rolling_apply_agg_window_nulls::<
1141 rolling::nulls::MomentWindow<_, rolling::nulls::VarianceMoment>,
1142 _,
1143 _,
1144 _,
1145 >(
1146 values,
1147 validity,
1148 offset_iter,
1149 Some(RollingFnParams::Var(RollingVarParams { ddof })),
1150 ),
1151 };
1152
1153 let mut ca = ChunkedArray::<T>::from(arr);
1154 ca.apply_mut(|v| v.powf(NumCast::from(0.5).unwrap()));
1155 ca.into_series()
1156 } else {
1157 _agg_helper_slice::<T, _>(groups, |[first, len]| {
1158 debug_assert!(len <= self.len() as IdxSize);
1159 match len {
1160 0 => None,
1161 1 => {
1162 if ddof == 0 {
1163 NumCast::from(0)
1164 } else {
1165 None
1166 }
1167 },
1168 _ => {
1169 let arr_group = _slice_from_offsets(self, first, len);
1170 arr_group.std(ddof).map(|flt| NumCast::from(flt).unwrap())
1171 },
1172 }
1173 })
1174 }
1175 },
1176 }
1177 }
1178}
1179
1180impl Float32Chunked {
1181 pub(crate) unsafe fn agg_quantile(
1182 &self,
1183 groups: &GroupsType,
1184 quantile: f64,
1185 method: QuantileMethod,
1186 ) -> Series {
1187 agg_quantile_generic::<_, Float32Type>(self, groups, quantile, method)
1188 }
1189 pub(crate) unsafe fn agg_median(&self, groups: &GroupsType) -> Series {
1190 agg_median_generic::<_, Float32Type>(self, groups)
1191 }
1192}
1193impl Float64Chunked {
1194 pub(crate) unsafe fn agg_quantile(
1195 &self,
1196 groups: &GroupsType,
1197 quantile: f64,
1198 method: QuantileMethod,
1199 ) -> Series {
1200 agg_quantile_generic::<_, Float64Type>(self, groups, quantile, method)
1201 }
1202 pub(crate) unsafe fn agg_median(&self, groups: &GroupsType) -> Series {
1203 agg_median_generic::<_, Float64Type>(self, groups)
1204 }
1205}
1206
1207impl<T> ChunkedArray<T>
1208where
1209 T: PolarsIntegerType,
1210 ChunkedArray<T>: ChunkAgg<T::Native> + ChunkVar,
1211 T::Native: NumericNative + Ord,
1212{
1213 pub(crate) unsafe fn agg_mean(&self, groups: &GroupsType) -> Series {
1214 match groups {
1215 GroupsType::Idx(groups) => {
1216 let ca = self.rechunk();
1217 let arr = ca.downcast_get(0).unwrap();
1218 _agg_helper_idx::<Float64Type, _>(groups, |(first, idx)| {
1219 debug_assert!(idx.len() <= self.len());
1225 if idx.is_empty() {
1226 None
1227 } else if idx.len() == 1 {
1228 self.get(first as usize).map(|sum| sum.to_f64().unwrap())
1229 } else {
1230 match (self.has_nulls(), self.chunks.len()) {
1231 (false, 1) => Some(
1232 take_agg_no_null_primitive_iter_unchecked(arr, idx2usize(idx))
1233 .fold(KahanSum::default(), |a, b| a + b.to_f64().unwrap())
1234 .sum()
1235 / idx.len() as f64,
1236 ),
1237 (_, 1) => {
1238 take_agg_primitive_iter_unchecked_count_nulls(
1239 arr,
1240 idx2usize(idx),
1241 KahanSum::default(),
1242 |a, b| a + b.to_f64().unwrap(),
1243 idx.len() as IdxSize,
1244 )
1245 }
1246 .map(|(sum, null_count)| {
1247 sum.sum() / (idx.len() as f64 - null_count as f64)
1248 }),
1249 _ => {
1250 let take = { self.take_unchecked(idx) };
1251 take.mean()
1252 },
1253 }
1254 }
1255 })
1256 },
1257 GroupsType::Slice {
1258 groups: groups_slice,
1259 overlapping,
1260 monotonic,
1261 } => {
1262 if _use_rolling_kernels(groups_slice, *overlapping, *monotonic, self.chunks()) {
1263 let ca = self
1264 .cast_with_options(&DataType::Float64, CastOptions::Overflowing)
1265 .unwrap();
1266 ca.agg_mean(groups)
1267 } else {
1268 _agg_helper_slice::<Float64Type, _>(groups_slice, |[first, len]| {
1269 debug_assert!(first + len <= self.len() as IdxSize);
1270 match len {
1271 0 => None,
1272 1 => self.get(first as usize).map(|v| NumCast::from(v).unwrap()),
1273 _ => {
1274 let arr_group = _slice_from_offsets(self, first, len);
1275 arr_group.mean()
1276 },
1277 }
1278 })
1279 }
1280 },
1281 }
1282 }
1283
1284 pub(crate) unsafe fn agg_var(&self, groups: &GroupsType, ddof: u8) -> Series {
1285 match groups {
1286 GroupsType::Idx(groups) => {
1287 let ca_self = self.rechunk();
1288 let arr = ca_self.downcast_iter().next().unwrap();
1289 let no_nulls = arr.null_count() == 0;
1290 agg_helper_idx_on_all::<Float64Type, _>(groups, |idx| {
1291 debug_assert!(idx.len() <= arr.len());
1292 if idx.is_empty() {
1293 return None;
1294 }
1295 if no_nulls {
1296 take_var_no_null_primitive_iter_unchecked(arr, idx2usize(idx), ddof)
1297 } else {
1298 take_var_nulls_primitive_iter_unchecked(arr, idx2usize(idx), ddof)
1299 }
1300 })
1301 },
1302 GroupsType::Slice {
1303 groups: groups_slice,
1304 overlapping,
1305 monotonic,
1306 } => {
1307 if _use_rolling_kernels(groups_slice, *overlapping, *monotonic, self.chunks()) {
1308 let ca = self
1309 .cast_with_options(&DataType::Float64, CastOptions::Overflowing)
1310 .unwrap();
1311 ca.agg_var(groups, ddof)
1312 } else {
1313 _agg_helper_slice::<Float64Type, _>(groups_slice, |[first, len]| {
1314 debug_assert!(first + len <= self.len() as IdxSize);
1315 match len {
1316 0 => None,
1317 1 => {
1318 if ddof == 0 {
1319 NumCast::from(0)
1320 } else {
1321 None
1322 }
1323 },
1324 _ => {
1325 let arr_group = _slice_from_offsets(self, first, len);
1326 arr_group.var(ddof)
1327 },
1328 }
1329 })
1330 }
1331 },
1332 }
1333 }
1334 pub(crate) unsafe fn agg_std(&self, groups: &GroupsType, ddof: u8) -> Series {
1335 match groups {
1336 GroupsType::Idx(groups) => {
1337 let ca_self = self.rechunk();
1338 let arr = ca_self.downcast_iter().next().unwrap();
1339 let no_nulls = arr.null_count() == 0;
1340 agg_helper_idx_on_all::<Float64Type, _>(groups, |idx| {
1341 debug_assert!(idx.len() <= self.len());
1342 if idx.is_empty() {
1343 return None;
1344 }
1345 let out = if no_nulls {
1346 take_var_no_null_primitive_iter_unchecked(arr, idx2usize(idx), ddof)
1347 } else {
1348 take_var_nulls_primitive_iter_unchecked(arr, idx2usize(idx), ddof)
1349 };
1350 out.map(|v| v.sqrt())
1351 })
1352 },
1353 GroupsType::Slice {
1354 groups: groups_slice,
1355 overlapping,
1356 monotonic,
1357 } => {
1358 if _use_rolling_kernels(groups_slice, *overlapping, *monotonic, self.chunks()) {
1359 let ca = self
1360 .cast_with_options(&DataType::Float64, CastOptions::Overflowing)
1361 .unwrap();
1362 ca.agg_std(groups, ddof)
1363 } else {
1364 _agg_helper_slice::<Float64Type, _>(groups_slice, |[first, len]| {
1365 debug_assert!(first + len <= self.len() as IdxSize);
1366 match len {
1367 0 => None,
1368 1 => {
1369 if ddof == 0 {
1370 NumCast::from(0)
1371 } else {
1372 None
1373 }
1374 },
1375 _ => {
1376 let arr_group = _slice_from_offsets(self, first, len);
1377 arr_group.std(ddof)
1378 },
1379 }
1380 })
1381 }
1382 },
1383 }
1384 }
1385
1386 pub(crate) unsafe fn agg_quantile(
1387 &self,
1388 groups: &GroupsType,
1389 quantile: f64,
1390 method: QuantileMethod,
1391 ) -> Series {
1392 agg_quantile_generic::<_, Float64Type>(self, groups, quantile, method)
1393 }
1394 pub(crate) unsafe fn agg_median(&self, groups: &GroupsType) -> Series {
1395 agg_median_generic::<_, Float64Type>(self, groups)
1396 }
1397}