Repository: narwhals-dev/narwhals
Pull request #1386 (2024-11-16 10:50:22)

feat: add `Series|Expr.cum_prod` method, add `reverse` kw in `cum_sum` method
## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [x] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues

- Related issue #1371 

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added
- [x] Documented the changes

## If you have comments or can explain your changes, please do so below

This is 3 folded (I could not resist)

- Introduce `cum_prod`
- Adds `reverse` in `cum_sum`
- Adds cumulative methods for dask if `reverse=False`, raise otherwise


Patch:
diff --git a/docs/api-reference/expr.md b/docs/api-reference/expr.md
index 4bc18070..867702b6 100644
--- a/docs/api-reference/expr.md
+++ b/docs/api-reference/expr.md
@@ -14,6 +14,7 @@
         - cum_count
         - cum_max
         - cum_min
+        - cum_prod
         - cum_sum
         - diff
         - drop_nulls
diff --git a/docs/api-reference/series.md b/docs/api-reference/series.md
index 25e14de4..b957bdbf 100644
--- a/docs/api-reference/series.md
+++ b/docs/api-reference/series.md
@@ -18,6 +18,7 @@
         - cum_count
         - cum_max
         - cum_min
+        - cum_prod
         - cum_sum
         - diff
         - drop_nulls
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index 4fc998b0..39e39c13 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -228,8 +228,8 @@ class ArrowExpr:
     def diff(self) -> Self:
         return reuse_series_implementation(self, "diff")
 
-    def cum_sum(self) -> Self:
-        return reuse_series_implementation(self, "cum_sum")
+    def cum_sum(self: Self, *, reverse: bool) -> Self:
+        return reuse_series_implementation(self, "cum_sum", reverse=reverse)
 
     def round(self, decimals: int) -> Self:
         return reuse_series_implementation(self, "round", decimals)
@@ -439,6 +439,9 @@ class ArrowExpr:
     def cum_max(self: Self, *, reverse: bool) -> Self:
         return reuse_series_implementation(self, "cum_max", reverse=reverse)
 
+    def cum_prod(self: Self, *, reverse: bool) -> Self:
+        return reuse_series_implementation(self, "cum_prod", reverse=reverse)
+
     @property
     def dt(self: Self) -> ArrowExprDateTimeNamespace:
         return ArrowExprDateTimeNamespace(self)
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 1ddf9d3d..19c424a7 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -391,12 +391,16 @@ class ArrowSeries:
 
         return self._from_native_series(pc.abs(self._native_series))
 
-    def cum_sum(self) -> Self:
+    def cum_sum(self: Self, *, reverse: bool) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
 
-        return self._from_native_series(
-            pc.cumulative_sum(self._native_series, skip_nulls=True)
+        native_series = self._native_series
+        result = (
+            pc.cumulative_sum(native_series, skip_nulls=True)
+            if not reverse
+            else pc.cumulative_sum(native_series[::-1], skip_nulls=True)[::-1]
         )
+        return self._from_native_series(result)
 
     def round(self, decimals: int) -> Self:
         import pyarrow.compute as pc  # ignore-banned-import()
@@ -815,12 +819,7 @@ class ArrowSeries:
         )[self.name]
 
     def cum_count(self: Self, *, reverse: bool) -> Self:
-        not_na_series = (~self.is_null()).cast(self._dtypes.UInt32())
-        return (
-            not_na_series.cum_sum()
-            if not reverse
-            else len(self) - not_na_series.cum_sum() + not_na_series - 1
-        )
+        return (~self.is_null()).cast(self._dtypes.UInt32()).cum_sum(reverse=reverse)
 
     def cum_min(self: Self, *, reverse: bool) -> Self:
         if self._backend_version < (13, 0, 0):
@@ -854,6 +853,22 @@ class ArrowSeries:
         )
         return self._from_native_series(result)
 
+    def cum_prod(self: Self, *, reverse: bool) -> Self:
+        if self._backend_version < (13, 0, 0):
+            msg = "cum_max method is not supported for pyarrow < 13.0.0"
+            raise NotImplementedError(msg)
+
+        import pyarrow.compute as pc  # ignore-banned-import
+
+        native_series = self._native_series
+
+        result = (
+            pc.cumulative_prod(native_series, skip_nulls=True)
+            if not reverse
+            else pc.cumulative_prod(native_series[::-1], skip_nulls=True)[::-1]
+        )
+        return self._from_native_series(result)
+
     def __iter__(self: Self) -> Iterator[Any]:
         yield from self._native_series.__iter__()
 
diff --git a/narwhals/_dask/expr.py b/narwhals/_dask/expr.py
index 8de079df..c2ff8592 100644
--- a/narwhals/_dask/expr.py
+++ b/narwhals/_dask/expr.py
@@ -434,13 +434,61 @@ class DaskExpr:
             returns_scalar=False,
         )
 
-    def cum_sum(self) -> Self:
+    def cum_sum(self: Self, *, reverse: bool) -> Self:
+        if reverse:
+            msg = "`cum_sum(reverse=True)` is not supported with Dask backend"
+            raise NotImplementedError(msg)
+
         return self._from_call(
             lambda _input: _input.cumsum(),
             "cum_sum",
             returns_scalar=False,
         )
 
+    def cum_count(self: Self, *, reverse: bool) -> Self:
+        if reverse:
+            msg = "`cum_count(reverse=True)` is not supported with Dask backend"
+            raise NotImplementedError(msg)
+
+        return self._from_call(
+            lambda _input: (~_input.isna()).astype(int).cumsum(),
+            "cum_count",
+            returns_scalar=False,
+        )
+
+    def cum_min(self: Self, *, reverse: bool) -> Self:
+        if reverse:
+            msg = "`cum_min(reverse=True)` is not supported with Dask backend"
+            raise NotImplementedError(msg)
+
+        return self._from_call(
+            lambda _input: _input.cummin(),
+            "cum_min",
+            returns_scalar=False,
+        )
+
+    def cum_max(self: Self, *, reverse: bool) -> Self:
+        if reverse:
+            msg = "`cum_max(reverse=True)` is not supported with Dask backend"
+            raise NotImplementedError(msg)
+
+        return self._from_call(
+            lambda _input: _input.cummax(),
+            "cum_max",
+            returns_scalar=False,
+        )
+
+    def cum_prod(self: Self, *, reverse: bool) -> Self:
+        if reverse:
+            msg = "`cum_prod(reverse=True)` is not supported with Dask backend"
+            raise NotImplementedError(msg)
+
+        return self._from_call(
+            lambda _input: _input.cumprod(),
+            "cum_prod",
+            returns_scalar=False,
+        )
+
     def is_between(
         self,
         lower_bound: Any,
@@ -749,10 +797,6 @@ class DaskExpr:
         msg = "`Expr.mode` is not supported for the Dask backend."
         raise NotImplementedError(msg)
 
-    def cum_count(self: Self, *, reverse: bool) -> Self:
-        msg = "`Expr.cum_count` is not supported for the Dask backend."
-        raise NotImplementedError(msg)
-
     @property
     def str(self: Self) -> DaskExprStringNamespace:
         return DaskExprStringNamespace(self)
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index cdab7d96..98f5352d 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -300,8 +300,8 @@ class PandasLikeExpr:
     def abs(self) -> Self:
         return reuse_series_implementation(self, "abs")
 
-    def cum_sum(self) -> Self:
-        return reuse_series_implementation(self, "cum_sum")
+    def cum_sum(self: Self, *, reverse: bool) -> Self:
+        return reuse_series_implementation(self, "cum_sum", reverse=reverse)
 
     def unique(self, *, maintain_order: bool = False) -> Self:
         return reuse_series_implementation(self, "unique", maintain_order=maintain_order)
@@ -450,6 +450,9 @@ class PandasLikeExpr:
     def cum_max(self: Self, *, reverse: bool) -> Self:
         return reuse_series_implementation(self, "cum_max", reverse=reverse)
 
+    def cum_prod(self: Self, *, reverse: bool) -> Self:
+        return reuse_series_implementation(self, "cum_prod", reverse=reverse)
+
     @property
     def str(self: Self) -> PandasLikeExprStringNamespace:
         return PandasLikeExprStringNamespace(self)
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 5b0e3aee..91fd4361 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -494,8 +494,14 @@ class PandasLikeSeries:
     def abs(self) -> PandasLikeSeries:
         return self._from_native_series(self._native_series.abs())
 
-    def cum_sum(self) -> PandasLikeSeries:
-        return self._from_native_series(self._native_series.cumsum())
+    def cum_sum(self: Self, *, reverse: bool) -> Self:
+        native_series = self._native_series
+        result = (
+            native_series.cumsum(skipna=True)
+            if not reverse
+            else native_series[::-1].cumsum(skipna=True)[::-1]
+        )
+        return self._from_native_series(result)
 
     def unique(self, *, maintain_order: bool = False) -> PandasLikeSeries:
         # The param `maintain_order` is only here for compatibility with the Polars API
@@ -783,6 +789,15 @@ class PandasLikeSeries:
         )
         return self._from_native_series(result)
 
+    def cum_prod(self: Self, *, reverse: bool) -> Self:
+        native_series = self._native_series
+        result = (
+            native_series.cumprod(skipna=True)
+            if not reverse
+            else native_series[::-1].cumprod(skipna=True)[::-1]
+        )
+        return self._from_native_series(result)
+
     def __iter__(self: Self) -> Iterator[Any]:
         yield from self._native_series.__iter__()
 
diff --git a/narwhals/expr.py b/narwhals/expr.py
index 4db54341..fce4dc5c 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -956,9 +956,12 @@ class Expr:
         """
         return self.__class__(lambda plx: self._call(plx).abs())
 
-    def cum_sum(self) -> Self:
+    def cum_sum(self: Self, *, reverse: bool = False) -> Self:
         """Return cumulative sum.
 
+        Arguments:
+            reverse: reverse the operation
+
         Returns:
             A new expression.
 
@@ -1007,7 +1010,7 @@ class Expr:
             a: [[1,2,5,10,15]]
             b: [[2,6,10,16,22]]
         """
-        return self.__class__(lambda plx: self._call(plx).cum_sum())
+        return self.__class__(lambda plx: self._call(plx).cum_sum(reverse=reverse))
 
     def diff(self) -> Self:
         """Returns the difference between each element and the previous one.
@@ -2697,6 +2700,9 @@ class Expr:
         Arguments:
             reverse: reverse the operation
 
+        Returns:
+            A new expression.
+
         Examples:
             >>> import narwhals as nw
             >>> import pandas as pd
@@ -2739,7 +2745,7 @@ class Expr:
             pyarrow.Table
             a: string
             cum_count: uint32
-            cum_count_reverse: int64
+            cum_count_reverse: uint32
             ----
             a: [["x","k",null,"d"]]
             cum_count: [[1,2,2,3]]
@@ -2753,6 +2759,9 @@ class Expr:
         Arguments:
             reverse: reverse the operation
 
+        Returns:
+            A new expression.
+
         Examples:
             >>> import narwhals as nw
             >>> import pandas as pd
@@ -2809,6 +2818,9 @@ class Expr:
         Arguments:
             reverse: reverse the operation
 
+        Returns:
+            A new expression.
+
         Examples:
             >>> import narwhals as nw
             >>> import pandas as pd
@@ -2859,6 +2871,65 @@ class Expr:
         """
         return self.__class__(lambda plx: self._call(plx).cum_max(reverse=reverse))
 
+    def cum_prod(self: Self, *, reverse: bool = False) -> Self:
+        r"""Return the cumulative product of the non-null values in the column.
+
+        Arguments:
+            reverse: reverse the operation
+
+        Returns:
+            A new expression.
+
+        Examples:
+            >>> import narwhals as nw
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> import pyarrow as pa
+            >>> data = {"a": [1, 3, None, 2]}
+
+            We define a library agnostic function:
+
+            >>> @nw.narwhalify
+            ... def func(df):
+            ...     return df.with_columns(
+            ...         nw.col("a").cum_prod().alias("cum_prod"),
+            ...         nw.col("a").cum_prod(reverse=True).alias("cum_prod_reverse"),
+            ...     )
+
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
+
+            >>> func(pd.DataFrame(data))
+                 a  cum_prod  cum_prod_reverse
+            0  1.0       1.0               6.0
+            1  3.0       3.0               6.0
+            2  NaN       NaN               NaN
+            3  2.0       6.0               2.0
+
+            >>> func(pl.DataFrame(data))
+            shape: (4, 3)
+            ┌──────┬──────────┬──────────────────┐
+            │ a    ┆ cum_prod ┆ cum_prod_reverse │
+            │ ---  ┆ ---      ┆ ---              │
+            │ i64  ┆ i64      ┆ i64              │
+            ╞══════╪══════════╪══════════════════╡
+            │ 1    ┆ 1        ┆ 6                │
+            │ 3    ┆ 3        ┆ 6                │
+            │ null ┆ null     ┆ null             │
+            │ 2    ┆ 6        ┆ 2                │
+            └──────┴──────────┴──────────────────┘
+
+            >>> func(pa.table(data))
+            pyarrow.Table
+            a: int64
+            cum_prod: int64
+            cum_prod_reverse: int64
+            ----
+            a: [[1,3,null,2]]
+            cum_prod: [[1,3,null,6]]
+            cum_prod_reverse: [[6,6,null,2]]
+        """
+        return self.__class__(lambda plx: self._call(plx).cum_prod(reverse=reverse))
+
     @property
     def str(self: Self) -> ExprStringNamespace[Self]:
         return ExprStringNamespace(self)
diff --git a/narwhals/series.py b/narwhals/series.py
index b1142a2a..5819fb3f 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -984,9 +984,12 @@ class Series:
         """
         return self._from_compliant_series(self._compliant_series.abs())
 
-    def cum_sum(self) -> Self:
+    def cum_sum(self: Self, *, reverse: bool = False) -> Self:
         """Calculate the cumulative sum.
 
+        Arguments:
+            reverse: reverse the operation
+
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
@@ -1017,7 +1020,9 @@ class Series:
                9
             ]
         """
-        return self._from_compliant_series(self._compliant_series.cum_sum())
+        return self._from_compliant_series(
+            self._compliant_series.cum_sum(reverse=reverse)
+        )
 
     def unique(self, *, maintain_order: bool = False) -> Self:
         """Returns unique values of the series.
@@ -2785,6 +2790,58 @@ class Series:
             self._compliant_series.cum_max(reverse=reverse)
         )
 
+    def cum_prod(self: Self, *, reverse: bool = False) -> Self:
+        r"""Return the cumulative product of the non-null values in the series.
+
+        Arguments:
+            reverse: reverse the operation
+
+        Examples:
+            >>> import narwhals as nw
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> import pyarrow as pa
+            >>> data = [1, 3, None, 2]
+
+            We define a library agnostic function:
+
+            >>> @nw.narwhalify
+            ... def func(s):
+            ...     return s.cum_prod()
+
+            We can then pass any supported library such as Pandas, Polars, or PyArrow to `func`:
+
+            >>> func(pd.Series(data))
+            0    1.0
+            1    3.0
+            2    NaN
+            3    6.0
+            dtype: float64
+            >>> func(pl.Series(data))  # doctest:+NORMALIZE_WHITESPACE
+            shape: (4,)
+            Series: '' [i64]
+            [
+               1
+               3
+               null
+               6
+            ]
+            >>> func(pa.chunked_array([data]))  # doctest:+ELLIPSIS
+            <pyarrow.lib.ChunkedArray object at ...>
+            [
+              [
+                1,
+                3,
+                null,
+                6
+              ]
+            ]
+
+        """
+        return self._from_compliant_series(
+            self._compliant_series.cum_prod(reverse=reverse)
+        )
+
     def __iter__(self: Self) -> Iterator[Any]:
         yield from self._compliant_series.__iter__()
 


Test patch:
diff --git a/tests/expr_and_series/cum_count_test.py b/tests/expr_and_series/cum_count_test.py
index e147fe58..6ddf6c99 100644
--- a/tests/expr_and_series/cum_count_test.py
+++ b/tests/expr_and_series/cum_count_test.py
@@ -9,21 +9,26 @@ from tests.utils import assert_equal_data
 
 data = {"a": ["x", "y", None, "z"]}
 
+expected = {
+    "cum_count": [1, 2, 2, 3],
+    "reverse_cum_count": [3, 2, 1, 1],
+}
 
-def test_cum_count_expr(request: pytest.FixtureRequest, constructor: Constructor) -> None:
-    if "dask" in str(constructor):
+
+@pytest.mark.parametrize("reverse", [True, False])
+def test_cum_count_expr(
+    request: pytest.FixtureRequest, constructor: Constructor, *, reverse: bool
+) -> None:
+    if "dask" in str(constructor) and reverse:
         request.applymarker(pytest.mark.xfail)
 
+    name = "reverse_cum_count" if reverse else "cum_count"
     df = nw.from_native(constructor(data))
     result = df.select(
-        cum_count=nw.col("a").cum_count(),
-        reverse_cum_count=nw.col("a").cum_count(reverse=True),
+        nw.col("a").cum_count(reverse=reverse).alias(name),
     )
-    expected = {
-        "cum_count": [1, 2, 2, 3],
-        "reverse_cum_count": [3, 2, 1, 1],
-    }
-    assert_equal_data(result, expected)
+
+    assert_equal_data(result, {name: expected[name]})
 
 
 def test_cum_count_series(constructor_eager: ConstructorEager) -> None:
diff --git a/tests/expr_and_series/cum_max_test.py b/tests/expr_and_series/cum_max_test.py
index 5351fb19..2a9c634f 100644
--- a/tests/expr_and_series/cum_max_test.py
+++ b/tests/expr_and_series/cum_max_test.py
@@ -17,8 +17,11 @@ expected = {
 }
 
 
-def test_cum_max_expr(request: pytest.FixtureRequest, constructor: Constructor) -> None:
-    if "dask" in str(constructor):
+@pytest.mark.parametrize("reverse", [True, False])
+def test_cum_max_expr(
+    request: pytest.FixtureRequest, constructor: Constructor, *, reverse: bool
+) -> None:
+    if "dask" in str(constructor) and reverse:
         request.applymarker(pytest.mark.xfail)
 
     if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor):
@@ -27,13 +30,13 @@ def test_cum_max_expr(request: pytest.FixtureRequest, constructor: Constructor)
     if PANDAS_VERSION < (2, 1) and "pandas_pyarrow" in str(constructor):
         request.applymarker(pytest.mark.xfail)
 
+    name = "reverse_cum_max" if reverse else "cum_max"
     df = nw.from_native(constructor(data))
     result = df.select(
-        cum_max=nw.col("a").cum_max(),
-        reverse_cum_max=nw.col("a").cum_max(reverse=True),
+        nw.col("a").cum_max(reverse=reverse).alias(name),
     )
 
-    assert_equal_data(result, expected)
+    assert_equal_data(result, {name: expected[name]})
 
 
 def test_cum_max_series(
diff --git a/tests/expr_and_series/cum_min_test.py b/tests/expr_and_series/cum_min_test.py
index b72f5a16..c5a2dbf9 100644
--- a/tests/expr_and_series/cum_min_test.py
+++ b/tests/expr_and_series/cum_min_test.py
@@ -17,8 +17,11 @@ expected = {
 }
 
 
-def test_cum_min_expr(request: pytest.FixtureRequest, constructor: Constructor) -> None:
-    if "dask" in str(constructor):
+@pytest.mark.parametrize("reverse", [True, False])
+def test_cum_min_expr(
+    request: pytest.FixtureRequest, constructor: Constructor, *, reverse: bool
+) -> None:
+    if "dask" in str(constructor) and reverse:
         request.applymarker(pytest.mark.xfail)
 
     if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor):
@@ -27,13 +30,13 @@ def test_cum_min_expr(request: pytest.FixtureRequest, constructor: Constructor)
     if PANDAS_VERSION < (2, 1) and "pandas_pyarrow" in str(constructor):
         request.applymarker(pytest.mark.xfail)
 
+    name = "reverse_cum_min" if reverse else "cum_min"
     df = nw.from_native(constructor(data))
     result = df.select(
-        cum_min=nw.col("a").cum_min(),
-        reverse_cum_min=nw.col("a").cum_min(reverse=True),
+        nw.col("a").cum_min(reverse=reverse).alias(name),
     )
 
-    assert_equal_data(result, expected)
+    assert_equal_data(result, {name: expected[name]})
 
 
 def test_cum_min_series(
diff --git a/tests/expr_and_series/cum_prod_test.py b/tests/expr_and_series/cum_prod_test.py
new file mode 100644
index 00000000..78e5a036
--- /dev/null
+++ b/tests/expr_and_series/cum_prod_test.py
@@ -0,0 +1,56 @@
+from __future__ import annotations
+
+import pytest
+
+import narwhals.stable.v1 as nw
+from tests.utils import PANDAS_VERSION
+from tests.utils import PYARROW_VERSION
+from tests.utils import Constructor
+from tests.utils import ConstructorEager
+from tests.utils import assert_equal_data
+
+data = {"a": [1, 2, None, 3]}
+
+expected = {
+    "cum_prod": [1, 2, None, 6],
+    "reverse_cum_prod": [6, 6, None, 3],
+}
+
+
+@pytest.mark.parametrize("reverse", [True, False])
+def test_cum_prod_expr(
+    request: pytest.FixtureRequest, constructor: Constructor, *, reverse: bool
+) -> None:
+    if "dask" in str(constructor) and reverse:
+        request.applymarker(pytest.mark.xfail)
+
+    if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+
+    if PANDAS_VERSION < (2, 1) and "pandas_pyarrow" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+
+    name = "reverse_cum_prod" if reverse else "cum_prod"
+    df = nw.from_native(constructor(data))
+    result = df.select(
+        nw.col("a").cum_prod(reverse=reverse).alias(name),
+    )
+
+    assert_equal_data(result, {name: expected[name]})
+
+
+def test_cum_prod_series(
+    request: pytest.FixtureRequest, constructor_eager: ConstructorEager
+) -> None:
+    if PYARROW_VERSION < (13, 0, 0) and "pyarrow_table" in str(constructor_eager):
+        request.applymarker(pytest.mark.xfail)
+
+    if PANDAS_VERSION < (2, 1) and "pandas_pyarrow" in str(constructor_eager):
+        request.applymarker(pytest.mark.xfail)
+
+    df = nw.from_native(constructor_eager(data), eager_only=True)
+    result = df.select(
+        cum_prod=df["a"].cum_prod(),
+        reverse_cum_prod=df["a"].cum_prod(reverse=True),
+    )
+    assert_equal_data(result, expected)
diff --git a/tests/expr_and_series/cum_sum_test.py b/tests/expr_and_series/cum_sum_test.py
index 5ea01a58..8df3396b 100644
--- a/tests/expr_and_series/cum_sum_test.py
+++ b/tests/expr_and_series/cum_sum_test.py
@@ -1,38 +1,39 @@
 from __future__ import annotations
 
+import pytest
+
 import narwhals.stable.v1 as nw
 from tests.utils import Constructor
 from tests.utils import ConstructorEager
 from tests.utils import assert_equal_data
 
-data = {
-    "a": [0, 1, 2, None, 4],
-    "b": [1, 2, 3, 5, 3],
-    "c": [5, 4, 3, 2, 1],
+data = {"a": [1, 2, None, 4]}
+expected = {
+    "cum_sum": [1, 3, None, 7],
+    "reverse_cum_sum": [7, 6, None, 4],
 }
 
 
-def test_cum_sum_simple(constructor: Constructor) -> None:
+@pytest.mark.parametrize("reverse", [True, False])
+def test_cum_sum_expr(
+    request: pytest.FixtureRequest, constructor: Constructor, *, reverse: bool
+) -> None:
+    if "dask" in str(constructor) and reverse:
+        request.applymarker(pytest.mark.xfail)
+
+    name = "reverse_cum_sum" if reverse else "cum_sum"
     df = nw.from_native(constructor(data))
-    result = df.select(nw.col("a", "b", "c").cum_sum())
-    expected = {
-        "a": [0, 1, 3, None, 7],
-        "b": [1, 3, 6, 11, 14],
-        "c": [5, 9, 12, 14, 15],
-    }
-    assert_equal_data(result, expected)
+    result = df.select(
+        nw.col("a").cum_sum(reverse=reverse).alias(name),
+    )
+
+    assert_equal_data(result, {name: expected[name]})
 
 
-def test_cum_sum_simple_series(constructor_eager: ConstructorEager) -> None:
+def test_cum_sum_series(constructor_eager: ConstructorEager) -> None:
     df = nw.from_native(constructor_eager(data), eager_only=True)
-    expected = {
-        "a": [0, 1, 3, None, 7],
-        "b": [1, 3, 6, 11, 14],
-        "c": [5, 9, 12, 14, 15],
-    }
     result = df.select(
-        df["a"].cum_sum(),
-        df["b"].cum_sum(),
-        df["c"].cum_sum(),
+        cum_sum=df["a"].cum_sum(),
+        reverse_cum_sum=df["a"].cum_sum(reverse=True),
     )
     assert_equal_data(result, expected)