From a7d3895341429907714e459a433f5ddab834158b Mon Sep 17 00:00:00 2001 From: J van Zundert Date: Sun, 2 Apr 2023 17:39:40 +0100 Subject: [PATCH] feat(python): Support Numpy ufunc with more than one expression (#7924) --- py-polars/polars/expr/expr.py | 14 ++++++++----- py-polars/tests/unit/test_df.py | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/py-polars/polars/expr/expr.py b/py-polars/polars/expr/expr.py index 87d87a35397b..a7661a0261bd 100644 --- a/py-polars/polars/expr/expr.py +++ b/py-polars/polars/expr/expr.py @@ -7,7 +7,7 @@ import random import warnings from datetime import timedelta -from functools import reduce +from functools import partial, reduce from typing import ( TYPE_CHECKING, Any, @@ -47,7 +47,7 @@ with contextlib.suppress(ImportError): # Module not available when building docs from polars.polars import arg_where as py_arg_where - + from polars.polars import reduce as pyreduce if TYPE_CHECKING: import sys @@ -282,9 +282,13 @@ def __array_ufunc__( """Numpy universal functions.""" num_expr = sum(isinstance(inp, Expr) for inp in inputs) if num_expr > 1: - raise ValueError( - f"Numpy ufunc can only be used with one expression, {num_expr} given. Use `pl.reduce` to call numpy functions over multiple expressions." - ) + if num_expr < len(inputs): + raise ValueError( + "Numpy ufunc with more than one expression can only be used if all non-expression inputs are provided as keyword arguments only" + ) + + exprs = selection_to_pyexpr_list(inputs) + return self._from_pyexpr(pyreduce(partial(ufunc, **kwargs), exprs)) def function(s: Series) -> Series: # pragma: no cover args = [inp if not isinstance(inp, Expr) else s for inp in inputs] diff --git a/py-polars/tests/unit/test_df.py b/py-polars/tests/unit/test_df.py index b27cb177e95d..aff71e1073a9 100644 --- a/py-polars/tests/unit/test_df.py +++ b/py-polars/tests/unit/test_df.py @@ -3654,6 +3654,41 @@ def test_ufunc_expr_not_first() -> None: assert_frame_equal(out, expected) +def test_ufunc_multiple_expressions() -> None: + # example from https://github.com/pola-rs/polars/issues/6770 + df = pl.DataFrame( + { + "v": [ + -4.293, + -2.4659, + -1.8378, + -0.2821, + -4.5649, + -3.8128, + -7.4274, + 3.3443, + 3.8604, + -4.2200, + ], + "u": [ + -11.2268, + 6.3478, + 7.1681, + 3.4986, + 2.7320, + -1.0695, + -10.1408, + 11.2327, + 6.6623, + -8.1412, + ], + } + ) + expected = np.arctan2(df.get_column("v"), df.get_column("u")) + result = df.select(np.arctan2(pl.col("v"), pl.col("u")))[:, 0] # type: ignore[call-overload] + assert_series_equal(expected, result) # type: ignore[arg-type] + + def test_window_deadlock() -> None: np.random.seed(12)