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