Repository: narwhals-dev/narwhals
Pull request #2143 (2025-03-04 13:23:56)

fix: pass ddof down in std().over() for pandas-like
missed this in #https://github.com/narwhals-dev/narwhals/pull/2138

<!--
# Thanks for contributing a pull request! 
## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues

- Related issue #\<issue number\>
- Closes #\<issue number\>

## Checklist

- [ ] Code follows style guide (ruff)
- [ ] Tests added
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below



Patch:
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index bb7c97d0..59c91390 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -63,8 +63,8 @@ def window_kwargs_to_pandas_equivalent(
         }
     elif function_name.startswith("cum_"):  # Cumulative operation
         pandas_kwargs = {"skipna": True}
-    else:
-        pandas_kwargs = {}
+    else:  # e.g. std, var
+        pandas_kwargs = kwargs
     return pandas_kwargs
 
 
diff --git a/narwhals/_spark_like/expr.py b/narwhals/_spark_like/expr.py
index f2c09ea8..14dcb824 100644
--- a/narwhals/_spark_like/expr.py
+++ b/narwhals/_spark_like/expr.py
@@ -384,7 +384,13 @@ class SparkLikeExpr(CompliantExpr["SparkLikeLazyFrame", "Column"]):  # type: ign
 
         from narwhals._spark_like.utils import _std
 
-        func = partial(_std, ddof=ddof, np_version=parse_version(np), functions=self._F)
+        func = partial(
+            _std,
+            ddof=ddof,
+            np_version=parse_version(np),
+            functions=self._F,
+            implementation=self._implementation,
+        )
 
         return self._from_call(func, "std")
 
@@ -395,7 +401,13 @@ class SparkLikeExpr(CompliantExpr["SparkLikeLazyFrame", "Column"]):  # type: ign
 
         from narwhals._spark_like.utils import _var
 
-        func = partial(_var, ddof=ddof, np_version=parse_version(np), functions=self._F)
+        func = partial(
+            _var,
+            ddof=ddof,
+            np_version=parse_version(np),
+            functions=self._F,
+            implementation=self._implementation,
+        )
 
         return self._from_call(func, "var")
 
diff --git a/narwhals/_spark_like/utils.py b/narwhals/_spark_like/utils.py
index 29558c3b..6332f35d 100644
--- a/narwhals/_spark_like/utils.py
+++ b/narwhals/_spark_like/utils.py
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
 from typing import Any
 
 from narwhals.exceptions import UnsupportedDTypeError
+from narwhals.utils import Implementation
 from narwhals.utils import import_dtypes_module
 from narwhals.utils import isinstance_or_issubclass
 
@@ -181,13 +182,17 @@ def maybe_evaluate_expr(df: SparkLikeLazyFrame, obj: SparkLikeExpr | object) ->
 
 
 def _std(
-    _input: Column | str, ddof: int, np_version: tuple[int, ...], functions: Any
+    _input: Column | str,
+    ddof: int,
+    np_version: tuple[int, ...],
+    functions: Any,
+    implementation: Implementation,
 ) -> Column:
-    if np_version > (2, 0):
-        if ddof == 1:
-            return functions.stddev_samp(_input)
+    if implementation is Implementation.SQLFRAME or np_version > (2, 0):
         if ddof == 0:
             return functions.stddev_pop(_input)
+        if ddof == 1:
+            return functions.stddev_samp(_input)
 
         n_rows = functions.count(_input)
         return functions.stddev_samp(_input) * functions.sqrt(
@@ -201,9 +206,15 @@ def _std(
 
 
 def _var(
-    _input: Column | str, ddof: int, np_version: tuple[int, ...], functions: Any
+    _input: Column | str,
+    ddof: int,
+    np_version: tuple[int, ...],
+    functions: Any,
+    implementation: Implementation,
 ) -> Column:
-    if np_version > (2, 0):
+    if implementation is Implementation.SQLFRAME or np_version > (2, 0):
+        if ddof == 0:
+            return functions.var_pop(_input)
         if ddof == 1:
             return functions.var_samp(_input)
 


Test patch:
diff --git a/tests/expr_and_series/over_test.py b/tests/expr_and_series/over_test.py
index 0c43f957..38ff7841 100644
--- a/tests/expr_and_series/over_test.py
+++ b/tests/expr_and_series/over_test.py
@@ -47,6 +47,40 @@ def test_over_single(request: pytest.FixtureRequest, constructor: Constructor) -
     assert_equal_data(result, expected)
 
 
+def test_over_std_var(request: pytest.FixtureRequest, constructor: Constructor) -> None:
+    if "duckdb" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+
+    df = nw.from_native(constructor(data))
+    expected = {
+        "a": ["a", "a", "b", "b", "b"],
+        "b": [1, 2, 3, 5, 3],
+        "c": [5, 4, 3, 2, 1],
+        "c_std0": [0.5, 0.5, 0.816496580927726, 0.816496580927726, 0.816496580927726],
+        "c_std1": [0.7071067811865476, 0.7071067811865476, 1.0, 1.0, 1.0],
+        "c_var0": [
+            0.25,
+            0.25,
+            0.6666666666666666,
+            0.6666666666666666,
+            0.6666666666666666,
+        ],
+        "c_var1": [0.5, 0.5, 1.0, 1.0, 1.0],
+    }
+
+    result = (
+        df.with_columns(
+            c_std0=nw.col("c").std(ddof=0).over("a"),
+            c_std1=nw.col("c").std(ddof=1).over("a"),
+            c_var0=nw.col("c").var(ddof=0).over("a"),
+            c_var1=nw.col("c").var(ddof=1).over("a"),
+        )
+        .sort("i")
+        .drop("i")
+    )
+    assert_equal_data(result, expected)
+
+
 def test_over_multiple(request: pytest.FixtureRequest, constructor: Constructor) -> None:
     if "duckdb" in str(constructor):
         request.applymarker(pytest.mark.xfail)