Skip to content

Commit aafd7a7

Browse files
feat: Add having to group_by context (pola-rs#23550)
Co-authored-by: coastalwhite <me@gburghoorn.com>
1 parent 29e2035 commit aafd7a7

12 files changed

Lines changed: 334 additions & 46 deletions

File tree

crates/polars-lazy/src/frame/mod.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,7 @@ impl LazyFrame {
11001100
logical_plan: self.logical_plan,
11011101
opt_state,
11021102
keys,
1103+
predicates: vec![],
11031104
maintain_order: false,
11041105
dynamic_options: None,
11051106
rolling_options: None,
@@ -1112,6 +1113,7 @@ impl LazyFrame {
11121113
logical_plan: self.logical_plan,
11131114
opt_state,
11141115
keys,
1116+
predicates: vec![],
11151117
maintain_order: false,
11161118
}
11171119
}
@@ -1147,6 +1149,7 @@ impl LazyFrame {
11471149
LazyGroupBy {
11481150
logical_plan: self.logical_plan,
11491151
opt_state,
1152+
predicates: vec![],
11501153
keys: group_by.as_ref().to_vec(),
11511154
maintain_order: true,
11521155
dynamic_options: None,
@@ -1192,6 +1195,7 @@ impl LazyFrame {
11921195
LazyGroupBy {
11931196
logical_plan: self.logical_plan,
11941197
opt_state,
1198+
predicates: vec![],
11951199
keys: group_by.as_ref().to_vec(),
11961200
maintain_order: true,
11971201
dynamic_options: Some(options),
@@ -1214,6 +1218,7 @@ impl LazyFrame {
12141218
logical_plan: self.logical_plan,
12151219
opt_state,
12161220
keys,
1221+
predicates: vec![],
12171222
maintain_order: true,
12181223
dynamic_options: None,
12191224
rolling_options: None,
@@ -1226,6 +1231,7 @@ impl LazyFrame {
12261231
logical_plan: self.logical_plan,
12271232
opt_state,
12281233
keys,
1234+
predicates: vec![],
12291235
maintain_order: true,
12301236
}
12311237
}
@@ -1975,6 +1981,7 @@ pub struct LazyGroupBy {
19751981
pub logical_plan: DslPlan,
19761982
opt_state: OptFlags,
19771983
keys: Vec<Expr>,
1984+
predicates: Vec<Expr>,
19781985
maintain_order: bool,
19791986
#[cfg(feature = "dynamic_group_by")]
19801987
dynamic_options: Option<DynamicGroupOptions>,
@@ -1993,6 +2000,31 @@ impl From<LazyGroupBy> for LazyFrame {
19932000
}
19942001

19952002
impl LazyGroupBy {
2003+
/// Filter groups with a predicate after aggregation.
2004+
///
2005+
/// Similarly to the [LazyGroupBy::agg] method, the predicate must run an aggregation as it
2006+
/// is evaluated on the groups.
2007+
/// This method can be chained in which case all predicates must evaluate to `true` for a
2008+
/// group to be kept.
2009+
///
2010+
/// # Example
2011+
///
2012+
/// ```rust
2013+
/// use polars_core::prelude::*;
2014+
/// use polars_lazy::prelude::*;
2015+
///
2016+
/// fn example(df: DataFrame) -> LazyFrame {
2017+
/// df.lazy()
2018+
/// .group_by_stable([col("date")])
2019+
/// .having(col("rain").sum().gt(lit(10)))
2020+
/// .agg([col("rain").min().alias("min_rain")])
2021+
/// }
2022+
/// ```
2023+
pub fn having(mut self, predicate: Expr) -> Self {
2024+
self.predicates.push(predicate);
2025+
self
2026+
}
2027+
19962028
/// Group by and aggregate.
19972029
///
19982030
/// Select a column with [col] and choose an aggregation.
@@ -2019,6 +2051,7 @@ impl LazyGroupBy {
20192051
let lp = DslBuilder::from(self.logical_plan)
20202052
.group_by(
20212053
self.keys,
2054+
self.predicates,
20222055
aggs,
20232056
None,
20242057
self.maintain_order,
@@ -2029,7 +2062,7 @@ impl LazyGroupBy {
20292062

20302063
#[cfg(not(feature = "dynamic_group_by"))]
20312064
let lp = DslBuilder::from(self.logical_plan)
2032-
.group_by(self.keys, aggs, None, self.maintain_order)
2065+
.group_by(self.keys, self.predicates, aggs, None, self.maintain_order)
20332066
.build();
20342067
LazyFrame::from_logical_plan(lp, self.opt_state)
20352068
}
@@ -2075,6 +2108,10 @@ impl LazyGroupBy {
20752108
/// **It is not recommended that you use this as materializing the DataFrame is very
20762109
/// expensive.**
20772110
pub fn apply(self, f: PlanCallback<DataFrame, DataFrame>, schema: SchemaRef) -> LazyFrame {
2111+
if !self.predicates.is_empty() {
2112+
panic!("not yet implemented: `apply` cannot be used with `having` predicates");
2113+
}
2114+
20782115
#[cfg(feature = "dynamic_group_by")]
20792116
let options = GroupbyOptions {
20802117
dynamic: self.dynamic_options,
@@ -2088,6 +2125,7 @@ impl LazyGroupBy {
20882125
let lp = DslPlan::GroupBy {
20892126
input: Arc::new(self.logical_plan),
20902127
keys: self.keys,
2128+
predicates: vec![],
20912129
aggs: vec![],
20922130
apply: Some((f, schema)),
20932131
maintain_order: self.maintain_order,

crates/polars-plan/dsl-schema-hashes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"Dimension": "68880cdb10230df6c8c1632b073c80bd8ceb5c56a368c0cb438431ca9f3d3b31",
4444
"DistinctOptionsDSL": "41be5ec69ef9a614f2b36ac5deadfecdea5cca847ae1ada9d4bc626ff52a5b38",
4545
"DslFunction": "221f1a46a043c8ed54f57be981bf24509f04f5f91f0f08e0acc180d96f842ebf",
46-
"DslPlan": "f9d1c8f632172a24dbaf76e56b2efab237937d2a2c615858d6f196932ada98f6",
46+
"DslPlan": "2c776aa38f65fd85707d969967aa733e3f91a1002a200ca3a3ef7c1328c313eb",
4747
"Duration": "44999d59023085cbb592ce94b30d34f9b983081fc72bd6435a49bdf0869c0074",
4848
"DynListLiteralValue": "2266a553cb4a943f7097f24539eaa802453cf8742675996215235bd682dec0e8",
4949
"DynLiteralValue": "47dc404f42bef5ab71659b9e10a97413202a61bfa3ac9fc66fff4a176653f7fe",

crates/polars-plan/src/dsl/builder_dsl.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,11 @@ impl DslBuilder {
241241
.into()
242242
}
243243

244+
#[allow(clippy::too_many_arguments)]
244245
pub fn group_by<E: AsRef<[Expr]>>(
245246
self,
246247
keys: Vec<Expr>,
248+
predicates: Vec<Expr>,
247249
aggs: E,
248250
apply: Option<(PlanCallback<DataFrame, DataFrame>, SchemaRef)>,
249251
maintain_order: bool,
@@ -262,6 +264,7 @@ impl DslBuilder {
262264
DslPlan::GroupBy {
263265
input: Arc::new(self.0),
264266
keys,
267+
predicates,
265268
aggs,
266269
apply,
267270
maintain_order,

crates/polars-plan/src/dsl/plan.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub enum DslPlan {
6666
GroupBy {
6767
input: Arc<DslPlan>,
6868
keys: Vec<Expr>,
69+
predicates: Vec<Expr>,
6970
aggs: Vec<Expr>,
7071
maintain_order: bool,
7172
options: Arc<GroupbyOptions>,
@@ -190,7 +191,7 @@ impl Clone for DslPlan {
190191
Self::Scan { sources, unified_scan_args, scan_type, cached_ir } => Self::Scan { sources: sources.clone(), unified_scan_args: unified_scan_args.clone(), scan_type: scan_type.clone(), cached_ir: cached_ir.clone() },
191192
Self::DataFrameScan { df, schema, } => Self::DataFrameScan { df: df.clone(), schema: schema.clone(), },
192193
Self::Select { expr, input, options } => Self::Select { expr: expr.clone(), input: input.clone(), options: options.clone() },
193-
Self::GroupBy { input, keys, aggs, apply, maintain_order, options } => Self::GroupBy { input: input.clone(), keys: keys.clone(), aggs: aggs.clone(), apply: apply.clone(), maintain_order: maintain_order.clone(), options: options.clone() },
194+
Self::GroupBy { input, keys, predicates, aggs, apply, maintain_order, options } => Self::GroupBy { input: input.clone(), keys: keys.clone(), predicates: predicates.clone(), aggs: aggs.clone(), apply: apply.clone(), maintain_order: maintain_order.clone(), options: options.clone() },
194195
Self::Join { input_left, input_right, left_on, right_on, predicates, options } => Self::Join { input_left: input_left.clone(), input_right: input_right.clone(), left_on: left_on.clone(), right_on: right_on.clone(), options: options.clone(), predicates: predicates.clone() },
195196
Self::HStack { input, exprs, options } => Self::HStack { input: input.clone(), exprs: exprs.clone(), options: options.clone() },
196197
Self::MatchToSchema { input, match_schema, per_column, extra_columns } => Self::MatchToSchema { input: input.clone(), match_schema: match_schema.clone(), per_column: per_column.clone(), extra_columns: *extra_columns },

crates/polars-plan/src/dsl/serializable_plan.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub(crate) enum SerializableDslPlanNode {
6161
input: DslPlanKey,
6262
keys: Vec<Expr>,
6363
aggs: Vec<Expr>,
64+
predicates: Vec<Expr>,
6465
maintain_order: bool,
6566
options: Arc<GroupbyOptions>,
6667
apply: Option<(PlanCallback<DataFrame, DataFrame>, SchemaRef)>,
@@ -218,13 +219,15 @@ fn convert_dsl_plan_to_serializable_plan(
218219
input,
219220
keys,
220221
aggs,
222+
predicates,
221223
maintain_order,
222224
options,
223225
apply,
224226
} => SP::GroupBy {
225227
input: dsl_plan_key(input, arenas),
226228
keys: keys.clone(),
227229
aggs: aggs.clone(),
230+
predicates: predicates.clone(),
228231
maintain_order: *maintain_order,
229232
options: options.clone(),
230233
apply: apply.clone(),
@@ -454,13 +457,15 @@ fn try_convert_serializable_plan_to_dsl_plan(
454457
input,
455458
keys,
456459
aggs,
460+
predicates,
457461
maintain_order,
458462
options,
459463
apply,
460464
} => Ok(DP::GroupBy {
461465
input: get_dsl_plan(*input, ser_dsl_plan, arenas)?,
462466
keys: keys.clone(),
463467
aggs: aggs.clone(),
468+
predicates: predicates.clone(),
464469
maintain_order: *maintain_order,
465470
options: options.clone(),
466471
apply: apply.clone(),

crates/polars-plan/src/plans/conversion/dsl_to_ir/mod.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use hive::hive_partitions_from_paths;
55
use polars_core::chunked_array::cast::CastOptions;
66
use polars_core::config::verbose;
77
use polars_utils::format_pl_smallstr;
8+
use polars_utils::itertools::Itertools;
89
use polars_utils::plpath::PlPath;
910
use polars_utils::unique_id::UniqueId;
1011

@@ -460,11 +461,51 @@ pub fn to_alp_impl(lp: DslPlan, ctxt: &mut DslConversionContext) -> PolarsResult
460461
DslPlan::GroupBy {
461462
input,
462463
keys,
463-
aggs,
464+
predicates,
465+
mut aggs,
464466
apply,
465467
maintain_order,
466468
options,
467469
} => {
470+
// If the group by contains any predicates, we update the plan by turning the
471+
// predicates into aggregations and filtering on them. Then, we recursively call
472+
// this function.
473+
if !predicates.is_empty() {
474+
let predicate_names = (0..predicates.len())
475+
.map(|i| PlSmallStr::from_string(format!("__POLARS_HAVING_{i}")))
476+
.collect::<Arc<[_]>>();
477+
let predicates = predicates
478+
.into_iter()
479+
.zip(predicate_names.iter())
480+
.map(|(p, name)| p.alias(name.clone()))
481+
.collect_vec();
482+
aggs.extend(predicates);
483+
484+
let lp = DslPlan::GroupBy {
485+
input,
486+
keys,
487+
predicates: vec![],
488+
aggs,
489+
apply,
490+
maintain_order,
491+
options,
492+
};
493+
let lp = DslBuilder::from(lp)
494+
.filter(
495+
all_horizontal(
496+
predicate_names.iter().map(|n| col(n.clone())).collect_vec(),
497+
)
498+
.unwrap(),
499+
)
500+
.drop(Selector::ByName {
501+
names: predicate_names,
502+
strict: true,
503+
})
504+
.build();
505+
return to_alp_impl(lp, ctxt);
506+
}
507+
508+
// NOTE: As we went into this branch, we know that no predicates are provided.
468509
let input =
469510
to_alp_impl(owned(input), ctxt).map_err(|e| e.context(failed_here!(group_by)))?;
470511

@@ -504,7 +545,6 @@ pub fn to_alp_impl(lp: DslPlan, ctxt: &mut DslConversionContext) -> PolarsResult
504545
maintain_order,
505546
options,
506547
};
507-
508548
return run_conversion(lp, ctxt, "group_by")
509549
.map_err(|e| e.context(failed_here!(group_by)));
510550
},
@@ -1476,6 +1516,7 @@ fn resolve_group_by(
14761516

14771517
Ok((keys, aggs, Arc::new(output_schema)))
14781518
}
1519+
14791520
fn stats_helper<F, E>(condition: F, expr: E, schema: &Schema) -> Vec<Expr>
14801521
where
14811522
F: Fn(&DataType) -> bool,

crates/polars-python/src/lazygroupby.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ pub struct PyLazyGroupBy {
1919

2020
#[pymethods]
2121
impl PyLazyGroupBy {
22+
fn having(&self, predicates: Vec<PyExpr>) -> PyLazyGroupBy {
23+
let mut lgb = self.lgb.clone().unwrap();
24+
let predicates = predicates.to_exprs();
25+
for predicate in predicates.into_iter() {
26+
lgb = lgb.having(predicate);
27+
}
28+
PyLazyGroupBy { lgb: Some(lgb) }
29+
}
30+
2231
fn agg(&self, aggs: Vec<PyExpr>) -> PyLazyFrame {
2332
let lgb = self.lgb.clone().unwrap();
2433
let aggs = aggs.to_exprs();

py-polars/src/polars/_plr.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2282,6 +2282,7 @@ class PyLazyGroupBy:
22822282
def agg(self, aggs: list[PyExpr]) -> PyLazyFrame: ...
22832283
def head(self, n: int) -> PyLazyFrame: ...
22842284
def tail(self, n: int) -> PyLazyFrame: ...
2285+
def having(self, predicates: list[PyExpr]) -> PyLazyGroupBy: ...
22852286
def map_groups(
22862287
self, lambda_function: Any, schema: Schema | None
22872288
) -> PyLazyFrame: ...

py-polars/src/polars/dataframe/frame.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7063,7 +7063,9 @@ def group_by(
70637063
f" group_by({value!r})"
70647064
)
70657065
raise TypeError(msg)
7066-
return GroupBy(self, *by, **named_by, maintain_order=maintain_order)
7066+
return GroupBy(
7067+
self, *by, **named_by, maintain_order=maintain_order, predicates=None
7068+
)
70677069

70687070
@deprecate_renamed_parameter("by", "group_by", version="0.20.14")
70697071
def rolling(
@@ -7220,6 +7222,7 @@ def rolling(
72207222
offset=offset,
72217223
closed=closed,
72227224
group_by=group_by,
7225+
predicates=None,
72237226
)
72247227

72257228
@deprecate_renamed_parameter("by", "group_by", version="0.20.14")
@@ -7540,6 +7543,7 @@ def group_by_dynamic(
75407543
closed=closed,
75417544
group_by=group_by,
75427545
start_by=start_by,
7546+
predicates=None,
75437547
)
75447548

75457549
@deprecate_renamed_parameter("by", "group_by", version="0.20.14")

0 commit comments

Comments
 (0)