From f777def94326d197f654056b65edfa9cf11d0bc4 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 1 Feb 2026 11:07:42 +0100 Subject: [PATCH 1/6] feat: Allow scalars in horizontal expressions --- narwhals/_arrow/namespace.py | 24 +++++++-------- narwhals/_dask/namespace.py | 8 ++--- narwhals/_pandas_like/namespace.py | 8 +++-- narwhals/functions.py | 26 +++++++++++++---- tests/expr_and_series/all_horizontal_test.py | 23 ++++++++++++++- tests/expr_and_series/any_horizontal_test.py | 22 ++++++++++++++ tests/expr_and_series/max_horizontal_test.py | 22 ++++++++++++++ tests/expr_and_series/mean_horizontal_test.py | 29 +++++++++++++++---- tests/expr_and_series/min_horizontal_test.py | 22 ++++++++++++++ tests/expr_and_series/sum_horizontal_test.py | 23 ++++++++++++++- 10 files changed, 174 insertions(+), 33 deletions(-) diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py index 83bb1c2bce..f0ad2b7386 100644 --- a/narwhals/_arrow/namespace.py +++ b/narwhals/_arrow/namespace.py @@ -126,8 +126,8 @@ def mean_horizontal(self, *exprs: ArrowExpr) -> ArrowExpr: def func(df: ArrowDataFrame) -> list[ArrowSeries]: expr_results = tuple(chain.from_iterable(expr(df) for expr in exprs)) - series = [s.fill_null(0, strategy=None, limit=None) for s in expr_results] - non_na = [1 - s.is_null().cast(int_64) for s in expr_results] + series = (s.fill_null(0, strategy=None, limit=None) for s in expr_results) + non_na = (1 - s.is_null().cast(int_64) for s in expr_results) return [reduce(operator.add, series) / reduce(operator.add, non_na)] return self._expr._from_callable( @@ -139,13 +139,11 @@ def func(df: ArrowDataFrame) -> list[ArrowSeries]: def min_horizontal(self, *exprs: ArrowExpr) -> ArrowExpr: def func(df: ArrowDataFrame) -> list[ArrowSeries]: - init_series, *series = tuple(chain.from_iterable(expr(df) for expr in exprs)) - native_series = reduce( - pc.min_element_wise, [s.native for s in series], init_series.native + series = tuple(chain.from_iterable(expr(df) for expr in exprs)) + result = reduce( + lambda s1, s2: s1._with_binary(pc.min_element_wise, s2), series ) - return [ - ArrowSeries(native_series, name=init_series.name, version=self._version) - ] + return [result] return self._expr._from_callable( func=func, @@ -156,13 +154,11 @@ def func(df: ArrowDataFrame) -> list[ArrowSeries]: def max_horizontal(self, *exprs: ArrowExpr) -> ArrowExpr: def func(df: ArrowDataFrame) -> list[ArrowSeries]: - init_series, *series = tuple(chain.from_iterable(expr(df) for expr in exprs)) - native_series = reduce( - pc.max_element_wise, [s.native for s in series], init_series.native + series = tuple(chain.from_iterable(expr(df) for expr in exprs)) + result = reduce( + lambda s1, s2: s1._with_binary(pc.max_element_wise, s2), series ) - return [ - ArrowSeries(native_series, name=init_series.name, version=self._version) - ] + return [result] return self._expr._from_callable( func=func, diff --git a/narwhals/_dask/namespace.py b/narwhals/_dask/namespace.py index af3a9bc2f9..d49779403b 100644 --- a/narwhals/_dask/namespace.py +++ b/narwhals/_dask/namespace.py @@ -172,11 +172,11 @@ def concat( def mean_horizontal(self, *exprs: DaskExpr) -> DaskExpr: def func(df: DaskLazyFrame) -> list[dx.Series]: - expr_results = [s for _expr in exprs for s in _expr(df)] - series = align_series_full_broadcast(df, *(s.fillna(0) for s in expr_results)) - non_na = align_series_full_broadcast( - df, *(1 - s.isna() for s in expr_results) + expr_results = align_series_full_broadcast( + df, *[s for _expr in exprs for s in _expr(df)] ) + series = (s.fillna(0) for s in expr_results) + non_na = (1 - s.isna() for s in expr_results) num = reduce(lambda x, y: x + y, series) # pyright: ignore[reportOperatorIssue] den = reduce(lambda x, y: x + y, non_na) # pyright: ignore[reportOperatorIssue] return [cast("dx.Series", num / den)] # pyright: ignore[reportOperatorIssue] diff --git a/narwhals/_pandas_like/namespace.py b/narwhals/_pandas_like/namespace.py index 604d06bd25..94aca63464 100644 --- a/narwhals/_pandas_like/namespace.py +++ b/narwhals/_pandas_like/namespace.py @@ -235,7 +235,9 @@ def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]: def min_horizontal(self, *exprs: PandasLikeExpr) -> PandasLikeExpr: def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]: - series = list(chain.from_iterable(expr(df) for expr in exprs)) + series = self._series._align_full_broadcast( + *chain.from_iterable(expr(df) for expr in exprs) + ) return [ PandasLikeSeries( self.concat( @@ -255,7 +257,9 @@ def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]: def max_horizontal(self, *exprs: PandasLikeExpr) -> PandasLikeExpr: def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]: - series = list(chain.from_iterable(expr(df) for expr in exprs)) + series = self._series._align_full_broadcast( + *chain.from_iterable(expr(df) for expr in exprs) + ) return [ PandasLikeSeries( self.concat( diff --git a/narwhals/functions.py b/narwhals/functions.py index a19478eb21..1fb356b67f 100644 --- a/narwhals/functions.py +++ b/narwhals/functions.py @@ -1217,7 +1217,9 @@ def _expr_with_horizontal_op(name: str, *exprs: IntoExpr, **kwargs: Any) -> Expr ) -def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: +def sum_horizontal( + *exprs: PythonLiteral | IntoExpr | Iterable[PythonLiteral | IntoExpr], +) -> Expr: """Sum all values horizontally across columns. Warning: @@ -1251,7 +1253,9 @@ def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: return _expr_with_horizontal_op("sum_horizontal", *flatten(exprs)) -def min_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: +def min_horizontal( + *exprs: PythonLiteral | IntoExpr | Iterable[PythonLiteral | IntoExpr], +) -> Expr: """Get the minimum value horizontally across columns. Notes: @@ -1283,7 +1287,9 @@ def min_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: return _expr_with_horizontal_op("min_horizontal", *flatten(exprs)) -def max_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: +def max_horizontal( + *exprs: PythonLiteral | IntoExpr | Iterable[PythonLiteral | IntoExpr], +) -> Expr: """Get the maximum value horizontally across columns. Notes: @@ -1381,7 +1387,10 @@ def when(*predicates: IntoExpr | Iterable[IntoExpr]) -> When: return When(*predicates) -def all_horizontal(*exprs: IntoExpr | Iterable[IntoExpr], ignore_nulls: bool) -> Expr: +def all_horizontal( + *exprs: PythonLiteral | IntoExpr | Iterable[PythonLiteral | IntoExpr], + ignore_nulls: bool, +) -> Expr: r"""Compute the bitwise AND horizontally across columns. Arguments: @@ -1510,7 +1519,10 @@ def lit(value: PythonLiteral, dtype: IntoDType | None = None) -> Expr: return Expr(ExprNode(ExprKind.LITERAL, "lit", value=value, dtype=dtype)) -def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr], ignore_nulls: bool) -> Expr: +def any_horizontal( + *exprs: PythonLiteral | IntoExpr | Iterable[PythonLiteral | IntoExpr], + ignore_nulls: bool, +) -> Expr: r"""Compute the bitwise OR horizontally across columns. Arguments: @@ -1558,7 +1570,9 @@ def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr], ignore_nulls: bool) -> ) -def mean_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr: +def mean_horizontal( + *exprs: PythonLiteral | IntoExpr | Iterable[PythonLiteral | IntoExpr], +) -> Expr: """Compute the mean of all values horizontally across columns. Arguments: diff --git a/tests/expr_and_series/all_horizontal_test.py b/tests/expr_and_series/all_horizontal_test.py index d980b3def3..ef20187de7 100644 --- a/tests/expr_and_series/all_horizontal_test.py +++ b/tests/expr_and_series/all_horizontal_test.py @@ -1,13 +1,16 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data +if TYPE_CHECKING: + from narwhals.typing import PythonLiteral + def test_allh(constructor: Constructor) -> None: data = {"a": [False, False, True], "b": [False, True, True]} @@ -157,3 +160,21 @@ def test_horizontal_expressions_empty(constructor: Constructor) -> None: ValueError, match=r"At least one expression must be passed.*min_horizontal" ): df.select(nw.min_horizontal()) + + +@pytest.mark.parametrize( + ("exprs", "name"), + [ + ((nw.col("a"), True), "a"), + ((nw.col("a"), nw.lit(True)), "a"), + ((True, nw.col("a")), "literal"), + ((nw.lit(True), nw.col("a")), "literal"), + ], +) +def test_allh_with_scalars( + constructor: Constructor, exprs: tuple[PythonLiteral | nw.Expr, ...], name: str +) -> None: + data = {"a": [False, True]} + df = nw.from_native(constructor(data)) + result = df.select(nw.all_horizontal(*exprs, ignore_nulls=True)) + assert_equal_data(result, {name: [False, True]}) diff --git a/tests/expr_and_series/any_horizontal_test.py b/tests/expr_and_series/any_horizontal_test.py index 04f0cba76c..44da3b55a2 100644 --- a/tests/expr_and_series/any_horizontal_test.py +++ b/tests/expr_and_series/any_horizontal_test.py @@ -1,12 +1,16 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise +from typing import TYPE_CHECKING import pytest import narwhals as nw from tests.utils import Constructor, assert_equal_data +if TYPE_CHECKING: + from narwhals.typing import PythonLiteral + def test_anyh(constructor: Constructor) -> None: data = {"a": [False, False, True], "b": [False, True, True]} @@ -85,3 +89,21 @@ def test_anyh_all(constructor: Constructor) -> None: result = df.select(nw.any_horizontal(nw.all(), ignore_nulls=False)) expected = {"a": [False, True, True]} assert_equal_data(result, expected) + + +@pytest.mark.parametrize( + ("exprs", "name"), + [ + ((nw.col("a"), False), "a"), + ((nw.col("a"), nw.lit(False)), "a"), + ((False, nw.col("a")), "literal"), + ((nw.lit(False), nw.col("a")), "literal"), + ], +) +def test_anyh_with_scalars( + constructor: Constructor, exprs: tuple[PythonLiteral | nw.Expr, ...], name: str +) -> None: + data = {"a": [False, True]} + df = nw.from_native(constructor(data)) + result = df.select(nw.any_horizontal(*exprs, ignore_nulls=True)) + assert_equal_data(result, {name: [False, True]}) diff --git a/tests/expr_and_series/max_horizontal_test.py b/tests/expr_and_series/max_horizontal_test.py index cc0bddfb1a..8e8a6da9b3 100644 --- a/tests/expr_and_series/max_horizontal_test.py +++ b/tests/expr_and_series/max_horizontal_test.py @@ -1,10 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw from tests.utils import Constructor, assert_equal_data +if TYPE_CHECKING: + from narwhals.typing import PythonLiteral + data = {"a": [1, 3, None, None], "b": [4, None, 6, None], "z": [3, 1, None, None]} expected_values = [4, 3, 6, None] @@ -23,3 +28,20 @@ def test_maxh_all(constructor: Constructor) -> None: result = df.select(nw.max_horizontal(nw.all()), c=nw.max_horizontal(nw.all())) expected = {"a": expected_values, "c": expected_values} assert_equal_data(result, expected) + + +@pytest.mark.parametrize( + ("exprs", "name"), + [ + ((nw.col("a"), 2), "a"), + ((nw.col("a"), nw.lit(2)), "a"), + ((2, nw.col("a")), "literal"), + ((nw.lit(2), nw.col("a")), "literal"), + ], +) +def test_maxh_with_scalars( + constructor: Constructor, exprs: tuple[PythonLiteral | nw.Expr, ...], name: str +) -> None: + df = nw.from_native(constructor({"a": [1, 2, 3]})) + result = df.select(nw.max_horizontal(*exprs)) + assert_equal_data(result, {name: [2, 2, 3]}) diff --git a/tests/expr_and_series/mean_horizontal_test.py b/tests/expr_and_series/mean_horizontal_test.py index bc5bc12fa6..5888918651 100644 --- a/tests/expr_and_series/mean_horizontal_test.py +++ b/tests/expr_and_series/mean_horizontal_test.py @@ -1,10 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw from tests.utils import Constructor, assert_equal_data +if TYPE_CHECKING: + from narwhals.typing import PythonLiteral + def test_meanh(constructor: Constructor) -> None: data = {"a": [1, 3, None, None], "b": [4, None, 6, None]} @@ -14,11 +19,7 @@ def test_meanh(constructor: Constructor) -> None: assert_equal_data(result, expected) -def test_meanh_with_literal( - constructor: Constructor, request: pytest.FixtureRequest -) -> None: - if "dask" in str(constructor): - request.applymarker(pytest.mark.xfail) +def test_meanh_with_literal(constructor: Constructor) -> None: data = {"a": [1, 3, None, None], "b": [4, None, 6, None]} df = nw.from_native(constructor(data)) result = df.select(horizontal_mean=nw.mean_horizontal(nw.lit(1), "a", nw.col("b"))) @@ -35,3 +36,21 @@ def test_meanh_all(constructor: Constructor) -> None: result = df.select(c=nw.mean_horizontal(nw.all())) expected = {"c": [6, 12, 18]} assert_equal_data(result, expected) + + +@pytest.mark.parametrize( + ("exprs", "name"), + [ + ((nw.col("a"), 1), "a"), + ((nw.col("a"), nw.lit(1)), "a"), + ((1, nw.col("a")), "literal"), + ((nw.lit(1), nw.col("a")), "literal"), + ], +) +def test_meanh_with_scalars( + constructor: Constructor, exprs: tuple[PythonLiteral | nw.Expr, ...], name: str +) -> None: + data = {"a": [1, 2, 3]} + df = nw.from_native(constructor(data)) + result = df.select(nw.mean_horizontal(*exprs)) + assert_equal_data(result, {name: [1.0, 1.5, 2.0]}) diff --git a/tests/expr_and_series/min_horizontal_test.py b/tests/expr_and_series/min_horizontal_test.py index df9ff31feb..4342eeb075 100644 --- a/tests/expr_and_series/min_horizontal_test.py +++ b/tests/expr_and_series/min_horizontal_test.py @@ -1,10 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import narwhals as nw from tests.utils import Constructor, assert_equal_data +if TYPE_CHECKING: + from narwhals.typing import PythonLiteral + data = {"a": [1, 3, None, None], "b": [4, None, 6, None], "z": [3, 1, None, None]} expected_values = [1, 1, 6, None] @@ -23,3 +28,20 @@ def test_minh_all(constructor: Constructor) -> None: result = df.select(nw.min_horizontal(nw.all()), c=nw.min_horizontal(nw.all())) expected = {"a": expected_values, "c": expected_values} assert_equal_data(result, expected) + + +@pytest.mark.parametrize( + ("exprs", "name"), + [ + ((nw.col("a"), 2), "a"), + ((nw.col("a"), nw.lit(2)), "a"), + ((2, nw.col("a")), "literal"), + ((nw.lit(2), nw.col("a")), "literal"), + ], +) +def test_minh_with_scalars( + constructor: Constructor, exprs: tuple[PythonLiteral | nw.Expr, ...], name: str +) -> None: + df = nw.from_native(constructor({"a": [1, 2, 3]})) + result = df.select(nw.min_horizontal(*exprs)) + assert_equal_data(result, {name: [1, 2, 2]}) diff --git a/tests/expr_and_series/sum_horizontal_test.py b/tests/expr_and_series/sum_horizontal_test.py index 94cc32d4b0..16580e05a3 100644 --- a/tests/expr_and_series/sum_horizontal_test.py +++ b/tests/expr_and_series/sum_horizontal_test.py @@ -1,12 +1,15 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import pytest import narwhals as nw from tests.utils import DUCKDB_VERSION, Constructor, assert_equal_data +if TYPE_CHECKING: + from narwhals.typing import PythonLiteral + def test_sumh(constructor: Constructor) -> None: data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8.0, 9.0]} @@ -60,3 +63,21 @@ def test_sumh_transformations(constructor: Constructor) -> None: result = df.select(d=nw.sum_horizontal("a", nw.lit(None, dtype=nw.Float64), "c")) expected = {"d": [8.0, 10.0, 12.0]} assert_equal_data(result, expected) + + +@pytest.mark.parametrize( + ("exprs", "name"), + [ + ((nw.col("a"), 1), "a"), + ((nw.col("a"), nw.lit(1)), "a"), + ((1, nw.col("a")), "literal"), + ((nw.lit(1), nw.col("a")), "literal"), + ], +) +def test_sumh_with_scalars( + constructor: Constructor, exprs: tuple[PythonLiteral | nw.Expr, ...], name: str +) -> None: + data = {"a": [1, 2, 3]} + df = nw.from_native(constructor(data)) + result = df.select(nw.sum_horizontal(*exprs)) + assert_equal_data(result, {name: [2, 3, 4]}) From 5332f338671c5201fb9e71ff265ff2996a595c07 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 1 Feb 2026 11:20:29 +0100 Subject: [PATCH 2/6] old polars name hacking, simplify pyarrow, avoid lambda's in dask --- narwhals/_arrow/namespace.py | 4 ++-- narwhals/_dask/namespace.py | 4 ++-- tests/expr_and_series/all_horizontal_test.py | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py index f0ad2b7386..106ac841af 100644 --- a/narwhals/_arrow/namespace.py +++ b/narwhals/_arrow/namespace.py @@ -139,7 +139,7 @@ def func(df: ArrowDataFrame) -> list[ArrowSeries]: def min_horizontal(self, *exprs: ArrowExpr) -> ArrowExpr: def func(df: ArrowDataFrame) -> list[ArrowSeries]: - series = tuple(chain.from_iterable(expr(df) for expr in exprs)) + series = chain.from_iterable(expr(df) for expr in exprs) result = reduce( lambda s1, s2: s1._with_binary(pc.min_element_wise, s2), series ) @@ -154,7 +154,7 @@ def func(df: ArrowDataFrame) -> list[ArrowSeries]: def max_horizontal(self, *exprs: ArrowExpr) -> ArrowExpr: def func(df: ArrowDataFrame) -> list[ArrowSeries]: - series = tuple(chain.from_iterable(expr(df) for expr in exprs)) + series = chain.from_iterable(expr(df) for expr in exprs) result = reduce( lambda s1, s2: s1._with_binary(pc.max_element_wise, s2), series ) diff --git a/narwhals/_dask/namespace.py b/narwhals/_dask/namespace.py index d49779403b..83d2ddcf1c 100644 --- a/narwhals/_dask/namespace.py +++ b/narwhals/_dask/namespace.py @@ -177,8 +177,8 @@ def func(df: DaskLazyFrame) -> list[dx.Series]: ) series = (s.fillna(0) for s in expr_results) non_na = (1 - s.isna() for s in expr_results) - num = reduce(lambda x, y: x + y, series) # pyright: ignore[reportOperatorIssue] - den = reduce(lambda x, y: x + y, non_na) # pyright: ignore[reportOperatorIssue] + num = reduce(operator.add, series) # pyright: ignore[reportOperatorIssue] + den = reduce(operator.add, non_na) # pyright: ignore[reportOperatorIssue] return [cast("dx.Series", num / den)] # pyright: ignore[reportOperatorIssue] return self._expr( diff --git a/tests/expr_and_series/all_horizontal_test.py b/tests/expr_and_series/all_horizontal_test.py index ef20187de7..b1068e7e3a 100644 --- a/tests/expr_and_series/all_horizontal_test.py +++ b/tests/expr_and_series/all_horizontal_test.py @@ -174,6 +174,9 @@ def test_horizontal_expressions_empty(constructor: Constructor) -> None: def test_allh_with_scalars( constructor: Constructor, exprs: tuple[PythonLiteral | nw.Expr, ...], name: str ) -> None: + if "polars" in str(constructor) and POLARS_VERSION < (1, 0, 0): + name = "a" + data = {"a": [False, True]} df = nw.from_native(constructor(data)) result = df.select(nw.all_horizontal(*exprs, ignore_nulls=True)) From af06b23a32b06997a9ee80cff9267a914c64dcd5 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 1 Feb 2026 14:57:22 +0100 Subject: [PATCH 3/6] DRY via _min_max_horizontal - suggested by @dangotbanned --- narwhals/_arrow/namespace.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py index 106ac841af..c9cbc2e081 100644 --- a/narwhals/_arrow/namespace.py +++ b/narwhals/_arrow/namespace.py @@ -137,13 +137,14 @@ def func(df: ArrowDataFrame) -> list[ArrowSeries]: context=self, ) - def min_horizontal(self, *exprs: ArrowExpr) -> ArrowExpr: + def _min_max_horizontal( + self, exprs: Sequence[ArrowExpr], /, op: Literal["min", "max"] + ) -> ArrowExpr: + agg = pc.min_element_wise if op == "min" else pc.max_element_wise + def func(df: ArrowDataFrame) -> list[ArrowSeries]: series = chain.from_iterable(expr(df) for expr in exprs) - result = reduce( - lambda s1, s2: s1._with_binary(pc.min_element_wise, s2), series - ) - return [result] + return [reduce(lambda s1, s2: s1._with_binary(agg, s2), series)] return self._expr._from_callable( func=func, @@ -152,20 +153,11 @@ def func(df: ArrowDataFrame) -> list[ArrowSeries]: context=self, ) - def max_horizontal(self, *exprs: ArrowExpr) -> ArrowExpr: - def func(df: ArrowDataFrame) -> list[ArrowSeries]: - series = chain.from_iterable(expr(df) for expr in exprs) - result = reduce( - lambda s1, s2: s1._with_binary(pc.max_element_wise, s2), series - ) - return [result] + def min_horizontal(self, *exprs: ArrowExpr) -> ArrowExpr: + return self._min_max_horizontal(exprs, "min") - return self._expr._from_callable( - func=func, - evaluate_output_names=combine_evaluate_output_names(*exprs), - alias_output_names=combine_alias_output_names(*exprs), - context=self, - ) + def max_horizontal(self, *exprs: ArrowExpr) -> ArrowExpr: + return self._min_max_horizontal(exprs, "max") def _concat_diagonal(self, dfs: Sequence[pa.Table], /) -> pa.Table: if self._backend_version >= (14,): From 0cad6c10072e96f34e720c94a345ec82202c00c4 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 1 Feb 2026 17:45:25 +0100 Subject: [PATCH 4/6] perf: avoid reduce --- narwhals/_arrow/namespace.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py index c9cbc2e081..253636d368 100644 --- a/narwhals/_arrow/namespace.py +++ b/narwhals/_arrow/namespace.py @@ -143,8 +143,13 @@ def _min_max_horizontal( agg = pc.min_element_wise if op == "min" else pc.max_element_wise def func(df: ArrowDataFrame) -> list[ArrowSeries]: - series = chain.from_iterable(expr(df) for expr in exprs) - return [reduce(lambda s1, s2: s1._with_binary(agg, s2), series)] + series = tuple(chain.from_iterable(expr(df) for expr in exprs)) + native_series = agg( + *(s.native[0] if s._broadcast else s.native for s in series) + ) + return [ + ArrowSeries(native_series, name=series[0].name, version=self._version) + ] return self._expr._from_callable( func=func, From ca99472b9f6546748530afe5137f60c3a1365613 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 1 Feb 2026 17:55:34 +0100 Subject: [PATCH 5/6] bisect polars version for wrong naming --- tests/expr_and_series/all_horizontal_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/expr_and_series/all_horizontal_test.py b/tests/expr_and_series/all_horizontal_test.py index b1068e7e3a..1d70917739 100644 --- a/tests/expr_and_series/all_horizontal_test.py +++ b/tests/expr_and_series/all_horizontal_test.py @@ -174,7 +174,7 @@ def test_horizontal_expressions_empty(constructor: Constructor) -> None: def test_allh_with_scalars( constructor: Constructor, exprs: tuple[PythonLiteral | nw.Expr, ...], name: str ) -> None: - if "polars" in str(constructor) and POLARS_VERSION < (1, 0, 0): + if "polars" in str(constructor) and POLARS_VERSION < (0, 20, 19): name = "a" data = {"a": [False, True]} From c91ece57b77dfdfb36b482dbf8ae05e95bc2a888 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 1 Feb 2026 17:57:51 +0100 Subject: [PATCH 6/6] compacting --- narwhals/_arrow/namespace.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py index 253636d368..1775521946 100644 --- a/narwhals/_arrow/namespace.py +++ b/narwhals/_arrow/namespace.py @@ -144,12 +144,8 @@ def _min_max_horizontal( def func(df: ArrowDataFrame) -> list[ArrowSeries]: series = tuple(chain.from_iterable(expr(df) for expr in exprs)) - native_series = agg( - *(s.native[0] if s._broadcast else s.native for s in series) - ) - return [ - ArrowSeries(native_series, name=series[0].name, version=self._version) - ] + result = agg(*(s.native[0] if s._broadcast else s.native for s in series)) + return [ArrowSeries(result, name=series[0].name, version=self._version)] return self._expr._from_callable( func=func,