Repository: modin-project/modin

Issue:
Align groupby objects signatures with pandas



Discussion:

Resolving patch:
diff --git a/modin/pandas/groupby.py b/modin/pandas/groupby.py
--- a/modin/pandas/groupby.py
+++ b/modin/pandas/groupby.py
@@ -239,11 +239,11 @@ def ffill(self, limit=None):
         )
         return self.fillna(limit=limit, method="ffill")
 
-    def sem(self, ddof=1):
+    def sem(self, ddof=1, numeric_only=no_default):
         return self._wrap_aggregation(
             type(self._query_compiler).groupby_sem,
             agg_kwargs=dict(ddof=ddof),
-            numeric_only=True,
+            numeric_only=numeric_only,
         )
 
     def sample(self, n=None, frac=None, replace=False, weights=None, random_state=None):
@@ -278,12 +278,22 @@ def value_counts(
             )
         )
 
-    def mean(self, numeric_only=None):
+    def mean(self, numeric_only=no_default, engine="cython", engine_kwargs=None):
+        if engine not in ("cython", None) and engine_kwargs is not None:
+            return self._default_to_pandas(
+                lambda df: df.mean(
+                    numeric_only=numeric_only,
+                    engine=engine,
+                    engine_kwargs=engine_kwargs,
+                )
+            )
         return self._check_index(
             self._wrap_aggregation(
                 type(self._query_compiler).groupby_mean,
+                agg_kwargs=dict(
+                    numeric_only=None if numeric_only is no_default else numeric_only,
+                ),
                 numeric_only=numeric_only,
-                agg_kwargs=dict(numeric_only=numeric_only),
             )
         )
 
@@ -343,20 +353,52 @@ def groups(self):
         self._groups_cache = self._compute_index_grouped(numerical=False)
         return self._groups_cache
 
-    def min(self, numeric_only=False, min_count=-1):
+    def min(self, numeric_only=False, min_count=-1, engine=None, engine_kwargs=None):
+        if engine not in ("cython", None) and engine_kwargs is not None:
+            return self._default_to_pandas(
+                lambda df: df.min(
+                    numeric_only=numeric_only,
+                    min_count=min_count,
+                    engine=engine,
+                    engine_kwargs=engine_kwargs,
+                )
+            )
         return self._wrap_aggregation(
             type(self._query_compiler).groupby_min,
+            agg_kwargs=dict(min_count=min_count),
             numeric_only=numeric_only,
+        )
+
+    def max(self, numeric_only=False, min_count=-1, engine=None, engine_kwargs=None):
+        if engine not in ("cython", None) and engine_kwargs is not None:
+            return self._default_to_pandas(
+                lambda df: df.max(
+                    numeric_only=numeric_only,
+                    min_count=min_count,
+                    engine=engine,
+                    engine_kwargs=engine_kwargs,
+                )
+            )
+        return self._wrap_aggregation(
+            type(self._query_compiler).groupby_max,
             agg_kwargs=dict(min_count=min_count),
+            numeric_only=numeric_only,
         )
 
-    def idxmax(self, axis=0, skipna=True, numeric_only=True):
+    def idxmax(self, axis=0, skipna=True, numeric_only=no_default):
         return self._wrap_aggregation(
             type(self._query_compiler).groupby_idxmax,
             agg_kwargs=dict(axis=axis, skipna=skipna),
             numeric_only=numeric_only,
         )
 
+    def idxmin(self, axis=0, skipna=True, numeric_only=no_default):
+        return self._wrap_aggregation(
+            type(self._query_compiler).groupby_idxmin,
+            agg_kwargs=dict(axis=axis, skipna=skipna),
+            numeric_only=numeric_only,
+        )
+
     @property
     def ndim(self):
         """
@@ -474,7 +516,7 @@ def indices(self):
         return self._indices_cache
 
     @_inherit_docstrings(pandas.core.groupby.DataFrameGroupBy.pct_change)
-    def pct_change(self, periods=1, fill_method="pad", limit=None, freq=None, axis=0):
+    def pct_change(self, periods=1, fill_method="ffill", limit=None, freq=None, axis=0):
         from .dataframe import DataFrame
 
         # Should check for API level errors
@@ -514,12 +556,12 @@ def filter(self, func, dropna=True, *args, **kwargs):
             lambda df: df.filter(func, dropna=dropna, *args, **kwargs)
         )
 
-    def cummax(self, axis=0, **kwargs):
+    def cummax(self, axis=0, numeric_only=False, **kwargs):
         return self._check_index_name(
             self._wrap_aggregation(
                 type(self._query_compiler).groupby_cummax,
                 agg_kwargs=dict(axis=axis, **kwargs),
-                numeric_only=False,
+                numeric_only=numeric_only,
             )
         )
 
@@ -550,11 +592,18 @@ def dtypes(self):
             )
         )
 
-    def first(self, **kwargs):
+    def first(self, numeric_only=False, min_count=-1):
         return self._wrap_aggregation(
             type(self._query_compiler).groupby_first,
-            agg_kwargs=dict(**kwargs),
-            numeric_only=False,
+            agg_kwargs=dict(min_count=min_count),
+            numeric_only=numeric_only,
+        )
+
+    def last(self, numeric_only=False, min_count=-1):
+        return self._wrap_aggregation(
+            type(self._query_compiler).groupby_last,
+            agg_kwargs=dict(min_count=min_count),
+            numeric_only=numeric_only,
         )
 
     def backfill(self, limit=None):
@@ -690,12 +739,12 @@ def __getitem__(self, key):
             **kwargs,
         )
 
-    def cummin(self, axis=0, **kwargs):
+    def cummin(self, axis=0, numeric_only=False, **kwargs):
         return self._check_index_name(
             self._wrap_aggregation(
                 type(self._query_compiler).groupby_cummin,
                 agg_kwargs=dict(axis=axis, **kwargs),
-                numeric_only=False,
+                numeric_only=numeric_only,
             )
         )
 
@@ -707,28 +756,36 @@ def bfill(self, limit=None):
         )
         return self.fillna(limit=limit, method="bfill")
 
-    def idxmin(self, axis=0, skipna=True, numeric_only=True):
-        return self._wrap_aggregation(
-            type(self._query_compiler).groupby_idxmin,
-            agg_kwargs=dict(axis=axis, skipna=skipna),
-            numeric_only=numeric_only,
-        )
-
-    def prod(self, numeric_only=None, min_count=0):
+    def prod(self, numeric_only=no_default, min_count=0):
         return self._wrap_aggregation(
             type(self._query_compiler).groupby_prod,
             agg_kwargs=dict(min_count=min_count),
             numeric_only=numeric_only,
         )
 
-    def std(self, ddof=1):
+    def std(self, ddof=1, engine=None, engine_kwargs=None, numeric_only=no_default):
+        if engine not in ("cython", None) and engine_kwargs is not None:
+            return self._default_to_pandas(
+                lambda df: df.std(
+                    ddof=ddof,
+                    engine=engine,
+                    engine_kwargs=engine_kwargs,
+                    numeric_only=numeric_only,
+                )
+            )
         return self._wrap_aggregation(
             type(self._query_compiler).groupby_std,
             agg_kwargs=dict(ddof=ddof),
-            numeric_only=True,
+            numeric_only=numeric_only,
         )
 
-    def aggregate(self, func=None, *args, **kwargs):
+    def aggregate(self, func=None, *args, engine=None, engine_kwargs=None, **kwargs):
+        if engine not in ("cython", None) and engine_kwargs is not None:
+            return self._default_to_pandas(
+                lambda df: df.aggregate(
+                    func, *args, engine=engine, engine_kwargs=engine_kwargs, **kwargs
+                )
+            )
         if self._axis != 0:
             # This is not implemented in pandas,
             # so we throw a different message
@@ -841,13 +898,6 @@ def do_relabel(obj_to_relabel):
 
     agg = aggregate
 
-    def last(self, **kwargs):
-        return self._wrap_aggregation(
-            type(self._query_compiler).groupby_last,
-            agg_kwargs=dict(**kwargs),
-            numeric_only=False,
-        )
-
     def mad(self, **kwargs):
         warnings.warn(
             (
@@ -859,10 +909,18 @@ def mad(self, **kwargs):
         )
         return self._default_to_pandas(lambda df: df.mad(**kwargs))
 
-    def rank(self, **kwargs):
+    def rank(
+        self, method="average", ascending=True, na_option="keep", pct=False, axis=0
+    ):
         result = self._wrap_aggregation(
             type(self._query_compiler).groupby_rank,
-            agg_kwargs=kwargs,
+            agg_kwargs=dict(
+                method=method,
+                ascending=ascending,
+                na_option=na_option,
+                pct=pct,
+                axis=axis,
+            ),
             numeric_only=False,
         )
         # pandas does not name the index on rank
@@ -881,18 +939,20 @@ def pad(self, limit=None):
         )
         return self.fillna(limit=limit, method="pad")
 
-    def max(self, numeric_only=False, min_count=-1):
-        return self._wrap_aggregation(
-            type(self._query_compiler).groupby_max,
-            numeric_only=numeric_only,
-            agg_kwargs=dict(min_count=min_count),
-        )
-
-    def var(self, ddof=1):
+    def var(self, ddof=1, engine=None, engine_kwargs=None, numeric_only=no_default):
+        if engine not in ("cython", None) and engine_kwargs is not None:
+            return self._default_to_pandas(
+                lambda df: df.var(
+                    ddof=ddof,
+                    engine=engine,
+                    engine_kwargs=engine_kwargs,
+                    numeric_only=numeric_only,
+                )
+            )
         return self._wrap_aggregation(
             type(self._query_compiler).groupby_var,
             agg_kwargs=dict(ddof=ddof),
-            numeric_only=True,
+            numeric_only=numeric_only,
         )
 
     def get_group(self, name, obj=None):
@@ -956,7 +1016,19 @@ def size(self):
             result.name = None
         return result.fillna(0)
 
-    def sum(self, numeric_only=None, min_count=0):
+    def sum(
+        self, numeric_only=no_default, min_count=0, engine=None, engine_kwargs=None
+    ):
+        if engine not in ("cython", None) and engine_kwargs is not None:
+            return self._default_to_pandas(
+                lambda df: df.sum(
+                    numeric_only=numeric_only,
+                    min_count=min_count,
+                    engine=engine,
+                    engine_kwargs=engine_kwargs,
+                )
+            )
+
         return self._wrap_aggregation(
             type(self._query_compiler).groupby_sum,
             agg_kwargs=dict(min_count=min_count),
@@ -977,6 +1049,9 @@ def boxplot(
         ax=None,
         figsize=None,
         layout=None,
+        sharex=False,
+        sharey=True,
+        backend=None,
         **kwargs,
     ):
         return self._default_to_pandas(
@@ -990,6 +1065,9 @@ def boxplot(
                 ax=ax,
                 figsize=figsize,
                 layout=layout,
+                sharex=sharex,
+                sharey=sharey,
+                backend=backend,
                 **kwargs,
             )
         )
@@ -1019,7 +1097,7 @@ def nunique(self, dropna=True):
     def resample(self, rule, *args, **kwargs):
         return self._default_to_pandas(lambda df: df.resample(rule, *args, **kwargs))
 
-    def median(self, numeric_only=None):
+    def median(self, numeric_only=no_default):
         return self._check_index(
             self._wrap_aggregation(
                 type(self._query_compiler).groupby_median,
@@ -1059,7 +1137,14 @@ def cov(self, min_periods=None, ddof=1, numeric_only=True):
             numeric_only=numeric_only,
         )
 
-    def transform(self, func, *args, **kwargs):
+    def transform(self, func, *args, engine=None, engine_kwargs=None, **kwargs):
+        if engine not in ("cython", None) and engine_kwargs is not None:
+            return self._default_to_pandas(
+                lambda df: df.transform(
+                    func, *args, engine=engine, engine_kwargs=engine_kwargs, **kwargs
+                )
+            )
+
         return self._check_index_name(
             self._wrap_aggregation(
                 qc_method=type(self._query_compiler).groupby_agg,
@@ -1411,7 +1496,7 @@ def _wrap_aggregation(
         agg_args = tuple() if agg_args is None else agg_args
         agg_kwargs = dict() if agg_kwargs is None else agg_kwargs
 
-        if numeric_only is None:
+        if numeric_only is None or numeric_only is no_default:
             # pandas behavior: if `numeric_only` wasn't explicitly specified then
             # the parameter is considered to be `False` if there are no numeric types
             # in the frame and `True` otherwise.
@@ -1684,10 +1769,11 @@ def nsmallest(self, n=5, keep="first"):
             )
         )
 
-    def aggregate(self, func=None, *args, **kwargs):
-        if isinstance(func, dict):
+    def aggregate(self, func=None, *args, engine=None, engine_kwargs=None, **kwargs):
+        engine_default = engine is None and engine_kwargs is None
+        if isinstance(func, dict) and engine_default:
             raise SpecificationError("nested renamer is not supported")
-        elif is_list_like(func):
+        elif is_list_like(func) and engine_default:
             from .dataframe import DataFrame
 
             result = DataFrame(
@@ -1708,7 +1794,9 @@ def aggregate(self, func=None, *args, **kwargs):
             maybe_squeezed = result.squeeze() if self._squeeze else result
             return maybe_squeezed.set_axis(labels=self._try_get_str_func(func), axis=1)
         else:
-            return super().aggregate(func, *args, **kwargs)
+            return super().aggregate(
+                func, *args, engine=engine, engine_kwargs=engine_kwargs, **kwargs
+            )
 
     agg = aggregate
 


Test patch:
diff --git a/modin/pandas/test/test_api.py b/modin/pandas/test/test_api.py
--- a/modin/pandas/test/test_api.py
+++ b/modin/pandas/test/test_api.py
@@ -229,7 +229,7 @@ def test_sparse_accessor_api_equality(obj):
 
 
 @pytest.mark.parametrize("obj", ["SeriesGroupBy", "DataFrameGroupBy"])
-def test_series_groupby_api_equality(obj):
+def test_groupby_api_equality(obj):
     modin_dir = [x for x in dir(getattr(pd.groupby, obj)) if x[0] != "_"]
     pandas_dir = [x for x in dir(getattr(pandas.core.groupby, obj)) if x[0] != "_"]
     # This attribute is hidden from the DataFrameGroupBy object
@@ -239,11 +239,16 @@ def test_series_groupby_api_equality(obj):
         len(missing_from_modin)
     )
     # FIXME: wrong inheritance
-    ignore = ["boxplot", "corrwith", "dtypes"]
+    ignore = (
+        ["boxplot", "corrwith", "dtypes"] if obj == "SeriesGroupBy" else ["boxplot"]
+    )
     extra_in_modin = set(modin_dir) - set(pandas_dir) - set(ignore)
     assert not len(extra_in_modin), "Differences found in API: {}".format(
         extra_in_modin
     )
+    assert_parameters_eq(
+        (getattr(pandas.core.groupby, obj), getattr(pd.groupby, obj)), modin_dir, ignore
+    )
 
 
 def test_series_api_equality():