Repository: modin-project/modin

Issue:
groupby_agg ingores exceptions
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**:  Windows 10 1809
- **Modin version** (`modin.__version__`):  0.7.3+41.gd617985
- **Python version**:  Python 3.7.5
- **Code we can use to reproduce**:
```python
if __name__ == "__main__":
    import pandas
    import modin.pandas as pd

    data = {
        "col1": [0, 1, 2, 3],
        "col2": [4, 5, 6, 7],
    }

    md_df, pd_df = (
        pd.DataFrame(data).astype({"col1": "category"}),
        pandas.DataFrame(data).astype({"col1": "category"}),
    )
    by = [1, 2, 1, 2]

    md_grp, pd_grp = md_df.groupby(by=by), pd_df.groupby(by=by)

    print(md_grp.quantile())  # prints an empty DataFrame
    print(pd_grp.quantile())  # throws TypeError: No matching signature found

```
<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->

### Describe the problem
<!-- Describe the problem clearly here. -->
Some of groupby aggregation operations in pandas may throw `TypeError` exception, just like `quantile` in related code. However modin in that case returns an empty DataFrame, which is different behavior from pandas.
### 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. -->
The problem is probably here: [modin/backends/pandas/query_compiler.py: def groupby_agg()](https://github.com/modin-project/modin/blob/master/modin/backends/pandas/query_compiler.py#L1251-L1260)
```python
...
grouped_df = df.groupby(by=by, axis=axis, **groupby_args)
try:
    result = agg_func(grouped_df, **agg_args)
except (DataError, TypeError):
    result = pandas.DataFrame(index=grouped_df.size().index)
return result
...
```
when our `agg_func` raises `TypeError` we catch it and try to handle that situation, but it seems that this approach sometimes gives us different from pandas behavior



Discussion:
Thanks @dchigarev this will be somewhat difficult because sometimes errors are thrown asynchronously.

Resolving patch:
diff --git a/modin/backends/pandas/query_compiler.py b/modin/backends/pandas/query_compiler.py
--- a/modin/backends/pandas/query_compiler.py
+++ b/modin/backends/pandas/query_compiler.py
@@ -1574,6 +1574,20 @@ def compute_groupby(df):
             axis, lambda df: groupby_agg_builder(df)
         )
         result = self.__constructor__(new_modin_frame)
+
+        # that means that exception in `compute_groupby` was raised
+        # in every partition, so we also should raise it
+        if len(result.columns) == 0 and len(self.columns) != 0:
+            # determening type of raised exception by applying `aggfunc`
+            # to empty DataFrame
+            try:
+                agg_func(
+                    pandas.DataFrame(index=[1], columns=[1]).groupby(level=0),
+                    **agg_args
+                )
+            except Exception as e:
+                raise type(e)("No numeric types to aggregate.")
+
         # Reset `as_index` because it was edited inplace.
         groupby_args["as_index"] = as_index
         if as_index:
diff --git a/modin/pandas/groupby.py b/modin/pandas/groupby.py
--- a/modin/pandas/groupby.py
+++ b/modin/pandas/groupby.py
@@ -348,6 +348,12 @@ def aggregate(self, func=None, *args, **kwargs):
             return self._default_to_pandas(
                 lambda df: df.aggregate(func, *args, **kwargs)
             )
+
+        if isinstance(func, str):
+            agg_func = getattr(self, func, None)
+            if callable(agg_func):
+                return agg_func(*args, **kwargs)
+
         return self._apply_agg_function(
             lambda df: df.aggregate(func, *args, **kwargs), drop=self._as_index
         )


Test patch:
diff --git a/modin/pandas/test/test_dataframe.py b/modin/pandas/test/test_dataframe.py
--- a/modin/pandas/test/test_dataframe.py
+++ b/modin/pandas/test/test_dataframe.py
@@ -55,6 +55,7 @@
     int_arg_keys,
     int_arg_values,
     eval_general,
+    create_test_dfs,
 )
 
 pd.DEFAULT_NPARTITIONS = 4
@@ -75,10 +76,6 @@ def eval_insert(modin_df, pandas_df, **kwargs):
     )
 
 
-def create_test_dfs(*args, **kwargs):
-    return pd.DataFrame(*args, **kwargs), pandas.DataFrame(*args, **kwargs)
-
-
 class TestDataFrameBinary:
     def inter_df_math_helper(self, modin_df, pandas_df, op):
         # Test dataframe to dataframe
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
@@ -16,7 +16,12 @@
 import numpy as np
 import modin.pandas as pd
 from modin.pandas.utils import from_pandas, to_pandas
-from .utils import df_equals, check_df_columns_have_nans
+from .utils import (
+    df_equals,
+    check_df_columns_have_nans,
+    create_test_dfs,
+    eval_general,
+)
 
 pd.DEFAULT_NPARTITIONS = 4
 
@@ -39,25 +44,16 @@ def modin_groupby_equals_pandas(modin_groupby, pandas_groupby):
         df_equals(g1[1], g2[1])
 
 
-def eval_general(
-    modin_groupby, pandas_groupby, fn, comparator=df_equals, is_default=False
-):
-    try:
-        pandas_result = fn(pandas_groupby)
-    except Exception as e:
-        with pytest.raises(type(e)):
-            if is_default:
-                with pytest.warns(UserWarning):
-                    fn(modin_groupby)
-            else:
-                fn(modin_groupby)
-    else:
-        if is_default:
-            with pytest.warns(UserWarning):
-                modin_result = fn(modin_groupby)
-        else:
-            modin_result = fn(modin_groupby)
-        comparator(modin_result, pandas_result)
+def eval_aggregation(md_df, pd_df, operation, by=None, *args, **kwargs):
+    if by is None:
+        by = md_df.columns[0]
+    return eval_general(
+        md_df,
+        pd_df,
+        operation=lambda df: df.groupby(by=by).agg(operation),
+        *args,
+        **kwargs,
+    )
 
 
 @pytest.mark.parametrize("as_index", [True, False])
@@ -1073,3 +1069,38 @@ def test_agg_func_None_rename():
         max=("col3", np.max), min=("col3", np.min)
     )
     df_equals(modin_result, pandas_result)
+
+
+@pytest.mark.parametrize(
+    "operation", ["quantile", "mean", "sum", "median", "unique", "cumprod"]
+)
+def test_agg_exceptions(operation):
+    N = 256
+    fill_data = [
+        ("nan_column", [None, np.datetime64("2010")] * (N // 2)),
+        (
+            "date_column",
+            [
+                np.datetime64("2010"),
+                np.datetime64("2011"),
+                np.datetime64("2011-06-15T00:00"),
+                np.datetime64("2009-01-01"),
+            ]
+            * (N // 4),
+        ),
+    ]
+
+    data1 = {
+        "column_to_by": ["foo", "bar", "baz", "bar"] * (N // 4),
+        "nan_column": [None] * N,
+    }
+
+    data2 = {
+        f"{key}{i}": value
+        for key, value in fill_data
+        for i in range(N // len(fill_data))
+    }
+
+    data = {**data1, **data2}
+
+    eval_aggregation(*create_test_dfs(data), operation=operation)
diff --git a/modin/pandas/test/utils.py b/modin/pandas/test/utils.py
--- a/modin/pandas/test/utils.py
+++ b/modin/pandas/test/utils.py
@@ -585,3 +585,7 @@ def execute_callable(fn, md_kwargs={}, pd_kwargs={}):
     values = execute_callable(operation, md_kwargs=md_kwargs, pd_kwargs=pd_kwargs)
     if values is not None:
         comparator(*values)
+
+
+def create_test_dfs(*args, **kwargs):
+    return pd.DataFrame(*args, **kwargs), pandas.DataFrame(*args, **kwargs)