Skip to main content

polars_core/frame/group_by/aggregations/
mod.rs

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
54// if the windows overlap, we can use the rolling_<agg> kernels
55// they maintain state, which saves a lot of compute by not naively traversing all elements every
56// window
57//
58// if the windows don't overlap, we should not use these kernels as they are single threaded, so
59// we miss out on easy parallelization.
60pub 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
72/// Rolling min_by/max_by for numeric `by` columns using O(n) deque kernel.
73///
74/// # Panics
75/// Panics if the `by` column's physical dtype is not primitive numeric or if it is a categorical.
76pub 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
106// Use an aggregation window that maintains the state
107pub 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    // This iterators length can be trusted
120    // these represent the number of groups in the group_by operation
121    let output_len = offsets.size_hint().0;
122    // start with a dummy index, will be overwritten on first iteration.
123    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            // SAFETY:
134            // we are in bounds
135            unsafe { agg_window.update(start as usize, end as usize) };
136            match agg_window.get_agg(idx) {
137                Some(val) => val,
138                None => {
139                    // SAFETY: we are in bounds
140                    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
150// Use an aggregation window that maintains the state.
151pub 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    // items (offset, len) -> so offsets are offset, offset + len
158    Agg: RollingAggWindowNoNulls<T, Out>,
159    O: Iterator<Item = (IdxSize, IdxSize)> + TrustedLen,
160    T: IsFloat + NativeType,
161    Out: NativeType,
162{
163    // start with a dummy index, will be overwritten on first iteration.
164    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            // SAFETY: we are in bounds.
172            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
185/// Helper that combines the groups into a parallel iterator over `(first, all): (u32, &Vec<u32>)`.
186pub 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
199/// Same helper as `_agg_helper_idx` but for aggregations that don't return an Option.
200pub 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
209/// Helper that iterates on the `all: Vec<Vec<u32>` collection,
210/// this doesn't have traverse the `first: Vec<u32>` memory and is therefore faster.
211fn 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
261/// Intermediate helper trait so we can have a single generic implementation
262/// This trait will ensure the specific dispatch works without complicating
263/// the trait bounds.
264trait 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                // checked with invalid quantile check
336                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                // this cast is a no-op for floats
346                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                // The rolling kernels works on the dtype, this is not yet the
375                // float output type we need.
376                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                            // unwrap checked with invalid quantile check
386                            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/// # Safety
424///
425/// No bounds checks on `groups`.
426#[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    // Prevent a rechunk for every individual group.
436
437    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    /// # Safety
472    ///
473    /// No bounds checks on `groups`.
474    pub(crate) unsafe fn agg_and(&self, groups: &GroupsType) -> Series {
475        unsafe { bitwise_agg(self, groups, ChunkBitwiseReduce::and_reduce) }
476    }
477
478    /// # Safety
479    ///
480    /// No bounds checks on `groups`.
481    pub(crate) unsafe fn agg_or(&self, groups: &GroupsType) -> Series {
482        unsafe { bitwise_agg(self, groups, ChunkBitwiseReduce::or_reduce) }
483    }
484
485    /// # Safety
486    ///
487    /// No bounds checks on `groups`.
488    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        // faster paths
501        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        // Sorted fast-path. We skip this for floats because the largest value might be NaN, which
684        // max is supposed to skip unless everything is NaN. We would need an
685        // agg_first_non_null_non_nan.
686        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                    // this can fail due to a bug in lazy code.
960                    // here users can create filters in aggregations
961                    // and thereby creating shorter columns than the original group tuples.
962                    // the group tuples are modified, but if that's done incorrect there can be out of bounds
963                    // access
964                    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                    // this can fail due to a bug in lazy code.
1247                    // here users can create filters in aggregations
1248                    // and thereby creating shorter columns than the original group tuples.
1249                    // the group tuples are modified, but if that's done incorrect there can be out of bounds
1250                    // access
1251                    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}