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