Repository: modin-project/modin

Issue:
df.groupby('Col') falls back to Pandas claiming I'm attempting multi-column groupby
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: MacOSX 10.15.2
- **Modin version** (`modin.__version__`): 0.7.1
- **Python version**: 3.6.8 (v3.6.8:3c6b436a57, Dec 24 2018, 02:04:31) \n[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)]
- **Code we can use to reproduce**:

```python
import glob
import modin.pandas as pd

df = pd.DataFrame()
for file in glob.glob('data/*.csv'):
    df = pd.concat([df, pd.read_csv(file)])

df.shape
# (98471725, 5)

df.loc[:, 'Prev'] = df.groupby('Session').Template.shift()
```

<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->

### Describe the problem

Attempting a simple `df.groupby('col')` defaults to Pandas claiming that I'm attempting to group by on multiple columns. Maybe this is a vocabulary thing. But sounds like Modin is confused about what I'm requesting on that groupby?

![image](https://user-images.githubusercontent.com/852234/75416600-e7ef4d00-58eb-11ea-9fd9-b746a8a6821d.png)


### Source code / logs
<!-- Include any logs or source code that would be helpful to diagnose the problem. If including tracebacks, please include the full traceback. Large logs and files should be attached. Try to provide a reproducible test case that is the bare minimum necessary to generate the problem. -->



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
@@ -48,8 +48,8 @@ def __init__(
                 isinstance(by, type(self._query_compiler)) and len(by.columns) > 1
             ) or (
                 not isinstance(by, type(self._query_compiler))
-                and all(obj in self._df for obj in self._by)
                 and axis == 0
+                and all(obj in self._query_compiler.columns for obj in self._by)
             )
         else:
             self._is_multi_by = False
@@ -252,7 +252,32 @@ def backfill(self, limit=None):
         return self.bfill(limit)
 
     def __getitem__(self, key):
-        return SeriesGroupBy(self._default_to_pandas(lambda df: df.__getitem__(key)))
+        kwargs = self._kwargs.copy()
+        if self._drop:
+            if not isinstance(key, list):
+                key = [key]
+                kwargs["squeeze"] = True
+        # When `as_index` is False, pandas will always convert to a `DataFrame`, we
+        # convert to a list here so that the result will be a `DataFrame`.
+        elif not self._as_index and not isinstance(key, list):
+            key = [key]
+        if isinstance(key, list):
+            return DataFrameGroupBy(
+                self._df[key],
+                self._by,
+                self._axis,
+                idx_name=self._idx_name,
+                drop=self._drop,
+                **kwargs
+            )
+        return SeriesGroupBy(
+            self._df[key],
+            self._by,
+            self._axis,
+            idx_name=self._idx_name,
+            drop=False,
+            **kwargs
+        )
 
     def cummin(self, axis=0, **kwargs):
         result = self._apply_agg_function(lambda df: df.cummin(axis=axis, **kwargs))
@@ -443,7 +468,12 @@ def hist(self):
     def quantile(self, q=0.5, **kwargs):
         import numpy as np
 
-        if self._df.dtypes.map(lambda x: x == np.dtype("O")).any():
+        if self.ndim > 1:
+            if self._df.dtypes.map(lambda x: x == np.dtype("O")).any():
+                raise TypeError(
+                    "'quantile' cannot be performed against 'object' dtypes!"
+                )
+        elif self._df.dtypes == np.dtype("O"):
             raise TypeError("'quantile' cannot be performed against 'object' dtypes!")
         if is_list_like(q):
             return self._default_to_pandas(lambda df: df.quantile(q=q, **kwargs))
@@ -472,9 +502,7 @@ def _groupby_reduce(
         else:
             groupby_qc = self._query_compiler
 
-        from .dataframe import DataFrame
-
-        return DataFrame(
+        result = type(self._df)(
             query_compiler=groupby_qc.groupby_reduce(
                 self._by,
                 self._axis,
@@ -487,6 +515,9 @@ def _groupby_reduce(
                 drop=self._drop,
             )
         )
+        if self._kwargs.get("squeeze", False):
+            return result.squeeze()
+        return result
 
     def _apply_agg_function(self, f, drop=True, **kwargs):
         """Perform aggregation and combine stages based on a given function.
@@ -498,7 +529,6 @@ def _apply_agg_function(self, f, drop=True, **kwargs):
              A new combined DataFrame with the result of all groups.
         """
         assert callable(f), "'{0}' object is not callable".format(type(f))
-        from .dataframe import DataFrame
 
         if self._is_multi_by:
             return self._default_to_pandas(f, **kwargs)
@@ -520,7 +550,10 @@ def _apply_agg_function(self, f, drop=True, **kwargs):
         )
         if self._idx_name is not None and self._as_index:
             new_manager.index.name = self._idx_name
-        return DataFrame(query_compiler=new_manager)
+        result = type(self._df)(query_compiler=new_manager)
+        if self._kwargs.get("squeeze", False):
+            return result.squeeze()
+        return result
 
     def _default_to_pandas(self, f, **kwargs):
         """Defailts the execution of this function to pandas.
@@ -547,35 +580,37 @@ def groupby_on_multiple_columns(df):
         return self._df._default_to_pandas(groupby_on_multiple_columns)
 
 
-class SeriesGroupBy(object):  # pragma: no cover
-    def __init__(self, pandas_groupby_obj):
-        self._pandas_groupby_obj = pandas_groupby_obj
-
-    def __getattribute__(self, item):
-        if item in ["_pandas_groupby_obj"]:
-            return object.__getattribute__(self, item)
-        return_val = self._pandas_groupby_obj.__getattribute__(item)
-        from .series import Series
-        from .dataframe import DataFrame
-
-        if isinstance(return_val, pandas.Series):
-            return Series(return_val)
-        elif isinstance(return_val, pandas.DataFrame):
-            return DataFrame(return_val)
-        elif callable(return_val):
-
-            # This wraps the pandas callable and intercepts the return value before it
-            # is given back to the user to re-distribute it.
-            def wrapper(*args, **kwargs):
-                pandas_return_val = return_val(*args, **kwargs)
-                if isinstance(pandas_return_val, pandas.Series):
-                    return Series(pandas_return_val)
-                elif isinstance(pandas_return_val, pandas.DataFrame):
-                    return DataFrame(pandas_return_val)
-                else:
-                    return pandas_return_val
+class SeriesGroupBy(DataFrameGroupBy):
+    @property
+    def ndim(self):
+        return 1  # ndim is always 1 for Series
 
-            return wrapper
+    @property
+    def _iter(self):
+        from .dataframe import Series
 
+        group_ids = self._index_grouped.keys()
+        if self._axis == 0:
+            return (
+                (
+                    k,
+                    Series(
+                        query_compiler=self._query_compiler.getitem_row_array(
+                            self._index.get_indexer_for(self._index_grouped[k].unique())
+                        )
+                    ),
+                )
+                for k in (sorted(group_ids) if self._sort else group_ids)
+            )
         else:
-            return return_val
+            return (
+                (
+                    k,
+                    Series(
+                        query_compiler=self._query_compiler.getitem_column_array(
+                            self._index_grouped[k].unique()
+                        )
+                    ),
+                )
+                for k in (sorted(group_ids) if self._sort else group_ids)
+            )
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -595,18 +595,27 @@ def groupby(
     ):
         from .groupby import SeriesGroupBy
 
+        if not as_index:
+            raise TypeError("as_index=False only valid with DataFrame")
+        # SeriesGroupBy expects a query compiler object if it is available
+        if isinstance(by, Series):
+            by = by._query_compiler
+        elif callable(by):
+            by = by(self.index)
+        elif by is None and level is None:
+            raise TypeError("You have to supply one of 'by' and 'level'")
         return SeriesGroupBy(
-            self._default_to_pandas(
-                pandas.Series.groupby,
-                by=by,
-                axis=axis,
-                level=level,
-                as_index=as_index,
-                sort=sort,
-                group_keys=group_keys,
-                squeeze=squeeze,
-                observed=observed,
-            )
+            self,
+            by,
+            axis,
+            level,
+            as_index,
+            sort,
+            group_keys,
+            squeeze,
+            idx_name=None,
+            observed=observed,
+            drop=False,
         )
 
     def gt(self, other, level=None, fill_value=None, axis=0):


Test patch:
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
@@ -9,7 +9,6 @@
 
 
 def ray_df_almost_equals_pandas(ray_df, pandas_df):
-    assert isinstance(ray_df, pd.DataFrame)
     difference = to_pandas(ray_df) - pandas_df
     diff_max = difference.max().max()
     assert (
@@ -109,7 +108,7 @@ def test_mixed_dtypes_groupby(as_index):
         eval_tail(ray_groupby, pandas_groupby, n)
         eval_quantile(ray_groupby, pandas_groupby)
         eval_take(ray_groupby, pandas_groupby)
-        eval___getattr__(ray_groupby, pandas_groupby)
+        eval___getattr__(ray_groupby, pandas_groupby, "col2")
         eval_groups(ray_groupby, pandas_groupby)
 
 
@@ -198,7 +197,7 @@ def test_simple_row_groupby(by, as_index):
     eval_tail(ray_groupby, pandas_groupby, n)
     eval_quantile(ray_groupby, pandas_groupby)
     eval_take(ray_groupby, pandas_groupby)
-    eval___getattr__(ray_groupby, pandas_groupby)
+    eval___getattr__(ray_groupby, pandas_groupby, "col3")
     eval_groups(ray_groupby, pandas_groupby)
 
 
@@ -281,7 +280,7 @@ def test_single_group_row_groupby():
     eval_tail(ray_groupby, pandas_groupby, n)
     eval_quantile(ray_groupby, pandas_groupby)
     eval_take(ray_groupby, pandas_groupby)
-    eval___getattr__(ray_groupby, pandas_groupby)
+    eval___getattr__(ray_groupby, pandas_groupby, "col2")
     eval_groups(ray_groupby, pandas_groupby)
 
 
@@ -434,10 +433,88 @@ def test_simple_col_groupby():
     eval_fillna(ray_groupby, pandas_groupby)
     eval_count(ray_groupby, pandas_groupby)
     eval_take(ray_groupby, pandas_groupby)
-    eval___getattr__(ray_groupby, pandas_groupby)
     eval_groups(ray_groupby, pandas_groupby)
 
 
+@pytest.mark.parametrize(
+    "by", [np.random.randint(0, 100, size=2 ** 8), lambda x: x % 3, None]
+)
+@pytest.mark.parametrize("as_index", [True, False])
+def test_series_groupby(by, as_index):
+    series_data = np.random.randint(97, 198, size=2 ** 8)
+    modin_series = pd.Series(series_data)
+    pandas_series = pandas.Series(series_data)
+    n = 1
+
+    try:
+        pandas_groupby = pandas_series.groupby(by, as_index=as_index)
+    except Exception as e:
+        with pytest.raises(type(e)):
+            modin_series.groupby(by, as_index=as_index)
+    else:
+        modin_groupby = modin_series.groupby(by, as_index=as_index)
+
+        ray_groupby_equals_pandas(modin_groupby, pandas_groupby)
+        eval_ngroups(modin_groupby, pandas_groupby)
+        eval_ffill(modin_groupby, pandas_groupby)
+        eval_sem(modin_groupby, pandas_groupby)
+        eval_mean(modin_groupby, pandas_groupby)
+        eval_any(modin_groupby, pandas_groupby)
+        eval_min(modin_groupby, pandas_groupby)
+        eval_idxmax(modin_groupby, pandas_groupby)
+        eval_ndim(modin_groupby, pandas_groupby)
+        eval_cumsum(modin_groupby, pandas_groupby)
+        eval_pct_change(modin_groupby, pandas_groupby)
+        eval_cummax(modin_groupby, pandas_groupby)
+
+        apply_functions = [lambda df: df.sum(), min]
+        for func in apply_functions:
+            eval_apply(modin_groupby, pandas_groupby, func)
+
+        eval_first(modin_groupby, pandas_groupby)
+        eval_backfill(modin_groupby, pandas_groupby)
+        eval_cummin(modin_groupby, pandas_groupby)
+        eval_bfill(modin_groupby, pandas_groupby)
+        eval_idxmin(modin_groupby, pandas_groupby)
+        eval_prod(modin_groupby, pandas_groupby)
+        if as_index:
+            eval_std(modin_groupby, pandas_groupby)
+            eval_var(modin_groupby, pandas_groupby)
+            eval_skew(modin_groupby, pandas_groupby)
+
+        agg_functions = ["min", "max"]
+        for func in agg_functions:
+            eval_agg(modin_groupby, pandas_groupby, func)
+            eval_aggregate(modin_groupby, pandas_groupby, func)
+
+        eval_last(modin_groupby, pandas_groupby)
+        eval_mad(modin_groupby, pandas_groupby)
+        eval_rank(modin_groupby, pandas_groupby)
+        eval_max(modin_groupby, pandas_groupby)
+        eval_len(modin_groupby, pandas_groupby)
+        eval_sum(modin_groupby, pandas_groupby)
+        eval_ngroup(modin_groupby, pandas_groupby)
+        eval_nunique(modin_groupby, pandas_groupby)
+        eval_median(modin_groupby, pandas_groupby)
+        eval_head(modin_groupby, pandas_groupby, n)
+        eval_cumprod(modin_groupby, pandas_groupby)
+
+        transform_functions = [lambda df: df + 4, lambda df: -df - 10]
+        for func in transform_functions:
+            eval_transform(modin_groupby, pandas_groupby, func)
+
+        pipe_functions = [lambda dfgb: dfgb.sum()]
+        for func in pipe_functions:
+            eval_pipe(modin_groupby, pandas_groupby, func)
+
+        eval_fillna(modin_groupby, pandas_groupby)
+        eval_count(modin_groupby, pandas_groupby)
+        eval_tail(modin_groupby, pandas_groupby, n)
+        eval_quantile(modin_groupby, pandas_groupby)
+        eval_take(modin_groupby, pandas_groupby)
+        eval_groups(modin_groupby, pandas_groupby)
+
+
 def test_multi_column_groupby():
     pandas_df = pandas.DataFrame(
         {
@@ -677,12 +754,14 @@ def eval_take(ray_groupby, pandas_groupby):
             pass
 
 
-def eval___getattr__(ray_groupby, pandas_groupby):
-    with pytest.warns(UserWarning):
-        ray_groupby["col1"]
-
-    with pytest.warns(UserWarning):
-        ray_groupby.col1
+def eval___getattr__(modin_groupby, pandas_groupby, item):
+    try:
+        pandas_result = pandas_groupby[item].count()
+    except Exception as e:
+        with pytest.raises(type(e)):
+            modin_groupby[item].count()
+    else:
+        df_equals(modin_groupby[item].count(), pandas_result)
 
 
 def eval_groups(ray_groupby, pandas_groupby):
diff --git a/modin/pandas/test/test_series.py b/modin/pandas/test/test_series.py
--- a/modin/pandas/test/test_series.py
+++ b/modin/pandas/test/test_series.py
@@ -1364,16 +1364,6 @@ def test_get(data):
     )
 
 
-@pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
-def test_groupby(data):
-    modin_series, _ = create_test_series(data)
-
-    with pytest.warns(UserWarning):
-        result = modin_series.groupby(modin_series).count()
-
-    assert isinstance(result, pd.Series)
-
-
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
 def test_gt(data):
     modin_series, pandas_series = create_test_series(data)