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_with_layout, encode_rows_with_layout_par, get_optimal_row_encoding_layout,
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 opts = vec![RowEncodingOptions::new_unsorted(); by.len()];
89                let layout = get_optimal_row_encoding_layout(&by, &[], &opts);
90                let rows = if multithreaded {
91                    encode_rows_with_layout_par(&by, &layout, false)?
92                } else {
93                    encode_rows_with_layout(&by, &layout, false)?
94                };
95                rows.group_tuples(multithreaded, sorted)
96            }
97        };
98        Ok(GroupBy::new(self, by, groups?.into_sliceable(), None))
99    }
100
101    /// Group DataFrame using a Series column.
102    ///
103    /// # Example
104    ///
105    /// ```
106    /// use polars_core::prelude::*;
107    /// fn group_by_example(df: &DataFrame) -> PolarsResult<DataFrame> {
108    ///     df.group_by(["column_name"])?
109    ///     .select(["agg_column_name"])
110    ///     .groups()
111    /// }
112    /// ```
113    pub fn group_by<I, S>(&self, by: I) -> PolarsResult<GroupBy<'_>>
114    where
115        I: IntoIterator<Item = S>,
116        S: AsRef<str>,
117    {
118        let selected_keys = self.select_to_vec(by)?;
119        self.group_by_with_series(selected_keys, true, false)
120    }
121
122    /// Group DataFrame using a Series column.
123    /// The groups are ordered by their smallest row index.
124    pub fn group_by_stable<I, S>(&self, by: I) -> PolarsResult<GroupBy<'_>>
125    where
126        I: IntoIterator<Item = S>,
127        S: AsRef<str>,
128    {
129        let selected_keys = self.select_to_vec(by)?;
130        self.group_by_with_series(selected_keys, true, true)
131    }
132}
133
134/// Returned by a group_by operation on a DataFrame. This struct supports
135/// several aggregations.
136///
137/// Until described otherwise, the examples in this struct are performed on the following DataFrame:
138///
139/// ```ignore
140/// use polars_core::prelude::*;
141///
142/// let dates = &[
143/// "2020-08-21",
144/// "2020-08-21",
145/// "2020-08-22",
146/// "2020-08-23",
147/// "2020-08-22",
148/// ];
149/// // date format
150/// let fmt = "%Y-%m-%d";
151/// // create date series
152/// let s0 = DateChunked::parse_from_str_slice("date", dates, fmt)
153///         .into_series();
154/// // create temperature series
155/// let s1 = Series::new("temp".into(), [20, 10, 7, 9, 1]);
156/// // create rain series
157/// let s2 = Series::new("rain".into(), [0.2, 0.1, 0.3, 0.1, 0.01]);
158/// // create a new DataFrame
159/// let df = DataFrame::new_infer_height(vec![s0, s1, s2]).unwrap();
160/// println!("{:?}", df);
161/// ```
162///
163/// Outputs:
164///
165/// ```text
166/// +------------+------+------+
167/// | date       | temp | rain |
168/// | ---        | ---  | ---  |
169/// | Date       | i32  | f64  |
170/// +============+======+======+
171/// | 2020-08-21 | 20   | 0.2  |
172/// +------------+------+------+
173/// | 2020-08-21 | 10   | 0.1  |
174/// +------------+------+------+
175/// | 2020-08-22 | 7    | 0.3  |
176/// +------------+------+------+
177/// | 2020-08-23 | 9    | 0.1  |
178/// +------------+------+------+
179/// | 2020-08-22 | 1    | 0.01 |
180/// +------------+------+------+
181/// ```
182///
183#[derive(Debug, Clone)]
184pub struct GroupBy<'a> {
185    pub df: &'a DataFrame,
186    pub(crate) selected_keys: Vec<Column>,
187    // [first idx, [other idx]]
188    groups: GroupPositions,
189    // columns selected for aggregation
190    pub(crate) selected_agg: Option<Vec<PlSmallStr>>,
191}
192
193impl<'a> GroupBy<'a> {
194    pub fn new(
195        df: &'a DataFrame,
196        by: Vec<Column>,
197        groups: GroupPositions,
198        selected_agg: Option<Vec<PlSmallStr>>,
199    ) -> Self {
200        GroupBy {
201            df,
202            selected_keys: by,
203            groups,
204            selected_agg,
205        }
206    }
207
208    /// Select the column(s) that should be aggregated.
209    /// You can select a single column or a slice of columns.
210    ///
211    /// Note that making a selection with this method is not required. If you
212    /// skip it all columns (except for the keys) will be selected for aggregation.
213    #[must_use]
214    pub fn select<I: IntoIterator<Item = S>, S: Into<PlSmallStr>>(mut self, selection: I) -> Self {
215        self.selected_agg = Some(selection.into_iter().map(|s| s.into()).collect());
216        self
217    }
218
219    /// Get the internal representation of the GroupBy operation.
220    /// The Vec returned contains:
221    ///     (first_idx, [`Vec<indexes>`])
222    ///     Where second value in the tuple is a vector with all matching indexes.
223    pub fn get_groups(&self) -> &GroupPositions {
224        &self.groups
225    }
226
227    pub fn into_groups(self) -> GroupPositions {
228        self.groups
229    }
230
231    pub fn keys_sliced(&self, slice: Option<(i64, usize)>) -> Vec<Column> {
232        #[allow(unused_assignments)]
233        // needed to keep the lifetimes valid for this scope
234        let mut groups_owned = None;
235
236        let groups = if let Some((offset, len)) = slice {
237            groups_owned = Some(self.groups.slice(offset, len));
238            groups_owned.as_deref().unwrap()
239        } else {
240            &self.groups
241        };
242        RAYON.install(|| {
243            self.selected_keys
244                .par_iter()
245                .map(Column::as_materialized_series)
246                .map(|s| {
247                    match groups {
248                        GroupsType::Idx(groups) => {
249                            // SAFETY: groups are always in bounds.
250                            let mut out = unsafe { s.take_slice_unchecked(groups.first()) };
251                            if groups.sorted_by_first_idx {
252                                out.set_sorted_flag(s.is_sorted_flag());
253                            };
254                            out
255                        },
256                        GroupsType::Slice {
257                            groups,
258                            overlapping,
259                            monotonic: _,
260                        } => {
261                            if *overlapping && !groups.is_empty() {
262                                // Groups can be sliced.
263                                let offset = groups[0][0];
264                                let [upper_offset, upper_len] = groups[groups.len() - 1];
265                                return s.slice(
266                                    offset as i64,
267                                    ((upper_offset + upper_len) - offset) as usize,
268                                );
269                            }
270
271                            let indices = groups
272                                .iter()
273                                .map(|&[first, _len]| first)
274                                .collect_ca(PlSmallStr::EMPTY);
275                            // SAFETY: groups are always in bounds.
276                            let mut out = unsafe { s.take_unchecked(&indices) };
277                            // Sliced groups are always in order of discovery.
278                            out.set_sorted_flag(s.is_sorted_flag());
279                            out
280                        },
281                    }
282                })
283                .map(Column::from)
284                .collect()
285        })
286    }
287
288    pub fn keys(&self) -> Vec<Column> {
289        self.keys_sliced(None)
290    }
291
292    fn prepare_agg(&self) -> PolarsResult<(Vec<Column>, Vec<Column>)> {
293        let keys = self.keys();
294
295        let agg_col = match &self.selected_agg {
296            Some(selection) => self.df.select_to_vec(selection),
297            None => {
298                let by: Vec<_> = self.selected_keys.iter().map(|s| s.name()).collect();
299                let selection = self
300                    .df
301                    .columns()
302                    .iter()
303                    .map(|s| s.name())
304                    .filter(|a| !by.contains(a))
305                    .cloned()
306                    .collect::<Vec<_>>();
307
308                self.df.select_to_vec(selection.as_slice())
309            },
310        }?;
311
312        Ok((keys, agg_col))
313    }
314
315    /// Aggregate grouped series and compute the number of values per group.
316    ///
317    /// # Example
318    ///
319    /// ```rust
320    /// # use polars_core::prelude::*;
321    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
322    ///     df.group_by(["date"])?.select(["temp"]).count()
323    /// }
324    /// ```
325    /// Returns:
326    ///
327    /// ```text
328    /// +------------+------------+
329    /// | date       | temp_count |
330    /// | ---        | ---        |
331    /// | Date       | u32        |
332    /// +============+============+
333    /// | 2020-08-23 | 1          |
334    /// +------------+------------+
335    /// | 2020-08-22 | 2          |
336    /// +------------+------------+
337    /// | 2020-08-21 | 2          |
338    /// +------------+------------+
339    /// ```
340    pub fn count(&self) -> PolarsResult<DataFrame> {
341        let (mut cols, agg_cols) = self.prepare_agg()?;
342
343        for agg_col in agg_cols {
344            let new_name = fmt_group_by_column(
345                agg_col.name().as_str(),
346                GroupByMethod::Count {
347                    include_nulls: true,
348                },
349            );
350            let mut ca = self.groups.group_count();
351            ca.rename(new_name);
352            cols.push(ca.into_column());
353        }
354        DataFrame::new_infer_height(cols)
355    }
356
357    /// Get the group_by group indexes.
358    ///
359    /// # Example
360    ///
361    /// ```rust
362    /// # use polars_core::prelude::*;
363    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
364    ///     df.group_by(["date"])?.groups()
365    /// }
366    /// ```
367    /// Returns:
368    ///
369    /// ```text
370    /// +--------------+------------+
371    /// | date         | groups     |
372    /// | ---          | ---        |
373    /// | Date(days)   | list [u32] |
374    /// +==============+============+
375    /// | 2020-08-23   | "[3]"      |
376    /// +--------------+------------+
377    /// | 2020-08-22   | "[2, 4]"   |
378    /// +--------------+------------+
379    /// | 2020-08-21   | "[0, 1]"   |
380    /// +--------------+------------+
381    /// ```
382    pub fn groups(&self) -> PolarsResult<DataFrame> {
383        let mut cols = self.keys();
384        let mut column = self.groups.as_list_chunked();
385        let new_name = fmt_group_by_column("", GroupByMethod::Groups);
386        column.rename(new_name);
387        cols.push(column.into_column());
388        DataFrame::new_infer_height(cols)
389    }
390
391    fn prepare_apply(&self) -> PolarsResult<DataFrame> {
392        if let Some(agg) = &self.selected_agg {
393            if agg.is_empty() {
394                Ok(self.df.clone())
395            } else {
396                let mut new_cols = Vec::with_capacity(self.selected_keys.len() + agg.len());
397                new_cols.extend_from_slice(&self.selected_keys);
398                let cols = self.df.select_to_vec(agg.as_slice())?;
399                new_cols.extend(cols);
400                Ok(unsafe { DataFrame::new_unchecked(self.df.height(), new_cols) })
401            }
402        } else {
403            Ok(self.df.clone())
404        }
405    }
406
407    /// Apply a closure over the groups as a new [`DataFrame`].
408    pub fn apply<F>(&self, f: F) -> PolarsResult<DataFrame>
409    where
410        F: FnMut(DataFrame) -> PolarsResult<DataFrame> + Send + Sync,
411    {
412        self.apply_sliced(None, f, None)
413    }
414
415    pub fn apply_sliced<F>(
416        &self,
417        slice: Option<(i64, usize)>,
418        mut f: F,
419        schema: Option<&SchemaRef>,
420    ) -> PolarsResult<DataFrame>
421    where
422        F: FnMut(DataFrame) -> PolarsResult<DataFrame> + Send + Sync,
423    {
424        // Rolling and dynamic group-bys can have rows but no windows.
425        if self.df.height() == 0 || self.get_groups().is_empty() {
426            if let Some(schema) = schema {
427                return Ok(DataFrame::empty_with_arc_schema(schema.clone()));
428            }
429
430            polars_bail!(ComputeError: "cannot group_by + apply on empty 'DataFrame'");
431        }
432
433        let df = self.prepare_apply()?;
434        let max_height = if let Some((offset, len)) = slice {
435            offset.try_into().unwrap_or(usize::MAX).saturating_add(len)
436        } else {
437            usize::MAX
438        };
439        let mut height = 0;
440        let mut dfs = Vec::with_capacity(self.get_groups().len());
441        for g in self.get_groups().iter() {
442            // SAFETY: groups are in bounds.
443            let sub_df = unsafe { take_df(&df, g) };
444            let df = f(sub_df)?;
445            height += df.height();
446            dfs.push(df);
447
448            // Even if max_height is zero we need at least one df, so check
449            // after first push.
450            if height >= max_height {
451                break;
452            }
453        }
454
455        let mut df = accumulate_dataframes_vertical(dfs)?;
456        if let Some((offset, len)) = slice {
457            df = df.slice(offset, len);
458        }
459        Ok(df)
460    }
461
462    pub fn sliced(mut self, slice: Option<(i64, usize)>) -> Self {
463        match slice {
464            None => self,
465            Some((offset, length)) => {
466                self.groups = self.groups.slice(offset, length);
467                self.selected_keys = self.keys_sliced(slice);
468                self
469            },
470        }
471    }
472}
473
474unsafe fn take_df(df: &DataFrame, g: GroupsIndicator) -> DataFrame {
475    match g {
476        GroupsIndicator::Idx(idx) => df.take_slice_unchecked(idx.1),
477        GroupsIndicator::Slice([first, len]) => df.slice(first as i64, len as usize),
478    }
479}
480
481#[derive(Copy, Clone, Debug)]
482pub enum GroupByMethod {
483    Min,
484    NanMin,
485    Max,
486    NanMax,
487    Median,
488    Mean,
489    First,
490    FirstNonNull,
491    Last,
492    LastNonNull,
493    Item { allow_empty: bool },
494    Sum { null_on_empty: bool },
495    Groups,
496    NUnique,
497    Quantile(f64, QuantileMethod),
498    Count { include_nulls: bool },
499    Implode { maintain_order: bool },
500    Std(u8),
501    Var(u8),
502    ArgMin,
503    ArgMax,
504}
505
506impl Display for GroupByMethod {
507    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
508        use GroupByMethod::*;
509        let s = match self {
510            Min => "min",
511            NanMin => "nan_min",
512            Max => "max",
513            NanMax => "nan_max",
514            Median => "median",
515            Mean => "mean",
516            First => "first",
517            FirstNonNull => "first_non_null",
518            Last => "last",
519            LastNonNull => "last_non_null",
520            Item { .. } => "item",
521            Sum { .. } => "sum",
522            Groups => "groups",
523            NUnique => "n_unique",
524            Quantile(_, _) => "quantile",
525            Count { .. } => "count",
526            Implode { .. } => "implode",
527            Std(_) => "std",
528            Var(_) => "var",
529            ArgMin => "arg_min",
530            ArgMax => "arg_max",
531        };
532        write!(f, "{s}")
533    }
534}
535
536// Formatting functions used in eager and lazy code for renaming grouped columns
537pub fn fmt_group_by_column(name: &str, method: GroupByMethod) -> PlSmallStr {
538    use GroupByMethod::*;
539    match method {
540        Min => format_pl_smallstr!("{name}_min"),
541        Max => format_pl_smallstr!("{name}_max"),
542        NanMin => format_pl_smallstr!("{name}_nan_min"),
543        NanMax => format_pl_smallstr!("{name}_nan_max"),
544        Median => format_pl_smallstr!("{name}_median"),
545        Mean => format_pl_smallstr!("{name}_mean"),
546        First => format_pl_smallstr!("{name}_first"),
547        FirstNonNull => format_pl_smallstr!("{name}_first_non_null"),
548        Last => format_pl_smallstr!("{name}_last"),
549        LastNonNull => format_pl_smallstr!("{name}_last_non_null"),
550        Item { .. } => format_pl_smallstr!("{name}_item"),
551        Sum { .. } => format_pl_smallstr!("{name}_sum"),
552        Groups => PlSmallStr::from_static("groups"),
553        NUnique => format_pl_smallstr!("{name}_n_unique"),
554        Count { .. } => format_pl_smallstr!("{name}_count"),
555        Implode { .. } => format_pl_smallstr!("{name}_agg_list"),
556        Quantile(quantile, _interpol) => format_pl_smallstr!("{name}_quantile_{quantile:.2}"),
557        Std(_) => format_pl_smallstr!("{name}_agg_std"),
558        Var(_) => format_pl_smallstr!("{name}_agg_var"),
559        ArgMin => format_pl_smallstr!("{name}_arg_min"),
560        ArgMax => format_pl_smallstr!("{name}_arg_max"),
561    }
562}