diff --git a/src/narwhals/_arrow/namespace.py b/src/narwhals/_arrow/namespace.py index 4641e91354..ec6260aae6 100644 --- a/src/narwhals/_arrow/namespace.py +++ b/src/narwhals/_arrow/namespace.py @@ -164,7 +164,11 @@ 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] - return [reduce(operator.add, series) / reduce(operator.add, non_na)] + count = reduce(operator.add, non_na) + mean = reduce(operator.add, series) / count + # A row where every input is null has a count of 0: return null, not 0/0 = NaN. + native = pc.if_else((count == 0).native, None, mean.native) + return [mean._with_native(native, preserve_broadcast=True)] return self._expr._from_callable( func=func, diff --git a/src/narwhals/_duckdb/namespace.py b/src/narwhals/_duckdb/namespace.py index 3541c6ef6b..2614d5fb11 100644 --- a/src/narwhals/_duckdb/namespace.py +++ b/src/narwhals/_duckdb/namespace.py @@ -153,7 +153,7 @@ def func(cols: Iterable[Expression]) -> Expression: count = reduce( operator.add, (col.isnotnull().cast(duckdb_dtypes.BIGINT) for col in cols) ) - return total / count + return when(count > lit(0), total / count) return self._expr._from_elementwise_horizontal_op(func, *exprs) diff --git a/src/narwhals/_ibis/namespace.py b/src/narwhals/_ibis/namespace.py index 595dc275f0..2970e0f1ed 100644 --- a/src/narwhals/_ibis/namespace.py +++ b/src/narwhals/_ibis/namespace.py @@ -136,7 +136,7 @@ def func(cols: Iterable[ir.Value]) -> ir.Value: for col in cols ), ) - return numerator / denominator + return numerator / denominator.nullif(lit(0)) return self._expr._from_elementwise_horizontal_op(func, *exprs) diff --git a/src/narwhals/_spark_like/namespace.py b/src/narwhals/_spark_like/namespace.py index 50d9d755bd..79cc442910 100644 --- a/src/narwhals/_spark_like/namespace.py +++ b/src/narwhals/_spark_like/namespace.py @@ -215,7 +215,7 @@ def func(cols: Iterable[Column]) -> Column: operator.add, (col.isNotNull().cast(self._native_dtypes.IntegerType()) for col in cols), ) - return true_divide(F, numerator, denominator) + return F.when(denominator > F.lit(0), true_divide(F, numerator, denominator)) return self._expr._from_elementwise_horizontal_op(func, *exprs) diff --git a/tests/expr_and_series/mean_horizontal_test.py b/tests/expr_and_series/mean_horizontal_test.py index a3f03ade2b..8e232517e1 100644 --- a/tests/expr_and_series/mean_horizontal_test.py +++ b/tests/expr_and_series/mean_horizontal_test.py @@ -29,3 +29,13 @@ 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) + + +def test_meanh_all_null_row_is_null(constructor: Constructor) -> None: + data = {"a": [1.0, None], "b": [3.0, None]} + df = nw.from_native(constructor(data)) + result = df.select(mean=nw.mean_horizontal("a", "b")).select( + nw.col("mean"), is_null=nw.col("mean").is_null() + ) + expected = {"mean": [2.0, None], "is_null": [False, True]} + assert_equal_data(result, expected)