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_example(df: &DataFrame) -> PolarsResult<DataFrame> {
107    ///     df.group_by(["column_name"])?
108    ///     .select(["agg_column_name"])
109    ///     .groups()
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 number of values 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"]).count()
334    /// }
335    /// ```
336    /// Returns:
337    ///
338    /// ```text
339    /// +------------+------------+
340    /// | date       | temp_count |
341    /// | ---        | ---        |
342    /// | Date       | u32        |
343    /// +============+============+
344    /// | 2020-08-23 | 1          |
345    /// +------------+------------+
346    /// | 2020-08-22 | 2          |
347    /// +------------+------------+
348    /// | 2020-08-21 | 2          |
349    /// +------------+------------+
350    /// ```
351    pub fn count(&self) -> PolarsResult<DataFrame> {
352        let (mut cols, agg_cols) = self.prepare_agg()?;
353
354        for agg_col in agg_cols {
355            let new_name = fmt_group_by_column(
356                agg_col.name().as_str(),
357                GroupByMethod::Count {
358                    include_nulls: true,
359                },
360            );
361            let mut ca = self.groups.group_count();
362            ca.rename(new_name);
363            cols.push(ca.into_column());
364        }
365        DataFrame::new_infer_height(cols)
366    }
367
368    /// Get the group_by group indexes.
369    ///
370    /// # Example
371    ///
372    /// ```rust
373    /// # use polars_core::prelude::*;
374    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
375    ///     df.group_by(["date"])?.groups()
376    /// }
377    /// ```
378    /// Returns:
379    ///
380    /// ```text
381    /// +--------------+------------+
382    /// | date         | groups     |
383    /// | ---          | ---        |
384    /// | Date(days)   | list [u32] |
385    /// +==============+============+
386    /// | 2020-08-23   | "[3]"      |
387    /// +--------------+------------+
388    /// | 2020-08-22   | "[2, 4]"   |
389    /// +--------------+------------+
390    /// | 2020-08-21   | "[0, 1]"   |
391    /// +--------------+------------+
392    /// ```
393    pub fn groups(&self) -> PolarsResult<DataFrame> {
394        let mut cols = self.keys();
395        let mut column = self.groups.as_list_chunked();
396        let new_name = fmt_group_by_column("", GroupByMethod::Groups);
397        column.rename(new_name);
398        cols.push(column.into_column());
399        DataFrame::new_infer_height(cols)
400    }
401
402    fn prepare_apply(&self) -> PolarsResult<DataFrame> {
403        if let Some(agg) = &self.selected_agg {
404            if agg.is_empty() {
405                Ok(self.df.clone())
406            } else {
407                let mut new_cols = Vec::with_capacity(self.selected_keys.len() + agg.len());
408                new_cols.extend_from_slice(&self.selected_keys);
409                let cols = self.df.select_to_vec(agg.as_slice())?;
410                new_cols.extend(cols);
411                Ok(unsafe { DataFrame::new_unchecked(self.df.height(), new_cols) })
412            }
413        } else {
414            Ok(self.df.clone())
415        }
416    }
417
418    /// Apply a closure over the groups as a new [`DataFrame`].
419    pub fn apply<F>(&self, f: F) -> PolarsResult<DataFrame>
420    where
421        F: FnMut(DataFrame) -> PolarsResult<DataFrame> + Send + Sync,
422    {
423        self.apply_sliced(None, f, None)
424    }
425
426    pub fn apply_sliced<F>(
427        &self,
428        slice: Option<(i64, usize)>,
429        mut f: F,
430        schema: Option<&SchemaRef>,
431    ) -> PolarsResult<DataFrame>
432    where
433        F: FnMut(DataFrame) -> PolarsResult<DataFrame> + Send + Sync,
434    {
435        if self.df.height() == 0 {
436            // return empty dataframe with correct schema
437            if let Some(schema) = schema {
438                return Ok(DataFrame::empty_with_arc_schema(schema.clone()));
439            }
440
441            polars_bail!(ComputeError: "cannot group_by + apply on empty 'DataFrame'");
442        }
443
444        let df = self.prepare_apply()?;
445        let max_height = if let Some((offset, len)) = slice {
446            offset.try_into().unwrap_or(usize::MAX).saturating_add(len)
447        } else {
448            usize::MAX
449        };
450        let mut height = 0;
451        let mut dfs = Vec::with_capacity(self.get_groups().len());
452        for g in self.get_groups().iter() {
453            // SAFETY: groups are in bounds.
454            let sub_df = unsafe { take_df(&df, g) };
455            let df = f(sub_df)?;
456            height += df.height();
457            dfs.push(df);
458
459            // Even if max_height is zero we need at least one df, so check
460            // after first push.
461            if height >= max_height {
462                break;
463            }
464        }
465
466        let mut df = accumulate_dataframes_vertical(dfs)?;
467        if let Some((offset, len)) = slice {
468            df = df.slice(offset, len);
469        }
470        Ok(df)
471    }
472
473    pub fn sliced(mut self, slice: Option<(i64, usize)>) -> Self {
474        match slice {
475            None => self,
476            Some((offset, length)) => {
477                self.groups = self.groups.slice(offset, length);
478                self.selected_keys = self.keys_sliced(slice);
479                self
480            },
481        }
482    }
483}
484
485unsafe fn take_df(df: &DataFrame, g: GroupsIndicator) -> DataFrame {
486    match g {
487        GroupsIndicator::Idx(idx) => df.take_slice_unchecked(idx.1),
488        GroupsIndicator::Slice([first, len]) => df.slice(first as i64, len as usize),
489    }
490}
491
492#[derive(Copy, Clone, Debug)]
493pub enum GroupByMethod {
494    Min,
495    NanMin,
496    Max,
497    NanMax,
498    Median,
499    Mean,
500    First,
501    FirstNonNull,
502    Last,
503    LastNonNull,
504    Item { allow_empty: bool },
505    Sum,
506    Groups,
507    NUnique,
508    Quantile(f64, QuantileMethod),
509    Count { include_nulls: bool },
510    Implode { maintain_order: bool },
511    Std(u8),
512    Var(u8),
513    ArgMin,
514    ArgMax,
515}
516
517impl Display for GroupByMethod {
518    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
519        use GroupByMethod::*;
520        let s = match self {
521            Min => "min",
522            NanMin => "nan_min",
523            Max => "max",
524            NanMax => "nan_max",
525            Median => "median",
526            Mean => "mean",
527            First => "first",
528            FirstNonNull => "first_non_null",
529            Last => "last",
530            LastNonNull => "last_non_null",
531            Item { .. } => "item",
532            Sum => "sum",
533            Groups => "groups",
534            NUnique => "n_unique",
535            Quantile(_, _) => "quantile",
536            Count { .. } => "count",
537            Implode { .. } => "implode",
538            Std(_) => "std",
539            Var(_) => "var",
540            ArgMin => "arg_min",
541            ArgMax => "arg_max",
542        };
543        write!(f, "{s}")
544    }
545}
546
547// Formatting functions used in eager and lazy code for renaming grouped columns
548pub fn fmt_group_by_column(name: &str, method: GroupByMethod) -> PlSmallStr {
549    use GroupByMethod::*;
550    match method {
551        Min => format_pl_smallstr!("{name}_min"),
552        Max => format_pl_smallstr!("{name}_max"),
553        NanMin => format_pl_smallstr!("{name}_nan_min"),
554        NanMax => format_pl_smallstr!("{name}_nan_max"),
555        Median => format_pl_smallstr!("{name}_median"),
556        Mean => format_pl_smallstr!("{name}_mean"),
557        First => format_pl_smallstr!("{name}_first"),
558        FirstNonNull => format_pl_smallstr!("{name}_first_non_null"),
559        Last => format_pl_smallstr!("{name}_last"),
560        LastNonNull => format_pl_smallstr!("{name}_last_non_null"),
561        Item { .. } => format_pl_smallstr!("{name}_item"),
562        Sum => format_pl_smallstr!("{name}_sum"),
563        Groups => PlSmallStr::from_static("groups"),
564        NUnique => format_pl_smallstr!("{name}_n_unique"),
565        Count { .. } => format_pl_smallstr!("{name}_count"),
566        Implode { .. } => format_pl_smallstr!("{name}_agg_list"),
567        Quantile(quantile, _interpol) => format_pl_smallstr!("{name}_quantile_{quantile:.2}"),
568        Std(_) => format_pl_smallstr!("{name}_agg_std"),
569        Var(_) => format_pl_smallstr!("{name}_agg_var"),
570        ArgMin => format_pl_smallstr!("{name}_arg_min"),
571        ArgMax => format_pl_smallstr!("{name}_arg_max"),
572    }
573}