Skip to main content

polars_core/frame/group_by/
mod.rs

1use std::fmt::{Debug, Display, Formatter};
2use std::hash::Hash;
3
4use num_traits::NumCast;
5use polars_compute::rolling::QuantileMethod;
6use polars_utils::broadcast::broadcast_len;
7use polars_utils::format_pl_smallstr;
8use polars_utils::hashing::DirtyHash;
9use rayon::prelude::*;
10
11use self::hashing::*;
12use crate::prelude::*;
13use crate::runtime::RAYON;
14use crate::utils::{_set_partition_size, accumulate_dataframes_vertical};
15
16pub mod aggregations;
17pub(crate) mod hashing;
18mod into_groups;
19mod position;
20
21pub use into_groups::*;
22pub use position::*;
23
24use crate::chunked_array::ops::row_encode::{
25    encode_rows_unordered, encode_rows_vertical_par_unordered,
26};
27
28impl DataFrame {
29    pub fn group_by_with_series(
30        &self,
31        mut by: Vec<Column>,
32        multithreaded: bool,
33        sorted: bool,
34    ) -> PolarsResult<GroupBy<'_>> {
35        polars_ensure!(
36            !by.is_empty(),
37            ComputeError: "at least one key is required in a group_by operation"
38        );
39
40        // Ensure all 'by' columns have the same common_height
41        // The condition self.width > 0 ensures we can still call this on a
42        // dummy dataframe where we provide the keys
43        let common_height = if self.width() > 0 {
44            self.height()
45        } else {
46            broadcast_len(by.iter()).context("group_by key")?
47        };
48        for by_key in by.iter_mut() {
49            by_key
50                .broadcast_in_place_to(common_height)
51                .context("group_by keys should have the same length as the DataFrame")?;
52        }
53
54        let groups = if by.len() == 1 {
55            let column = &by[0];
56            column
57                .as_materialized_series()
58                .group_tuples(multithreaded, sorted)
59        } else if by.iter().any(|s| s.dtype().is_object()) {
60            #[cfg(feature = "object")]
61            {
62                let mut df = DataFrame::new(self.height(), by.clone()).unwrap();
63                let n = df.height();
64                let rows = df.to_av_rows();
65                let iter = (0..n).map(|i| rows.get(i));
66                Ok(group_by(iter, sorted))
67            }
68            #[cfg(not(feature = "object"))]
69            {
70                unreachable!()
71            }
72        } else {
73            // Skip null dtype.
74            let by = by
75                .iter()
76                .filter(|s| !s.dtype().is_null())
77                .cloned()
78                .collect::<Vec<_>>();
79            if by.is_empty() {
80                let groups = if self.height() == 0 {
81                    vec![]
82                } else {
83                    vec![[0, self.height() as IdxSize]]
84                };
85
86                Ok(GroupsType::new_slice(groups, false, true))
87            } else {
88                let rows = if multithreaded {
89                    encode_rows_vertical_par_unordered(&by)
90                } else {
91                    encode_rows_unordered(&by)
92                }?
93                .into_series();
94                rows.group_tuples(multithreaded, sorted)
95            }
96        };
97        Ok(GroupBy::new(self, by, groups?.into_sliceable(), None))
98    }
99
100    /// Group DataFrame using a Series column.
101    ///
102    /// # Example
103    ///
104    /// ```
105    /// use polars_core::prelude::*;
106    /// fn group_by_sum(df: &DataFrame) -> PolarsResult<DataFrame> {
107    ///     df.group_by(["column_name"])?
108    ///     .select(["agg_column_name"])
109    ///     .sum()
110    /// }
111    /// ```
112    pub fn group_by<I, S>(&self, by: I) -> PolarsResult<GroupBy<'_>>
113    where
114        I: IntoIterator<Item = S>,
115        S: AsRef<str>,
116    {
117        let selected_keys = self.select_to_vec(by)?;
118        self.group_by_with_series(selected_keys, true, false)
119    }
120
121    /// Group DataFrame using a Series column.
122    /// The groups are ordered by their smallest row index.
123    pub fn group_by_stable<I, S>(&self, by: I) -> PolarsResult<GroupBy<'_>>
124    where
125        I: IntoIterator<Item = S>,
126        S: AsRef<str>,
127    {
128        let selected_keys = self.select_to_vec(by)?;
129        self.group_by_with_series(selected_keys, true, true)
130    }
131}
132
133/// Returned by a group_by operation on a DataFrame. This struct supports
134/// several aggregations.
135///
136/// Until described otherwise, the examples in this struct are performed on the following DataFrame:
137///
138/// ```ignore
139/// use polars_core::prelude::*;
140///
141/// let dates = &[
142/// "2020-08-21",
143/// "2020-08-21",
144/// "2020-08-22",
145/// "2020-08-23",
146/// "2020-08-22",
147/// ];
148/// // date format
149/// let fmt = "%Y-%m-%d";
150/// // create date series
151/// let s0 = DateChunked::parse_from_str_slice("date", dates, fmt)
152///         .into_series();
153/// // create temperature series
154/// let s1 = Series::new("temp".into(), [20, 10, 7, 9, 1]);
155/// // create rain series
156/// let s2 = Series::new("rain".into(), [0.2, 0.1, 0.3, 0.1, 0.01]);
157/// // create a new DataFrame
158/// let df = DataFrame::new_infer_height(vec![s0, s1, s2]).unwrap();
159/// println!("{:?}", df);
160/// ```
161///
162/// Outputs:
163///
164/// ```text
165/// +------------+------+------+
166/// | date       | temp | rain |
167/// | ---        | ---  | ---  |
168/// | Date       | i32  | f64  |
169/// +============+======+======+
170/// | 2020-08-21 | 20   | 0.2  |
171/// +------------+------+------+
172/// | 2020-08-21 | 10   | 0.1  |
173/// +------------+------+------+
174/// | 2020-08-22 | 7    | 0.3  |
175/// +------------+------+------+
176/// | 2020-08-23 | 9    | 0.1  |
177/// +------------+------+------+
178/// | 2020-08-22 | 1    | 0.01 |
179/// +------------+------+------+
180/// ```
181///
182#[derive(Debug, Clone)]
183pub struct GroupBy<'a> {
184    pub df: &'a DataFrame,
185    pub(crate) selected_keys: Vec<Column>,
186    // [first idx, [other idx]]
187    groups: GroupPositions,
188    // columns selected for aggregation
189    pub(crate) selected_agg: Option<Vec<PlSmallStr>>,
190}
191
192impl<'a> GroupBy<'a> {
193    pub fn new(
194        df: &'a DataFrame,
195        by: Vec<Column>,
196        groups: GroupPositions,
197        selected_agg: Option<Vec<PlSmallStr>>,
198    ) -> Self {
199        GroupBy {
200            df,
201            selected_keys: by,
202            groups,
203            selected_agg,
204        }
205    }
206
207    /// Select the column(s) that should be aggregated.
208    /// You can select a single column or a slice of columns.
209    ///
210    /// Note that making a selection with this method is not required. If you
211    /// skip it all columns (except for the keys) will be selected for aggregation.
212    #[must_use]
213    pub fn select<I: IntoIterator<Item = S>, S: Into<PlSmallStr>>(mut self, selection: I) -> Self {
214        self.selected_agg = Some(selection.into_iter().map(|s| s.into()).collect());
215        self
216    }
217
218    /// Get the internal representation of the GroupBy operation.
219    /// The Vec returned contains:
220    ///     (first_idx, [`Vec<indexes>`])
221    ///     Where second value in the tuple is a vector with all matching indexes.
222    pub fn get_groups(&self) -> &GroupPositions {
223        &self.groups
224    }
225
226    /// Get the internal representation of the GroupBy operation.
227    /// The Vec returned contains:
228    ///     (first_idx, [`Vec<indexes>`])
229    ///     Where second value in the tuple is a vector with all matching indexes.
230    ///
231    /// # Safety
232    /// Groups should always be in bounds of the `DataFrame` hold by this [`GroupBy`].
233    /// If you mutate it, you must hold that invariant.
234    pub unsafe fn get_groups_mut(&mut self) -> &mut GroupPositions {
235        &mut self.groups
236    }
237
238    pub fn into_groups(self) -> GroupPositions {
239        self.groups
240    }
241
242    pub fn keys_sliced(&self, slice: Option<(i64, usize)>) -> Vec<Column> {
243        #[allow(unused_assignments)]
244        // needed to keep the lifetimes valid for this scope
245        let mut groups_owned = None;
246
247        let groups = if let Some((offset, len)) = slice {
248            groups_owned = Some(self.groups.slice(offset, len));
249            groups_owned.as_deref().unwrap()
250        } else {
251            &self.groups
252        };
253        RAYON.install(|| {
254            self.selected_keys
255                .par_iter()
256                .map(Column::as_materialized_series)
257                .map(|s| {
258                    match groups {
259                        GroupsType::Idx(groups) => {
260                            // SAFETY: groups are always in bounds.
261                            let mut out = unsafe { s.take_slice_unchecked(groups.first()) };
262                            if groups.sorted_by_first_idx {
263                                out.set_sorted_flag(s.is_sorted_flag());
264                            };
265                            out
266                        },
267                        GroupsType::Slice {
268                            groups,
269                            overlapping,
270                            monotonic: _,
271                        } => {
272                            if *overlapping && !groups.is_empty() {
273                                // Groups can be sliced.
274                                let offset = groups[0][0];
275                                let [upper_offset, upper_len] = groups[groups.len() - 1];
276                                return s.slice(
277                                    offset as i64,
278                                    ((upper_offset + upper_len) - offset) as usize,
279                                );
280                            }
281
282                            let indices = groups
283                                .iter()
284                                .map(|&[first, _len]| first)
285                                .collect_ca(PlSmallStr::EMPTY);
286                            // SAFETY: groups are always in bounds.
287                            let mut out = unsafe { s.take_unchecked(&indices) };
288                            // Sliced groups are always in order of discovery.
289                            out.set_sorted_flag(s.is_sorted_flag());
290                            out
291                        },
292                    }
293                })
294                .map(Column::from)
295                .collect()
296        })
297    }
298
299    pub fn keys(&self) -> Vec<Column> {
300        self.keys_sliced(None)
301    }
302
303    fn prepare_agg(&self) -> PolarsResult<(Vec<Column>, Vec<Column>)> {
304        let keys = self.keys();
305
306        let agg_col = match &self.selected_agg {
307            Some(selection) => self.df.select_to_vec(selection),
308            None => {
309                let by: Vec<_> = self.selected_keys.iter().map(|s| s.name()).collect();
310                let selection = self
311                    .df
312                    .columns()
313                    .iter()
314                    .map(|s| s.name())
315                    .filter(|a| !by.contains(a))
316                    .cloned()
317                    .collect::<Vec<_>>();
318
319                self.df.select_to_vec(selection.as_slice())
320            },
321        }?;
322
323        Ok((keys, agg_col))
324    }
325
326    /// Aggregate grouped series and compute the mean per group.
327    ///
328    /// # Example
329    ///
330    /// ```rust
331    /// # use polars_core::prelude::*;
332    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
333    ///     df.group_by(["date"])?.select(["temp", "rain"]).mean()
334    /// }
335    /// ```
336    /// Returns:
337    ///
338    /// ```text
339    /// +------------+-----------+-----------+
340    /// | date       | temp_mean | rain_mean |
341    /// | ---        | ---       | ---       |
342    /// | Date       | f64       | f64       |
343    /// +============+===========+===========+
344    /// | 2020-08-23 | 9         | 0.1       |
345    /// +------------+-----------+-----------+
346    /// | 2020-08-22 | 4         | 0.155     |
347    /// +------------+-----------+-----------+
348    /// | 2020-08-21 | 15        | 0.15      |
349    /// +------------+-----------+-----------+
350    /// ```
351    #[deprecated(since = "0.24.1", note = "use polars.lazy aggregations")]
352    pub fn mean(&self) -> PolarsResult<DataFrame> {
353        let (mut cols, agg_cols) = self.prepare_agg()?;
354
355        for agg_col in agg_cols {
356            let new_name = fmt_group_by_column(agg_col.name().as_str(), GroupByMethod::Mean);
357            let mut agg = unsafe { agg_col.agg_mean(&self.groups) };
358            agg.rename(new_name);
359            cols.push(agg);
360        }
361
362        DataFrame::new_infer_height(cols)
363    }
364
365    /// Aggregate grouped series and compute the sum per group.
366    ///
367    /// # Example
368    ///
369    /// ```rust
370    /// # use polars_core::prelude::*;
371    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
372    ///     df.group_by(["date"])?.select(["temp"]).sum()
373    /// }
374    /// ```
375    /// Returns:
376    ///
377    /// ```text
378    /// +------------+----------+
379    /// | date       | temp_sum |
380    /// | ---        | ---      |
381    /// | Date       | i32      |
382    /// +============+==========+
383    /// | 2020-08-23 | 9        |
384    /// +------------+----------+
385    /// | 2020-08-22 | 8        |
386    /// +------------+----------+
387    /// | 2020-08-21 | 30       |
388    /// +------------+----------+
389    /// ```
390    #[deprecated(since = "0.24.1", note = "use polars.lazy aggregations")]
391    pub fn sum(&self) -> PolarsResult<DataFrame> {
392        let (mut cols, agg_cols) = self.prepare_agg()?;
393
394        for agg_col in agg_cols {
395            let new_name = fmt_group_by_column(agg_col.name().as_str(), GroupByMethod::Sum);
396            let mut agg = unsafe { agg_col.agg_sum(&self.groups) };
397            agg.rename(new_name);
398            cols.push(agg);
399        }
400        DataFrame::new_infer_height(cols)
401    }
402
403    /// Aggregate grouped series and compute the minimal value per group.
404    ///
405    /// # Example
406    ///
407    /// ```rust
408    /// # use polars_core::prelude::*;
409    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
410    ///     df.group_by(["date"])?.select(["temp"]).min()
411    /// }
412    /// ```
413    /// Returns:
414    ///
415    /// ```text
416    /// +------------+----------+
417    /// | date       | temp_min |
418    /// | ---        | ---      |
419    /// | Date       | i32      |
420    /// +============+==========+
421    /// | 2020-08-23 | 9        |
422    /// +------------+----------+
423    /// | 2020-08-22 | 1        |
424    /// +------------+----------+
425    /// | 2020-08-21 | 10       |
426    /// +------------+----------+
427    /// ```
428    #[deprecated(since = "0.24.1", note = "use polars.lazy aggregations")]
429    pub fn min(&self) -> PolarsResult<DataFrame> {
430        let (mut cols, agg_cols) = self.prepare_agg()?;
431        for agg_col in agg_cols {
432            let new_name = fmt_group_by_column(agg_col.name().as_str(), GroupByMethod::Min);
433            let mut agg = unsafe { agg_col.agg_min(&self.groups) };
434            agg.rename(new_name);
435            cols.push(agg);
436        }
437        DataFrame::new_infer_height(cols)
438    }
439
440    /// Aggregate grouped series and compute the maximum value per group.
441    ///
442    /// # Example
443    ///
444    /// ```rust
445    /// # use polars_core::prelude::*;
446    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
447    ///     df.group_by(["date"])?.select(["temp"]).max()
448    /// }
449    /// ```
450    /// Returns:
451    ///
452    /// ```text
453    /// +------------+----------+
454    /// | date       | temp_max |
455    /// | ---        | ---      |
456    /// | Date       | i32      |
457    /// +============+==========+
458    /// | 2020-08-23 | 9        |
459    /// +------------+----------+
460    /// | 2020-08-22 | 7        |
461    /// +------------+----------+
462    /// | 2020-08-21 | 20       |
463    /// +------------+----------+
464    /// ```
465    #[deprecated(since = "0.24.1", note = "use polars.lazy aggregations")]
466    pub fn max(&self) -> PolarsResult<DataFrame> {
467        let (mut cols, agg_cols) = self.prepare_agg()?;
468        for agg_col in agg_cols {
469            let new_name = fmt_group_by_column(agg_col.name().as_str(), GroupByMethod::Max);
470            let mut agg = unsafe { agg_col.agg_max(&self.groups) };
471            agg.rename(new_name);
472            cols.push(agg);
473        }
474        DataFrame::new_infer_height(cols)
475    }
476
477    /// Aggregate grouped `Series` and find the first value per group.
478    ///
479    /// # Example
480    ///
481    /// ```rust
482    /// # use polars_core::prelude::*;
483    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
484    ///     df.group_by(["date"])?.select(["temp"]).first()
485    /// }
486    /// ```
487    /// Returns:
488    ///
489    /// ```text
490    /// +------------+------------+
491    /// | date       | temp_first |
492    /// | ---        | ---        |
493    /// | Date       | i32        |
494    /// +============+============+
495    /// | 2020-08-23 | 9          |
496    /// +------------+------------+
497    /// | 2020-08-22 | 7          |
498    /// +------------+------------+
499    /// | 2020-08-21 | 20         |
500    /// +------------+------------+
501    /// ```
502    #[deprecated(since = "0.24.1", note = "use polars.lazy aggregations")]
503    pub fn first(&self) -> PolarsResult<DataFrame> {
504        let (mut cols, agg_cols) = self.prepare_agg()?;
505        for agg_col in agg_cols {
506            let new_name = fmt_group_by_column(agg_col.name().as_str(), GroupByMethod::First);
507            let mut agg = unsafe { agg_col.agg_first(&self.groups) };
508            agg.rename(new_name);
509            cols.push(agg);
510        }
511        DataFrame::new_infer_height(cols)
512    }
513
514    /// Aggregate grouped `Series` and return the last value per group.
515    ///
516    /// # Example
517    ///
518    /// ```rust
519    /// # use polars_core::prelude::*;
520    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
521    ///     df.group_by(["date"])?.select(["temp"]).last()
522    /// }
523    /// ```
524    /// Returns:
525    ///
526    /// ```text
527    /// +------------+------------+
528    /// | date       | temp_last |
529    /// | ---        | ---        |
530    /// | Date       | i32        |
531    /// +============+============+
532    /// | 2020-08-23 | 9          |
533    /// +------------+------------+
534    /// | 2020-08-22 | 1          |
535    /// +------------+------------+
536    /// | 2020-08-21 | 10         |
537    /// +------------+------------+
538    /// ```
539    #[deprecated(since = "0.24.1", note = "use polars.lazy aggregations")]
540    pub fn last(&self) -> PolarsResult<DataFrame> {
541        let (mut cols, agg_cols) = self.prepare_agg()?;
542        for agg_col in agg_cols {
543            let new_name = fmt_group_by_column(agg_col.name().as_str(), GroupByMethod::Last);
544            let mut agg = unsafe { agg_col.agg_last(&self.groups) };
545            agg.rename(new_name);
546            cols.push(agg);
547        }
548        DataFrame::new_infer_height(cols)
549    }
550
551    /// Aggregate grouped `Series` by counting the number of unique values.
552    ///
553    /// # Example
554    ///
555    /// ```rust
556    /// # use polars_core::prelude::*;
557    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
558    ///     df.group_by(["date"])?.select(["temp"]).n_unique()
559    /// }
560    /// ```
561    /// Returns:
562    ///
563    /// ```text
564    /// +------------+---------------+
565    /// | date       | temp_n_unique |
566    /// | ---        | ---           |
567    /// | Date       | u32           |
568    /// +============+===============+
569    /// | 2020-08-23 | 1             |
570    /// +------------+---------------+
571    /// | 2020-08-22 | 2             |
572    /// +------------+---------------+
573    /// | 2020-08-21 | 2             |
574    /// +------------+---------------+
575    /// ```
576    #[deprecated(since = "0.24.1", note = "use polars.lazy aggregations")]
577    pub fn n_unique(&self) -> PolarsResult<DataFrame> {
578        let (mut cols, agg_cols) = self.prepare_agg()?;
579        for agg_col in agg_cols {
580            let new_name = fmt_group_by_column(agg_col.name().as_str(), GroupByMethod::NUnique);
581            let mut agg = unsafe { agg_col.agg_n_unique(&self.groups) };
582            agg.rename(new_name);
583            cols.push(agg);
584        }
585        DataFrame::new_infer_height(cols)
586    }
587
588    /// Aggregate grouped [`Series`] and determine the quantile per group.
589    ///
590    /// # Example
591    ///
592    /// ```rust
593    /// # use polars_core::prelude::*;
594    ///
595    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
596    ///     df.group_by(["date"])?.select(["temp"]).quantile(0.2, QuantileMethod::default())
597    /// }
598    /// ```
599    #[deprecated(since = "0.24.1", note = "use polars.lazy aggregations")]
600    pub fn quantile(&self, quantile: f64, method: QuantileMethod) -> PolarsResult<DataFrame> {
601        polars_ensure!(
602            (0.0..=1.0).contains(&quantile),
603            ComputeError: "`quantile` should be within 0.0 and 1.0"
604        );
605        let (mut cols, agg_cols) = self.prepare_agg()?;
606        for agg_col in agg_cols {
607            let new_name = fmt_group_by_column(
608                agg_col.name().as_str(),
609                GroupByMethod::Quantile(quantile, method),
610            );
611            let mut agg = unsafe { agg_col.agg_quantile(&self.groups, quantile, method) };
612            agg.rename(new_name);
613            cols.push(agg);
614        }
615        DataFrame::new_infer_height(cols)
616    }
617
618    /// Aggregate grouped [`Series`] and determine the median per group.
619    ///
620    /// # Example
621    ///
622    /// ```rust
623    /// # use polars_core::prelude::*;
624    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
625    ///     df.group_by(["date"])?.select(["temp"]).median()
626    /// }
627    /// ```
628    #[deprecated(since = "0.24.1", note = "use polars.lazy aggregations")]
629    pub fn median(&self) -> PolarsResult<DataFrame> {
630        let (mut cols, agg_cols) = self.prepare_agg()?;
631        for agg_col in agg_cols {
632            let new_name = fmt_group_by_column(agg_col.name().as_str(), GroupByMethod::Median);
633            let mut agg = unsafe { agg_col.agg_median(&self.groups) };
634            agg.rename(new_name);
635            cols.push(agg);
636        }
637        DataFrame::new_infer_height(cols)
638    }
639
640    /// Aggregate grouped [`Series`] and determine the variance per group.
641    #[deprecated(since = "0.24.1", note = "use polars.lazy aggregations")]
642    pub fn var(&self, ddof: u8) -> PolarsResult<DataFrame> {
643        let (mut cols, agg_cols) = self.prepare_agg()?;
644        for agg_col in agg_cols {
645            let new_name = fmt_group_by_column(agg_col.name().as_str(), GroupByMethod::Var(ddof));
646            let mut agg = unsafe { agg_col.agg_var(&self.groups, ddof) };
647            agg.rename(new_name);
648            cols.push(agg);
649        }
650        DataFrame::new_infer_height(cols)
651    }
652
653    /// Aggregate grouped [`Series`] and determine the standard deviation per group.
654    #[deprecated(since = "0.24.1", note = "use polars.lazy aggregations")]
655    pub fn std(&self, ddof: u8) -> PolarsResult<DataFrame> {
656        let (mut cols, agg_cols) = self.prepare_agg()?;
657        for agg_col in agg_cols {
658            let new_name = fmt_group_by_column(agg_col.name().as_str(), GroupByMethod::Std(ddof));
659            let mut agg = unsafe { agg_col.agg_std(&self.groups, ddof) };
660            agg.rename(new_name);
661            cols.push(agg);
662        }
663        DataFrame::new_infer_height(cols)
664    }
665
666    /// Aggregate grouped series and compute the number of values per group.
667    ///
668    /// # Example
669    ///
670    /// ```rust
671    /// # use polars_core::prelude::*;
672    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
673    ///     df.group_by(["date"])?.select(["temp"]).count()
674    /// }
675    /// ```
676    /// Returns:
677    ///
678    /// ```text
679    /// +------------+------------+
680    /// | date       | temp_count |
681    /// | ---        | ---        |
682    /// | Date       | u32        |
683    /// +============+============+
684    /// | 2020-08-23 | 1          |
685    /// +------------+------------+
686    /// | 2020-08-22 | 2          |
687    /// +------------+------------+
688    /// | 2020-08-21 | 2          |
689    /// +------------+------------+
690    /// ```
691    pub fn count(&self) -> PolarsResult<DataFrame> {
692        let (mut cols, agg_cols) = self.prepare_agg()?;
693
694        for agg_col in agg_cols {
695            let new_name = fmt_group_by_column(
696                agg_col.name().as_str(),
697                GroupByMethod::Count {
698                    include_nulls: true,
699                },
700            );
701            let mut ca = self.groups.group_count();
702            ca.rename(new_name);
703            cols.push(ca.into_column());
704        }
705        DataFrame::new_infer_height(cols)
706    }
707
708    /// Get the group_by group indexes.
709    ///
710    /// # Example
711    ///
712    /// ```rust
713    /// # use polars_core::prelude::*;
714    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
715    ///     df.group_by(["date"])?.groups()
716    /// }
717    /// ```
718    /// Returns:
719    ///
720    /// ```text
721    /// +--------------+------------+
722    /// | date         | groups     |
723    /// | ---          | ---        |
724    /// | Date(days)   | list [u32] |
725    /// +==============+============+
726    /// | 2020-08-23   | "[3]"      |
727    /// +--------------+------------+
728    /// | 2020-08-22   | "[2, 4]"   |
729    /// +--------------+------------+
730    /// | 2020-08-21   | "[0, 1]"   |
731    /// +--------------+------------+
732    /// ```
733    pub fn groups(&self) -> PolarsResult<DataFrame> {
734        let mut cols = self.keys();
735        let mut column = self.groups.as_list_chunked();
736        let new_name = fmt_group_by_column("", GroupByMethod::Groups);
737        column.rename(new_name);
738        cols.push(column.into_column());
739        DataFrame::new_infer_height(cols)
740    }
741
742    fn prepare_apply(&self) -> PolarsResult<DataFrame> {
743        if let Some(agg) = &self.selected_agg {
744            if agg.is_empty() {
745                Ok(self.df.clone())
746            } else {
747                let mut new_cols = Vec::with_capacity(self.selected_keys.len() + agg.len());
748                new_cols.extend_from_slice(&self.selected_keys);
749                let cols = self.df.select_to_vec(agg.as_slice())?;
750                new_cols.extend(cols);
751                Ok(unsafe { DataFrame::new_unchecked(self.df.height(), new_cols) })
752            }
753        } else {
754            Ok(self.df.clone())
755        }
756    }
757
758    /// Apply a closure over the groups as a new [`DataFrame`] in parallel.
759    #[deprecated(since = "0.24.1", note = "use polars.lazy aggregations")]
760    pub fn par_apply<F>(&self, f: F) -> PolarsResult<DataFrame>
761    where
762        F: Fn(DataFrame) -> PolarsResult<DataFrame> + Send + Sync,
763    {
764        polars_ensure!(self.df.height() > 0, ComputeError: "cannot group_by + apply on empty 'DataFrame'");
765        let df = self.prepare_apply()?;
766        let dfs = self
767            .get_groups()
768            .par_iter()
769            .map(|g| {
770                // SAFETY:
771                // groups are in bounds
772                let sub_df = unsafe { take_df(&df, g) };
773                f(sub_df)
774            })
775            .collect::<PolarsResult<Vec<_>>>()?;
776
777        let mut df = accumulate_dataframes_vertical(dfs)?;
778        df.rechunk_mut_par();
779        Ok(df)
780    }
781
782    /// Apply a closure over the groups as a new [`DataFrame`].
783    pub fn apply<F>(&self, f: F) -> PolarsResult<DataFrame>
784    where
785        F: FnMut(DataFrame) -> PolarsResult<DataFrame> + Send + Sync,
786    {
787        self.apply_sliced(None, f, None)
788    }
789
790    pub fn apply_sliced<F>(
791        &self,
792        slice: Option<(i64, usize)>,
793        mut f: F,
794        schema: Option<&SchemaRef>,
795    ) -> PolarsResult<DataFrame>
796    where
797        F: FnMut(DataFrame) -> PolarsResult<DataFrame> + Send + Sync,
798    {
799        if self.df.height() == 0 {
800            // return empty dataframe with correct schema
801            if let Some(schema) = schema {
802                return Ok(DataFrame::empty_with_arc_schema(schema.clone()));
803            }
804
805            polars_bail!(ComputeError: "cannot group_by + apply on empty 'DataFrame'");
806        }
807
808        let df = self.prepare_apply()?;
809        let max_height = if let Some((offset, len)) = slice {
810            offset.try_into().unwrap_or(usize::MAX).saturating_add(len)
811        } else {
812            usize::MAX
813        };
814        let mut height = 0;
815        let mut dfs = Vec::with_capacity(self.get_groups().len());
816        for g in self.get_groups().iter() {
817            // SAFETY: groups are in bounds.
818            let sub_df = unsafe { take_df(&df, g) };
819            let df = f(sub_df)?;
820            height += df.height();
821            dfs.push(df);
822
823            // Even if max_height is zero we need at least one df, so check
824            // after first push.
825            if height >= max_height {
826                break;
827            }
828        }
829
830        let mut df = accumulate_dataframes_vertical(dfs)?;
831        if let Some((offset, len)) = slice {
832            df = df.slice(offset, len);
833        }
834        Ok(df)
835    }
836
837    pub fn sliced(mut self, slice: Option<(i64, usize)>) -> Self {
838        match slice {
839            None => self,
840            Some((offset, length)) => {
841                self.groups = self.groups.slice(offset, length);
842                self.selected_keys = self.keys_sliced(slice);
843                self
844            },
845        }
846    }
847}
848
849unsafe fn take_df(df: &DataFrame, g: GroupsIndicator) -> DataFrame {
850    match g {
851        GroupsIndicator::Idx(idx) => df.take_slice_unchecked(idx.1),
852        GroupsIndicator::Slice([first, len]) => df.slice(first as i64, len as usize),
853    }
854}
855
856#[derive(Copy, Clone, Debug)]
857pub enum GroupByMethod {
858    Min,
859    NanMin,
860    Max,
861    NanMax,
862    Median,
863    Mean,
864    First,
865    FirstNonNull,
866    Last,
867    LastNonNull,
868    Item { allow_empty: bool },
869    Sum,
870    Groups,
871    NUnique,
872    Quantile(f64, QuantileMethod),
873    Count { include_nulls: bool },
874    Implode { maintain_order: bool },
875    Std(u8),
876    Var(u8),
877    ArgMin,
878    ArgMax,
879}
880
881impl Display for GroupByMethod {
882    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
883        use GroupByMethod::*;
884        let s = match self {
885            Min => "min",
886            NanMin => "nan_min",
887            Max => "max",
888            NanMax => "nan_max",
889            Median => "median",
890            Mean => "mean",
891            First => "first",
892            FirstNonNull => "first_non_null",
893            Last => "last",
894            LastNonNull => "last_non_null",
895            Item { .. } => "item",
896            Sum => "sum",
897            Groups => "groups",
898            NUnique => "n_unique",
899            Quantile(_, _) => "quantile",
900            Count { .. } => "count",
901            Implode { .. } => "implode",
902            Std(_) => "std",
903            Var(_) => "var",
904            ArgMin => "arg_min",
905            ArgMax => "arg_max",
906        };
907        write!(f, "{s}")
908    }
909}
910
911// Formatting functions used in eager and lazy code for renaming grouped columns
912pub fn fmt_group_by_column(name: &str, method: GroupByMethod) -> PlSmallStr {
913    use GroupByMethod::*;
914    match method {
915        Min => format_pl_smallstr!("{name}_min"),
916        Max => format_pl_smallstr!("{name}_max"),
917        NanMin => format_pl_smallstr!("{name}_nan_min"),
918        NanMax => format_pl_smallstr!("{name}_nan_max"),
919        Median => format_pl_smallstr!("{name}_median"),
920        Mean => format_pl_smallstr!("{name}_mean"),
921        First => format_pl_smallstr!("{name}_first"),
922        FirstNonNull => format_pl_smallstr!("{name}_first_non_null"),
923        Last => format_pl_smallstr!("{name}_last"),
924        LastNonNull => format_pl_smallstr!("{name}_last_non_null"),
925        Item { .. } => format_pl_smallstr!("{name}_item"),
926        Sum => format_pl_smallstr!("{name}_sum"),
927        Groups => PlSmallStr::from_static("groups"),
928        NUnique => format_pl_smallstr!("{name}_n_unique"),
929        Count { .. } => format_pl_smallstr!("{name}_count"),
930        Implode { .. } => format_pl_smallstr!("{name}_agg_list"),
931        Quantile(quantile, _interpol) => format_pl_smallstr!("{name}_quantile_{quantile:.2}"),
932        Std(_) => format_pl_smallstr!("{name}_agg_std"),
933        Var(_) => format_pl_smallstr!("{name}_agg_var"),
934        ArgMin => format_pl_smallstr!("{name}_arg_min"),
935        ArgMax => format_pl_smallstr!("{name}_arg_max"),
936    }
937}
938
939#[cfg(test)]
940mod test {
941    use num_traits::FloatConst;
942
943    use crate::prelude::*;
944
945    #[test]
946    #[cfg(feature = "dtype-date")]
947    #[cfg_attr(miri, ignore)]
948    fn test_group_by() -> PolarsResult<()> {
949        let s0 = Column::new(
950            PlSmallStr::from_static("date"),
951            &[
952                "2020-08-21",
953                "2020-08-21",
954                "2020-08-22",
955                "2020-08-23",
956                "2020-08-22",
957            ],
958        );
959        let s1 = Column::new(PlSmallStr::from_static("temp"), [20, 10, 7, 9, 1]);
960        let s2 = Column::new(PlSmallStr::from_static("rain"), [0.2, 0.1, 0.3, 0.1, 0.01]);
961        let df = DataFrame::new_infer_height(vec![s0, s1, s2]).unwrap();
962
963        let out = df.group_by_stable(["date"])?.select(["temp"]).count()?;
964        assert_eq!(
965            out.column("temp_count")?,
966            &Column::new(PlSmallStr::from_static("temp_count"), [2 as IdxSize, 2, 1])
967        );
968
969        // Use of deprecated mean() for testing purposes
970        #[allow(deprecated)]
971        // Select multiple
972        let out = df
973            .group_by_stable(["date"])?
974            .select(["temp", "rain"])
975            .mean()?;
976        assert_eq!(
977            out.column("temp_mean")?,
978            &Column::new(PlSmallStr::from_static("temp_mean"), [15.0f64, 4.0, 9.0])
979        );
980
981        // Use of deprecated `mean()` for testing purposes
982        #[allow(deprecated)]
983        // Group by multiple
984        let out = df
985            .group_by_stable(["date", "temp"])?
986            .select(["rain"])
987            .mean()?;
988        assert!(out.column("rain_mean").is_ok());
989
990        // Use of deprecated `sum()` for testing purposes
991        #[allow(deprecated)]
992        let out = df.group_by_stable(["date"])?.select(["temp"]).sum()?;
993        assert_eq!(
994            out.column("temp_sum")?,
995            &Column::new(PlSmallStr::from_static("temp_sum"), [30, 8, 9])
996        );
997
998        // Use of deprecated `n_unique()` for testing purposes
999        #[allow(deprecated)]
1000        // implicit select all and only aggregate on methods that support that aggregation
1001        let gb = df.group_by(["date"]).unwrap().n_unique().unwrap();
1002        // check the group by column is filtered out.
1003        assert_eq!(gb.width(), 3);
1004        Ok(())
1005    }
1006
1007    #[test]
1008    #[cfg_attr(miri, ignore)]
1009    fn test_static_group_by_by_12_columns() {
1010        // Build GroupBy DataFrame.
1011        let s0 = Column::new("G1".into(), ["A", "A", "B", "B", "C"].as_ref());
1012        let s1 = Column::new("N".into(), [1, 2, 2, 4, 2].as_ref());
1013        let s2 = Column::new("G2".into(), ["k", "l", "m", "m", "l"].as_ref());
1014        let s3 = Column::new("G3".into(), ["a", "b", "c", "c", "d"].as_ref());
1015        let s4 = Column::new("G4".into(), ["1", "2", "3", "3", "4"].as_ref());
1016        let s5 = Column::new("G5".into(), ["X", "Y", "Z", "Z", "W"].as_ref());
1017        let s6 = Column::new("G6".into(), [false, true, true, true, false].as_ref());
1018        let s7 = Column::new("G7".into(), ["r", "x", "q", "q", "o"].as_ref());
1019        let s8 = Column::new("G8".into(), ["R", "X", "Q", "Q", "O"].as_ref());
1020        let s9 = Column::new("G9".into(), [1, 2, 3, 3, 4].as_ref());
1021        let s10 = Column::new("G10".into(), [".", "!", "?", "?", "/"].as_ref());
1022        let s11 = Column::new("G11".into(), ["(", ")", "@", "@", "$"].as_ref());
1023        let s12 = Column::new("G12".into(), ["-", "_", ";", ";", ","].as_ref());
1024
1025        let df = DataFrame::new_infer_height(vec![
1026            s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12,
1027        ])
1028        .unwrap();
1029
1030        // Use of deprecated `sum()` for testing purposes
1031        #[allow(deprecated)]
1032        let adf = df
1033            .group_by([
1034                "G1", "G2", "G3", "G4", "G5", "G6", "G7", "G8", "G9", "G10", "G11", "G12",
1035            ])
1036            .unwrap()
1037            .select(["N"])
1038            .sum()
1039            .unwrap();
1040
1041        assert_eq!(
1042            Vec::from(&adf.column("N_sum").unwrap().i32().unwrap().sort(false)),
1043            &[Some(1), Some(2), Some(2), Some(6)]
1044        );
1045    }
1046
1047    #[test]
1048    #[cfg_attr(miri, ignore)]
1049    fn test_dynamic_group_by_by_13_columns() {
1050        // The content for every group_by series.
1051        let series_content = ["A", "A", "B", "B", "C"];
1052
1053        // The name of every group_by series.
1054        let series_names = [
1055            "G1", "G2", "G3", "G4", "G5", "G6", "G7", "G8", "G9", "G10", "G11", "G12", "G13",
1056        ];
1057
1058        // Vector to contain every series.
1059        let mut columns = Vec::with_capacity(14);
1060
1061        // Create a series for every group name.
1062        for series_name in series_names {
1063            let group_columns = Column::new(series_name.into(), series_content.as_ref());
1064            columns.push(group_columns);
1065        }
1066
1067        // Create a series for the aggregation column.
1068        let agg_series = Column::new("N".into(), [1, 2, 3, 3, 4].as_ref());
1069        columns.push(agg_series);
1070
1071        // Create the dataframe with the computed series.
1072        let df = DataFrame::new_infer_height(columns).unwrap();
1073
1074        // Use of deprecated `sum()` for testing purposes
1075        #[allow(deprecated)]
1076        // Compute the aggregated DataFrame by the 13 columns defined in `series_names`.
1077        let adf = df
1078            .group_by(series_names)
1079            .unwrap()
1080            .select(["N"])
1081            .sum()
1082            .unwrap();
1083
1084        // Check that the results of the group-by are correct. The content of every column
1085        // is equal, then, the grouped columns shall be equal and in the same order.
1086        for series_name in &series_names {
1087            assert_eq!(
1088                Vec::from(&adf.column(series_name).unwrap().str().unwrap().sort(false)),
1089                &[Some("A"), Some("B"), Some("C")]
1090            );
1091        }
1092
1093        // Check the aggregated column is the expected one.
1094        assert_eq!(
1095            Vec::from(&adf.column("N_sum").unwrap().i32().unwrap().sort(false)),
1096            &[Some(3), Some(4), Some(6)]
1097        );
1098    }
1099
1100    #[test]
1101    #[cfg_attr(miri, ignore)]
1102    fn test_group_by_floats() {
1103        let df = df! {"flt" => [1., 1., 2., 2., 3.],
1104                    "val" => [1, 1, 1, 1, 1]
1105        }
1106        .unwrap();
1107        // Use of deprecated `sum()` for testing purposes
1108        #[allow(deprecated)]
1109        let res = df.group_by(["flt"]).unwrap().sum().unwrap();
1110        let res = res.sort(["flt"], SortMultipleOptions::default()).unwrap();
1111        assert_eq!(
1112            Vec::from(res.column("val_sum").unwrap().i32().unwrap()),
1113            &[Some(2), Some(2), Some(1)]
1114        );
1115    }
1116
1117    #[test]
1118    #[cfg_attr(miri, ignore)]
1119    #[cfg(feature = "dtype-categorical")]
1120    fn test_group_by_categorical() {
1121        let mut df = df! {"foo" => ["a", "a", "b", "b", "c"],
1122                    "ham" => ["a", "a", "b", "b", "c"],
1123                    "bar" => [1, 1, 1, 1, 1]
1124        }
1125        .unwrap();
1126
1127        df.apply("foo", |s| {
1128            s.cast(&DataType::from_categories(Categories::global()))
1129                .unwrap()
1130        })
1131        .unwrap();
1132
1133        // Use of deprecated `sum()` for testing purposes
1134        #[allow(deprecated)]
1135        // check multiple keys and categorical
1136        let res = df
1137            .group_by_stable(["foo", "ham"])
1138            .unwrap()
1139            .select(["bar"])
1140            .sum()
1141            .unwrap();
1142
1143        assert_eq!(
1144            Vec::from(
1145                res.column("bar_sum")
1146                    .unwrap()
1147                    .as_materialized_series()
1148                    .i32()
1149                    .unwrap()
1150            ),
1151            &[Some(2), Some(2), Some(1)]
1152        );
1153    }
1154
1155    #[test]
1156    #[cfg_attr(miri, ignore)]
1157    fn test_group_by_null_handling() -> PolarsResult<()> {
1158        let df = df!(
1159            "a" => ["a", "a", "a", "b", "b"],
1160            "b" => [Some(1), Some(2), None, None, Some(1)]
1161        )?;
1162        // Use of deprecated `mean()` for testing purposes
1163        #[allow(deprecated)]
1164        let out = df.group_by_stable(["a"])?.mean()?;
1165
1166        assert_eq!(
1167            Vec::from(out.column("b_mean")?.as_materialized_series().f64()?),
1168            &[Some(1.5), Some(1.0)]
1169        );
1170        Ok(())
1171    }
1172
1173    #[test]
1174    #[cfg_attr(miri, ignore)]
1175    fn test_group_by_var() -> PolarsResult<()> {
1176        // check variance and proper coercion to f64
1177        let df = df![
1178            "g" => ["foo", "foo", "bar"],
1179            "flt" => [1.0, 2.0, 3.0],
1180            "int" => [1, 2, 3]
1181        ]?;
1182
1183        // Use of deprecated `sum()` for testing purposes
1184        #[allow(deprecated)]
1185        let out = df.group_by_stable(["g"])?.select(["int"]).var(1)?;
1186
1187        assert_eq!(out.column("int_agg_var")?.f64()?.get(0), Some(0.5));
1188        // Use of deprecated `std()` for testing purposes
1189        #[allow(deprecated)]
1190        let out = df.group_by_stable(["g"])?.select(["int"]).std(1)?;
1191        let val = out.column("int_agg_std")?.f64()?.get(0).unwrap();
1192        let expected = f64::FRAC_1_SQRT_2();
1193        assert!((val - expected).abs() < 0.000001);
1194        Ok(())
1195    }
1196
1197    #[test]
1198    #[cfg_attr(miri, ignore)]
1199    #[cfg(feature = "dtype-categorical")]
1200    fn test_group_by_null_group() -> PolarsResult<()> {
1201        // check if null is own group
1202        let mut df = df![
1203            "g" => [Some("foo"), Some("foo"), Some("bar"), None, None],
1204            "flt" => [1.0, 2.0, 3.0, 1.0, 1.0],
1205            "int" => [1, 2, 3, 1, 1]
1206        ]?;
1207
1208        df.try_apply("g", |s| {
1209            s.cast(&DataType::from_categories(Categories::global()))
1210        })?;
1211
1212        // Use of deprecated `sum()` for testing purposes
1213        #[allow(deprecated)]
1214        let _ = df.group_by(["g"])?.sum()?;
1215        Ok(())
1216    }
1217}