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