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::*;
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
53// Unlike `rolling_min` and `rolling_max`, group aggregations ignore NaN.
54type 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
59// if the windows overlap, we can use the rolling_<agg> kernels
60// they maintain state, which saves a lot of compute by not naively traversing all elements every
61// window
62//
63// if the windows don't overlap, we should not use these kernels as they are single threaded, so
64// we miss out on easy parallelization.
65pub 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
77/// Rolling min_by/max_by for numeric `by` columns using O(n) deque kernel.
78///
79/// # Panics
80/// Panics if the `by` column's physical dtype is not primitive numeric or if it is a categorical.
81pub 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
108// Use an aggregation window that maintains the state
109pub 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    // This iterators length can be trusted
122    // these represent the number of groups in the group_by operation
123    let output_len = offsets.size_hint().0;
124    // start with a dummy index, will be overwritten on first iteration.
125    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            // SAFETY:
136            // we are in bounds
137            unsafe { agg_window.update(start as usize, end as usize) };
138            match agg_window.get_agg(idx) {
139                Some(val) => val,
140                None => {
141                    // SAFETY: we are in bounds
142                    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
152// Use an aggregation window that maintains the state.
153pub 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    // items (offset, len) -> so offsets are offset, offset + len
160    Agg: RollingAggWindowNoNulls<T, Out>,
161    O: Iterator<Item = (IdxSize, IdxSize)> + TrustedLen,
162    T: IsFloat + NativeType,
163    Out: NativeType,
164{
165    // start with a dummy index, will be overwritten on first iteration.
166    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            // SAFETY: we are in bounds.
174            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
187/// Helper that combines the groups into a parallel iterator over `(first, all): (u32, &Vec<u32>)`.
188pub 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
201/// Same helper as `_agg_helper_idx` but for aggregations that don't return an Option.
202pub 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
211/// Helper that iterates on the `all: Vec<Vec<u32>` collection,
212/// this doesn't have traverse the `first: Vec<u32>` memory and is therefore faster.
213fn 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
263/// Intermediate helper trait so we can have a single generic implementation
264/// This trait will ensure the specific dispatch works without complicating
265/// the trait bounds.
266trait 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                // checked with invalid quantile check
338                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                // this cast is a no-op for floats
348                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                // The rolling kernels works on the dtype, this is not yet the
377                // float output type we need.
378                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                            // unwrap checked with invalid quantile check
388                            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/// # Safety
426///
427/// No bounds checks on `groups`.
428#[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    // Prevent a rechunk for every individual group.
438
439    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    /// # Safety
474    ///
475    /// No bounds checks on `groups`.
476    pub(crate) unsafe fn agg_and(&self, groups: &GroupsType) -> Series {
477        unsafe { bitwise_agg(self, groups, ChunkBitwiseReduce::and_reduce) }
478    }
479
480    /// # Safety
481    ///
482    /// No bounds checks on `groups`.
483    pub(crate) unsafe fn agg_or(&self, groups: &GroupsType) -> Series {
484        unsafe { bitwise_agg(self, groups, ChunkBitwiseReduce::or_reduce) }
485    }
486
487    /// # Safety
488    ///
489    /// No bounds checks on `groups`.
490    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        // faster paths
503        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        // 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) => _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        // Skipped for floats for the same reason as in `agg_max`.
763        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                    // this can fail due to a bug in lazy code.
961                    // here users can create filters in aggregations
962                    // and thereby creating shorter columns than the original group tuples.
963                    // the group tuples are modified, but if that's done incorrect there can be out of bounds
964                    // access
965                    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                    // this can fail due to a bug in lazy code.
1248                    // here users can create filters in aggregations
1249                    // and thereby creating shorter columns than the original group tuples.
1250                    // the group tuples are modified, but if that's done incorrect there can be out of bounds
1251                    // access
1252                    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}