Repository: modin-project/modin

Issue:
Test the interfaces of public objects for compliance with what is in pandas.
For example:
- `SeriesGroupBy`
- `DataFrameGroupBy`

This will help prevent problems such as #4154.

We are already doing this for DataFrame in https://github.com/modin-project/modin/blob/810072cfaf2d7c1fad584b14855574db1b9066b7/modin/pandas/test/test_api.py#L153 and Series in https://github.com/modin-project/modin/blob/810072cfaf2d7c1fad584b14855574db1b9066b7/modin/pandas/test/test_api.py#L242


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
@@ -223,6 +223,20 @@ def ffill(self, limit=None):
     def sem(self, ddof=1):
         return self._default_to_pandas(lambda df: df.sem(ddof=ddof))
 
+    def sample(self, n=None, frac=None, replace=False, weights=None, random_state=None):
+        return self._default_to_pandas(
+            lambda df: df.sample(
+                n=n,
+                frac=frac,
+                replace=replace,
+                weights=weights,
+                random_state=random_state,
+            )
+        )
+
+    def ewm(self, *args, **kwargs):
+        return self._default_to_pandas(lambda df: df.ewm(*args, **kwargs))
+
     def value_counts(
         self,
         subset=None,
@@ -1386,6 +1400,27 @@ def value_counts(
             )
         )
 
+    @property
+    def is_monotonic_decreasing(self):
+        return self._default_to_pandas(lambda ser: ser.is_monotonic_decreasing)
+
+    @property
+    def is_monotonic_increasing(self):
+        return self._default_to_pandas(lambda ser: ser.is_monotonic_increasing)
+
+    def nlargest(self, n: int = 5, keep: str = "first") -> Series:
+        return self._default_to_pandas(lambda ser: ser.nlargest(n=n, keep=keep))
+
+    def nsmallest(self, n: int = 5, keep: str = "first") -> Series:
+        return self._default_to_pandas(lambda ser: ser.nsmallest(n=n, keep=keep))
+
+    def unique(self):
+        return self._default_to_pandas(lambda ser: ser.unique())
+
+    @property
+    def dtype(self):
+        return self._default_to_pandas(lambda ser: ser.dtype)
+
 
 if IsExperimental.get():
     from modin.experimental.cloud.meta_magic import make_wrapped_class


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
@@ -13,8 +13,8 @@
 
 import pandas
 import inspect
-import numpy as np
 import pytest
+import numpy as np
 
 import modin.pandas as pd
 
@@ -293,6 +293,24 @@ def test_sparse_accessor_api_equality(obj):
     )
 
 
+@pytest.mark.parametrize("obj", ["SeriesGroupBy", "DataFrameGroupBy"])
+def test_series_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
+    ignore = ["keys"]
+    missing_from_modin = set(pandas_dir) - set(modin_dir) - set(ignore)
+    assert not len(missing_from_modin), "Differences found in API: {}".format(
+        len(missing_from_modin)
+    )
+    # FIXME: wrong inheritance
+    ignore = ["boxplot", "corrwith", "dtypes"]
+    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
+    )
+
+
 def test_series_api_equality():
     modin_dir = [obj for obj in dir(pd.Series) if obj[0] != "_"]
     pandas_dir = [obj for obj in dir(pandas.Series) if obj[0] != "_"]
diff --git a/modin/pandas/test/test_groupby.py b/modin/pandas/test/test_groupby.py
--- a/modin/pandas/test/test_groupby.py
+++ b/modin/pandas/test/test_groupby.py
@@ -137,6 +137,10 @@ def test_mixed_dtypes_groupby(as_index):
             lambda df: df.sem(),
             modin_df_almost_equals_pandas,
         )
+        eval_general(
+            modin_groupby, pandas_groupby, lambda df: df.sample(random_state=1)
+        )
+        eval_general(modin_groupby, pandas_groupby, lambda df: df.ewm(com=0.5).std())
         eval_shift(modin_groupby, pandas_groupby)
         eval_mean(modin_groupby, pandas_groupby)
         eval_any(modin_groupby, pandas_groupby)
@@ -902,6 +906,20 @@ def test_series_groupby(by, as_index_series_or_dataframe):
             lambda df: df.sem(),
             modin_df_almost_equals_pandas,
         )
+        eval_general(
+            modin_groupby, pandas_groupby, lambda df: df.sample(random_state=1)
+        )
+        eval_general(modin_groupby, pandas_groupby, lambda df: df.ewm(com=0.5).std())
+        eval_general(
+            modin_groupby, pandas_groupby, lambda df: df.is_monotonic_decreasing
+        )
+        eval_general(
+            modin_groupby, pandas_groupby, lambda df: df.is_monotonic_increasing
+        )
+        eval_general(modin_groupby, pandas_groupby, lambda df: df.nlargest())
+        eval_general(modin_groupby, pandas_groupby, lambda df: df.nsmallest())
+        eval_general(modin_groupby, pandas_groupby, lambda df: df.unique())
+        eval_general(modin_groupby, pandas_groupby, lambda df: df.dtype)
         eval_mean(modin_groupby, pandas_groupby)
         eval_any(modin_groupby, pandas_groupby)
         eval_min(modin_groupby, pandas_groupby)