Skip to content

Commit 5dca2d3

Browse files
committed
Support pl.Expr.product in cudf_polars
1 parent 15eb369 commit 5dca2d3

4 files changed

Lines changed: 45 additions & 4 deletions

File tree

python/cudf_polars/cudf_polars/dsl/expressions/aggregation.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ def __init__(
118118
req = plc.aggregation.mean()
119119
elif name == "sum":
120120
req = plc.aggregation.sum()
121+
elif name == "product":
122+
req = plc.aggregation.product()
121123
elif name == "std":
122124
# TODO: handle nans
123125
req = plc.aggregation.std(ddof=options)
@@ -172,7 +174,7 @@ def __init__(
172174
op = partial(op, propagate_nans=options)
173175
elif name == "count":
174176
op = partial(op, include_nulls=options)
175-
elif name in {"sum", "first", "last", "item", "first_non_null"}:
177+
elif name in {"sum", "product", "first", "last", "item", "first_non_null"}:
176178
pass
177179
else:
178180
raise NotImplementedError(
@@ -194,6 +196,7 @@ def __init__(
194196
"m2",
195197
"merge_m2",
196198
"sum",
199+
"product",
197200
"count",
198201
"std",
199202
"var",
@@ -280,6 +283,20 @@ def _sum(self, column: Column, stream: Stream) -> Column:
280283
)
281284
return self._reduce(column, request=plc.aggregation.sum(), stream=stream)
282285

286+
def _product(self, column: Column, stream: Stream) -> Column:
287+
if column.size == 0 or column.null_count == column.size:
288+
# The product of an empty or all-null column is 1 in polars.
289+
return Column(
290+
plc.Column.from_scalar(
291+
plc.Scalar.from_py(1, self.dtype.plc_type, stream=stream),
292+
1,
293+
stream=stream,
294+
),
295+
name=column.name,
296+
dtype=self.dtype,
297+
)
298+
return self._reduce(column, request=plc.aggregation.product(), stream=stream)
299+
283300
def _min(self, column: Column, *, propagate_nans: bool, stream: Stream) -> Column:
284301
nan_count = column.nan_count(stream=stream)
285302
if propagate_nans and nan_count > 0:

python/cudf_polars/cudf_polars/dsl/translate.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,8 @@ def _(
10021002
)
10031003
elif name == "pow":
10041004
return expr.BinOp(dtype, plc.binaryop.BinaryOperator.POW, *children)
1005+
elif name == "product":
1006+
return expr.Agg(dtype, "product", None, translator._expr_context, *children)
10051007
elif not POLARS_VERSION_LT_141 and name == "quantile":
10061008
# polars >= 1.41 emits quantile as a string-named Function
10071009
# expression (function_data=("quantile", interpolation)) with the

python/cudf_polars/cudf_polars/dsl/utils/aggregations.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ def decompose_single_agg(
321321
new_children = [child] if not is_quantile else [child, agg.children[1]]
322322
named_expr = named_expr.reconstruct(agg.reconstruct(new_children))
323323

324-
if agg.name == "sum":
324+
if agg.name in {"sum", "product"}:
325325
col = (
326326
expr.Cast(
327327
agg.dtype,
@@ -339,8 +339,10 @@ def decompose_single_agg(
339339
# - ROLLING: sum(all-null window) => null; sum(empty window) => 0 (fill only if empty)
340340
#
341341
# Must post-process because libcudf returns null for both empty and all-null windows/groups
342+
# product uses an identity of 1 (empty/all-null product is 1 in polars).
343+
identity = 1 if agg.name == "product" else 0
342344
return [(named_expr, True)], expr.NamedExpr(
343-
name, replace_nulls(col, 0, is_top=is_top)
345+
name, replace_nulls(col, identity, is_top=is_top)
344346
)
345347
elif agg.name in {"mean", "median", "quantile", "std", "var"}:
346348
post_agg_col: expr.Expr = expr.Col(

python/cudf_polars/tests/expressions/test_agg.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES.
1+
# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33
from __future__ import annotations
44

@@ -112,6 +112,26 @@ def test_bool_agg(engine: pl.GPUEngine, agg):
112112
assert_gpu_result_equal(q, engine=engine, check_exact=False)
113113

114114

115+
@pytest.mark.parametrize(
116+
"data,dtype",
117+
[
118+
([1, 2, 3, 4], pl.Int32),
119+
([1, 2, None, 4], pl.Int32),
120+
([1, 0, 3], pl.Int32),
121+
([2, 3, 4], pl.Int8),
122+
([1.5, 2.0, 3.0], pl.Float64),
123+
([1.5, None, 3.0], pl.Float64),
124+
([True, False, True], pl.Boolean),
125+
([], pl.Int32),
126+
([None, None], pl.Int32),
127+
],
128+
)
129+
def test_product(engine: pl.GPUEngine, data, dtype):
130+
df = pl.LazyFrame({"a": pl.Series(data, dtype=dtype)})
131+
q = df.select(pl.col("a").product())
132+
assert_gpu_result_equal(q, engine=engine, check_exact=False)
133+
134+
115135
@pytest.mark.parametrize("cum_agg", sorted(expr.UnaryFunction._supported_cum_aggs))
116136
def test_cum_agg_reverse_unsupported(engine: pl.GPUEngine, cum_agg):
117137
df = pl.LazyFrame({"a": [1, 2, 3]})

0 commit comments

Comments
 (0)