Repository: dask/dask

Issue:
DataFrame.clip does not support argument `axis`
**Describe the issue**:

ddf.clip(...) does not take the keyword argument `axis`, which is required by pandas: 

```python
ValueError: Metadata inference failed in `clip`.
You have supplied a custom function and Dask is unable to 
determine the type of output that that function returns. 
To resolve this please provide a meta= keyword.
The docstring of the Dask function you ran should have more information.
Original error is below:
------------------------
ValueError('Must specify axis=0 or 1')
```

**Minimal Complete Verifiable Example**:

N/A

**Anything else we need to know?**:

N/A

**Environment**:

- Dask version: 2023.10.0
- Python version: 3.11.5
- Operating System: Windows 10
- Install method (conda, pip, source): pip



Discussion:

Patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -4380,12 +4380,19 @@ def between(self, left, right, inclusive="both"):
         )
 
     @derived_from(pd.Series)
-    def clip(self, lower=None, upper=None, out=None):
+    def clip(self, lower=None, upper=None, out=None, axis=None):
         if out is not None:
             raise ValueError("'out' must be None")
+        if axis not in (None, 0):
+            raise ValueError(f"Series.clip does not support axis={axis}")
         # np.clip may pass out
         return self.map_partitions(
-            M.clip, lower=lower, upper=upper, enforce_metadata=False
+            M.clip,
+            lower=lower,
+            upper=upper,
+            enforce_metadata=False,
+            # See: https://github.com/rapidsai/cudf/issues/14369
+            **({} if axis is None else {"axis": axis}),
         )
 
     @derived_from(pd.Series)
@@ -5662,11 +5669,16 @@ def dropna(self, how=no_default, subset=None, thresh=no_default):
         return self.map_partitions(M.dropna, **kwargs, enforce_metadata=False)
 
     @derived_from(pd.DataFrame)
-    def clip(self, lower=None, upper=None, out=None):
+    def clip(self, lower=None, upper=None, out=None, axis=None):
         if out is not None:
             raise ValueError("'out' must be None")
         return self.map_partitions(
-            M.clip, lower=lower, upper=upper, enforce_metadata=False
+            M.clip,
+            lower=lower,
+            upper=upper,
+            enforce_metadata=False,
+            # See: https://github.com/rapidsai/cudf/issues/14369
+            **({} if axis is None else {"axis": axis}),
         )
 
     @derived_from(pd.DataFrame)


Test patch:
diff --git a/dask/dataframe/tests/test_dataframe.py b/dask/dataframe/tests/test_dataframe.py
--- a/dask/dataframe/tests/test_dataframe.py
+++ b/dask/dataframe/tests/test_dataframe.py
@@ -849,6 +849,50 @@ def test_clip(lower, upper):
     assert_eq(ds.clip(upper=upper), s.clip(upper=upper))
 
 
+def test_clip_axis_0():
+    df = pd.DataFrame(
+        {
+            "a": [1, 2, 3, 4, 5, 6, 7, 8, 9],
+            "b": [3, 5, 2, 5, 7, 2, 4, 2, 4],
+        }
+    )
+    s = pd.Series([1, 2, 3, 4, 5, 6, 7, 8, 9])
+    l = pd.Series([3] * len(df))
+    u = pd.Series([7] * len(df))
+
+    ddf = dd.from_pandas(df, 3)
+    ds = dd.from_pandas(s, 3)
+    dl = dd.from_pandas(l, 3)
+    du = dd.from_pandas(u, 3)
+
+    assert_eq(ddf.clip(lower=dl, upper=du, axis=0), df.clip(lower=l, upper=u, axis=0))
+    assert_eq(ddf.clip(lower=dl, axis=0), df.clip(lower=l, axis=0))
+    assert_eq(ddf.clip(upper=du, axis=0), df.clip(upper=u, axis=0))
+
+    assert_eq(ds.clip(lower=dl, upper=du, axis=0), s.clip(lower=l, upper=u, axis=0))
+    assert_eq(ds.clip(lower=dl, axis=0), s.clip(lower=l, axis=0))
+    assert_eq(ds.clip(upper=du, axis=0), s.clip(upper=u, axis=0))
+
+
+def test_clip_axis_1():
+    df = pd.DataFrame(
+        {
+            "a": [1, 2, 3, 4, 5, 6, 7, 8, 9],
+            "b": [3, 5, 2, 5, 7, 2, 4, 2, 4],
+        }
+    )
+    ddf = dd.from_pandas(df, 3)
+    l = pd.Series({"a": 2, "b": 3})
+    u = pd.Series({"a": 7, "b": 5})
+
+    assert_eq(ddf.clip(lower=l, upper=u, axis=1), df.clip(lower=l, upper=u, axis=1))
+    assert_eq(ddf.clip(lower=l, axis=1), df.clip(lower=l, axis=1))
+    assert_eq(ddf.clip(upper=u, axis=1), df.clip(upper=u, axis=1))
+
+    with pytest.raises(ValueError, match="Series.clip does not support axis=1"):
+        ddf.a.clip(lower=l, upper=u, axis=1)
+
+
 def test_squeeze():
     df = pd.DataFrame({"x": [1, 3, 6]})
     df2 = pd.DataFrame({"x": [0]})