Repository: narwhals-dev/narwhals (python)

Issue:
enh: support `.diff().over('group')` for pandas-like
This should work out of the box

Pull request:
feat: Add support for pandas-like `.diff().over(group)`
## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [x] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues

- Closes #2095

## Checklist

- [x] Code follows style guide (ruff)
- [x] 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 73971a13..3332ffd9 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -44,6 +44,7 @@ MANY_TO_MANY_AGG_FUNCTIONS_TO_PANDAS_EQUIVALENT = {
     "cum_count": "cumsum",
     "shift": "shift",
     "rank": "rank",
+    "diff": "diff",
 }
 
 
@@ -456,10 +457,12 @@ class PandasLikeExpr(CompliantExpr["PandasLikeDataFrame", PandasLikeSeries]):
                         "na_option": "keep",
                         "pct": False,
                     }
-                else:  # Cumulative operation
+                elif function_name.startswith("cum_"):  # Cumulative operation
                     if self._call_kwargs["reverse"]:
                         raise NotImplementedError(unsupported_reverse_msg)
                     kwargs = {"skipna": True}
+                else:
+                    kwargs = {}
 
                 res_native = getattr(
                     df._native_frame.groupby(keys)[list(output_names)],


Test patch:
diff --git a/tests/expr_and_series/over_test.py b/tests/expr_and_series/over_test.py
index ddd497f3..ebbda168 100644
--- a/tests/expr_and_series/over_test.py
+++ b/tests/expr_and_series/over_test.py
@@ -123,7 +123,7 @@ def test_over_cumcount(
 def test_over_cummax(
     request: pytest.FixtureRequest, constructor_eager: ConstructorEager
 ) -> None:
-    if any(x in str(constructor_eager) for x in ("pyarrow_table",)):
+    if "pyarrow_table" in str(constructor_eager):
         request.applymarker(pytest.mark.xfail)
     if "pandas_pyarrow" in str(constructor_eager) and PANDAS_VERSION < (2, 1):
         request.applymarker(pytest.mark.xfail)
@@ -171,7 +171,7 @@ def test_over_cummin(
 def test_over_cumprod(
     request: pytest.FixtureRequest, constructor_eager: ConstructorEager
 ) -> None:
-    if any(x in str(constructor_eager) for x in ("pyarrow_table",)):
+    if "pyarrow_table" in str(constructor_eager):
         request.applymarker(pytest.mark.xfail)
     if "pandas_pyarrow" in str(constructor_eager) and PANDAS_VERSION < (2, 1):
         request.applymarker(pytest.mark.xfail)
@@ -251,9 +251,7 @@ def test_over_anonymous_reduction(
 def test_over_shift(
     request: pytest.FixtureRequest, constructor_eager: ConstructorEager
 ) -> None:
-    if "pyarrow_table_constructor" in str(constructor_eager):
-        request.applymarker(pytest.mark.xfail)
-    if ("pyspark" in str(constructor_eager)) or "duckdb" in str(constructor_eager):
+    if "pyarrow_table" in str(constructor_eager):
         request.applymarker(pytest.mark.xfail)
 
     df = nw.from_native(constructor_eager(data))
@@ -267,6 +265,23 @@ def test_over_shift(
     assert_equal_data(result, expected)
 
 
+def test_over_diff(
+    request: pytest.FixtureRequest, constructor_eager: ConstructorEager
+) -> None:
+    if "pyarrow_table" in str(constructor_eager):
+        request.applymarker(pytest.mark.xfail)
+
+    df = nw.from_native(constructor_eager(data))
+    expected = {
+        "a": ["a", "a", "b", "b", "b"],
+        "b": [1, 2, 3, 5, 3],
+        "c": [5, 4, 3, 2, 1],
+        "b_diff": [None, 1, None, 2, -2],
+    }
+    result = df.with_columns(b_diff=nw.col("b").diff().over("a")).sort("i").drop("i")
+    assert_equal_data(result, expected)
+
+
 def test_over_cum_reverse() -> None:
     df = pd.DataFrame({"a": [1, 1, 2], "b": [4, 5, 6]})