Repository: dask/dask

Issue:
Implement `numeric_only` kwarg for dask.dataframe reductions
In tracking down some failures on upstream tests #7710 I realized that dask.dataframe should implement `numeric_only` for reductions like `prod`, `sum` and so on. This has some extra urgency now since pandas 1.3.0 will raise a warning if the `numeric_only` kwarg is not explicitly set.

```python
FutureWarning: Dropping of nuisance columns in DataFrame reductions (with 'numeric_only=None') is deprecated; in a future version this will raise TypeError.  Select only valid columns before calling the reduction.
```

Ping @kkraus14 and @quasiben to check if cudf needs to implement this kwarg as well.


Discussion:
cc @shwina 
cuDF supports this kwarg for reductions on `DataFrame` objects ([example](https://docs.rapids.ai/api/cudf/nightly/api.html#cudf.core.dataframe.DataFrame.max)).
Thanks! 

As a side-note since I've been thinking about optimization, it would be neat to handle `numeric_only` on the dask-side as a cull. 

Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -86,6 +86,23 @@
 pd.set_option("compute.use_numexpr", False)
 
 
+def _numeric_only(func):
+    """Decorator for methods that accept a numeric_only kwarg"""
+
+    @wraps(func)
+    def wrapper(self, *args, **kwargs):
+        # numeric_only is None by default - in that case self = self.
+        if kwargs.get("numeric_only") is False:
+            raise NotImplementedError(
+                "'numeric_only=False' is not implemented in Dask."
+            )
+        elif kwargs.get("numeric_only") is True:
+            self = self._get_numeric_data()
+        return func(self, *args, **kwargs)
+
+    return wrapper
+
+
 def _concat(args, ignore_index=False):
     if not args:
         return args
@@ -1711,6 +1728,7 @@ def any(self, axis=None, skipna=True, split_every=False, out=None):
             "any", axis=axis, skipna=skipna, split_every=split_every, out=out
         )
 
+    @_numeric_only
     @derived_from(pd.DataFrame)
     def sum(
         self,
@@ -1720,6 +1738,7 @@ def sum(
         dtype=None,
         out=None,
         min_count=None,
+        numeric_only=None,
     ):
         result = self._reduction_agg(
             "sum", axis=axis, skipna=skipna, split_every=split_every, out=out
@@ -1735,6 +1754,7 @@ def sum(
         else:
             return result
 
+    @_numeric_only
     @derived_from(pd.DataFrame)
     def prod(
         self,
@@ -1744,6 +1764,7 @@ def prod(
         dtype=None,
         out=None,
         min_count=None,
+        numeric_only=None,
     ):
         result = self._reduction_agg(
             "prod", axis=axis, skipna=skipna, split_every=split_every, out=out
@@ -1761,14 +1782,20 @@ def prod(
 
     product = prod  # aliased dd.product
 
+    @_numeric_only
     @derived_from(pd.DataFrame)
-    def max(self, axis=None, skipna=True, split_every=False, out=None):
+    def max(
+        self, axis=None, skipna=True, split_every=False, out=None, numeric_only=None
+    ):
         return self._reduction_agg(
             "max", axis=axis, skipna=skipna, split_every=split_every, out=out
         )
 
+    @_numeric_only
     @derived_from(pd.DataFrame)
-    def min(self, axis=None, skipna=True, split_every=False, out=None):
+    def min(
+        self, axis=None, skipna=True, split_every=False, out=None, numeric_only=None
+    ):
         return self._reduction_agg(
             "min", axis=axis, skipna=skipna, split_every=split_every, out=out
         )
@@ -1839,8 +1866,9 @@ def idxmin(self, axis=None, skipna=True, split_every=False):
                 result.divisions = (min(self.columns), max(self.columns))
             return result
 
+    @_numeric_only
     @derived_from(pd.DataFrame)
-    def count(self, axis=None, split_every=False):
+    def count(self, axis=None, split_every=False, numeric_only=None):
         axis = self._validate_axis(axis)
         token = self._token_prefix + "count"
         if axis == 1:
@@ -1875,8 +1903,17 @@ def mode(self, dropna=True, split_every=False):
         )
         return mode_series
 
+    @_numeric_only
     @derived_from(pd.DataFrame)
-    def mean(self, axis=None, skipna=True, split_every=False, dtype=None, out=None):
+    def mean(
+        self,
+        axis=None,
+        skipna=True,
+        split_every=False,
+        dtype=None,
+        out=None,
+        numeric_only=None,
+    ):
         axis = self._validate_axis(axis)
         _raise_if_object_series(self, "mean")
         meta = self._meta_nonempty.mean(axis=axis, skipna=skipna)
@@ -1909,9 +1946,17 @@ def mean(self, axis=None, skipna=True, split_every=False, dtype=None, out=None):
                 result.divisions = (self.columns.min(), self.columns.max())
             return handle_out(out, result)
 
+    @_numeric_only
     @derived_from(pd.DataFrame)
     def var(
-        self, axis=None, skipna=True, ddof=1, split_every=False, dtype=None, out=None
+        self,
+        axis=None,
+        skipna=True,
+        ddof=1,
+        split_every=False,
+        dtype=None,
+        out=None,
+        numeric_only=None,
     ):
         axis = self._validate_axis(axis)
         _raise_if_object_series(self, "var")
@@ -2056,9 +2101,17 @@ def _var_1d(self, column, skipna=True, ddof=1, split_every=False):
             graph, name, column._meta_nonempty.var(), divisions=[None, None]
         )
 
+    @_numeric_only
     @derived_from(pd.DataFrame)
     def std(
-        self, axis=None, skipna=True, ddof=1, split_every=False, dtype=None, out=None
+        self,
+        axis=None,
+        skipna=True,
+        ddof=1,
+        split_every=False,
+        dtype=None,
+        out=None,
+        numeric_only=None,
     ):
         axis = self._validate_axis(axis)
         _raise_if_object_series(self, "std")
@@ -2089,8 +2142,11 @@ def std(
             )
             return handle_out(out, result)
 
+    @_numeric_only
     @derived_from(pd.DataFrame)
-    def skew(self, axis=None, bias=True, nan_policy="propagate", out=None):
+    def skew(
+        self, axis=None, bias=True, nan_policy="propagate", out=None, numeric_only=None
+    ):
         """
         .. note::
 
@@ -2189,9 +2245,16 @@ def _skew_numeric(self, bias=True, nan_policy="propagate"):
             graph, name, num._meta_nonempty.skew(), divisions=[None, None]
         )
 
+    @_numeric_only
     @derived_from(pd.DataFrame)
     def kurtosis(
-        self, axis=None, fisher=True, bias=True, nan_policy="propagate", out=None
+        self,
+        axis=None,
+        fisher=True,
+        bias=True,
+        nan_policy="propagate",
+        out=None,
+        numeric_only=None,
     ):
         """
         .. note::
@@ -2302,8 +2365,9 @@ def _kurtosis_numeric(self, fisher=True, bias=True, nan_policy="propagate"):
             graph, name, num._meta_nonempty.kurtosis(), divisions=[None, None]
         )
 
+    @_numeric_only
     @derived_from(pd.DataFrame)
-    def sem(self, axis=None, skipna=None, ddof=1, split_every=False):
+    def sem(self, axis=None, skipna=None, ddof=1, split_every=False, numeric_only=None):
         axis = self._validate_axis(axis)
         _raise_if_object_series(self, "sem")
         meta = self._meta_nonempty.sem(axis=axis, skipna=skipna, ddof=ddof)


Test patch:
diff --git a/dask/dataframe/tests/test_arithmetics_reduction.py b/dask/dataframe/tests/test_arithmetics_reduction.py
--- a/dask/dataframe/tests/test_arithmetics_reduction.py
+++ b/dask/dataframe/tests/test_arithmetics_reduction.py
@@ -1235,6 +1235,71 @@ def test_reductions_frame_dtypes():
     assert_eq(df_numerics.var(), ddf_numerics.var())
 
 
+def test_reductions_frame_dtypes_numeric_only():
+    df = pd.DataFrame(
+        {
+            "int": [1, 2, 3, 4, 5, 6, 7, 8],
+            "float": [1.0, 2.0, 3.0, 4.0, np.nan, 6.0, 7.0, 8.0],
+            "dt": [pd.NaT] + [datetime(2011, i, 1) for i in range(1, 8)],
+            "str": list("abcdefgh"),
+            "timedelta": pd.to_timedelta([1, 2, 3, 4, 5, 6, 7, np.nan]),
+            "bool": [True, False] * 4,
+        }
+    )
+
+    ddf = dd.from_pandas(df, 3)
+    kwargs = {"numeric_only": True}
+    funcs = [
+        "sum",
+        "prod",
+        "product",
+        "min",
+        "max",
+        "mean",
+        "var",
+        "std",
+        "count",
+        "sem",
+    ]
+
+    for func in funcs:
+        assert_eq(
+            getattr(df, func)(**kwargs),
+            getattr(ddf, func)(**kwargs),
+            check_dtypes=func in ["mean", "max"]
+            and (PANDAS_GT_120 or not PANDAS_GT_100),
+        )
+        with pytest.raises(NotImplementedError, match="'numeric_only=False"):
+            getattr(ddf, func)(numeric_only=False)
+
+    assert_eq(df.sem(ddof=0, **kwargs), ddf.sem(ddof=0, **kwargs))
+    assert_eq(df.std(ddof=0, **kwargs), ddf.std(ddof=0, **kwargs))
+    assert_eq(df.var(ddof=0, **kwargs), ddf.var(ddof=0, **kwargs))
+    assert_eq(df.var(skipna=False, **kwargs), ddf.var(skipna=False, **kwargs))
+    assert_eq(
+        df.var(skipna=False, ddof=0, **kwargs), ddf.var(skipna=False, ddof=0, **kwargs)
+    )
+
+    # ------ only include numerics columns ------ #
+    assert_eq(df._get_numeric_data(), ddf._get_numeric_data())
+
+    df_numerics = df[["int", "float", "bool"]]
+    ddf_numerics = ddf[["int", "float", "bool"]]
+
+    assert_eq(df_numerics, ddf._get_numeric_data())
+    assert ddf_numerics._get_numeric_data().dask == ddf_numerics.dask
+
+    for func in funcs:
+        assert_eq(
+            getattr(df_numerics, func)(),
+            getattr(ddf_numerics, func)(),
+            check_dtypes=func in ["mean", "max"]
+            and (PANDAS_GT_120 or not PANDAS_GT_100),
+            check_dtype=func in ["mean", "max"]
+            and (PANDAS_GT_120 or not PANDAS_GT_100),
+        )
+
+
 @pytest.mark.parametrize("split_every", [False, 2])
 def test_reductions_frame_nan(split_every):
     df = pd.DataFrame(
diff --git a/dask/dataframe/tests/test_groupby.py b/dask/dataframe/tests/test_groupby.py
--- a/dask/dataframe/tests/test_groupby.py
+++ b/dask/dataframe/tests/test_groupby.py
@@ -1859,13 +1859,10 @@ def test_groupby_select_column_agg(func):
     assert_eq(actual, expected)
 
 
-@pytest.mark.filterwarnings(
-    "ignore:Dropping of nuisance columns:FutureWarning"
-)  # https://github.com/dask/dask/issues/7714
 @pytest.mark.parametrize(
     "func",
     [
-        lambda x: x.std(),
+        lambda x: x.std(numeric_only=True),
         lambda x: x.groupby("x").std(),
         lambda x: x.groupby("x").var(),
         lambda x: x.groupby("x").mean(),