Repository: modin-project/modin

Issue:
Named aggregation doesn't work with Series objects
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**:

Ubuntu 20.04

- **Modin version** (`modin.__version__`):

0.8.2.1+6.g2b0b755

- **Python version**:

Python 3.7.8

- **Code we can use to reproduce**:

```python
import modin.pandas as pd

s1 = pd.Series([10, 10, 10, 1, 1, 1, 2, 3])
s1.name = "data"

print(s1)
print(type(s1))
gb = s1.groupby(level=0)
print(gb.groups)
s2 = gb.agg(result=("max"))
print(s2)
print(type(s2))
```

<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->

### Describe the problem
<!-- Describe the problem clearly here. -->

On Modin this code throws exception from Pandas `reconstruct_func` because it doesn't accept such arguments.
```
Traceback (most recent call last):
  File "groupby_test24.py", line 10, in <module>
    s2 = gb.agg(result=("max"))
  File "/localdisk/gashiman/modin/modin/pandas/groupby.py", line 376, in aggregate
    func, **kwargs
  File "/localdisk/gashiman/modin/modin/pandas/groupby.py", line 364, in _reconstruct_func
    func, **kwargs
  File "/localdisk/gashiman/miniconda3/envs/modin_on_omnisci/lib/python3.7/site-packages/pandas/core/aggregation.py", line 86, in reconstruct_func
    raise TypeError("Must provide 'func' or tuples of '(column, aggfunc).")
TypeError: Must provide 'func' or tuples of '(column, aggfunc).
```


Discussion:
I am able to reproduce this bug on the latest master. 

Resolving patch:
diff --git a/modin/pandas/groupby.py b/modin/pandas/groupby.py
--- a/modin/pandas/groupby.py
+++ b/modin/pandas/groupby.py
@@ -1924,8 +1924,47 @@ def nsmallest(self, n=5, keep="first"):
             )
         )
 
+    def _validate_func_kwargs(self, kwargs: dict):
+        """
+        Validate types of user-provided "named aggregation" kwargs.
+
+        Parameters
+        ----------
+        kwargs : dict
+
+        Returns
+        -------
+        columns : List[str]
+            List of user-provided keys.
+        funcs : List[Union[str, callable[...,Any]]]
+            List of user-provided aggfuncs.
+
+        Raises
+        ------
+        `TypeError` is raised if aggfunc is not `str` or callable.
+
+        Notes
+        -----
+        Copied from pandas.
+        """
+        columns = list(kwargs)
+        funcs = []
+        for col_func in kwargs.values():
+            if not (isinstance(col_func, str) or callable(col_func)):
+                raise TypeError(
+                    f"func is expected but received {type(col_func).__name__} in **kwargs."
+                )
+            funcs.append(col_func)
+        if not columns:
+            raise TypeError("Must provide 'func' or named aggregation **kwargs.")
+        return columns, funcs
+
     def aggregate(self, func=None, *args, engine=None, engine_kwargs=None, **kwargs):
         engine_default = engine is None and engine_kwargs is None
+        # if func is None, will switch to user-provided "named aggregation" kwargs
+        if func_is_none := func is None:
+            columns, func = self._validate_func_kwargs(kwargs)
+            kwargs = {}
         if isinstance(func, dict) and engine_default:
             raise SpecificationError("nested renamer is not supported")
         elif is_list_like(func) and engine_default:
@@ -1946,6 +1985,8 @@ def aggregate(self, func=None, *args, engine=None, engine_kwargs=None, **kwargs)
             # because there is no need to identify which original column's aggregation
             # the new column represents. alternatively we could give the query compiler
             # a hint that it's for a series, not a dataframe.
+            if func_is_none:
+                return result.set_axis(labels=columns, axis=1, copy=False)
             return result.set_axis(
                 labels=self._try_get_str_func(func), axis=1, copy=False
             )


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
@@ -40,6 +40,7 @@
 from .utils import (
     check_df_columns_have_nans,
     create_test_dfs,
+    create_test_series,
     default_to_pandas_ignore_string,
     df_equals,
     dict_equals,
@@ -2989,6 +2990,13 @@ def test_groupby_apply_series_result(modify_config):
     )
 
 
+def test_groupby_named_aggregation():
+    modin_ser, pandas_ser = create_test_series([10, 10, 10, 1, 1, 1, 2, 3], name="data")
+    eval_general(
+        modin_ser, pandas_ser, lambda ser: ser.groupby(level=0).agg(result=("max"))
+    )
+
+
 ### TEST GROUPBY WARNINGS ###
 
 
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
@@ -1033,6 +1033,11 @@ def create_test_dfs(*args, **kwargs):
     )
 
 
+def create_test_series(*args, **kwargs):
+    post_fn = kwargs.pop("post_fn", lambda df: df)
+    return map(post_fn, [pd.Series(*args, **kwargs), pandas.Series(*args, **kwargs)])
+
+
 def generate_dfs():
     df = pandas.DataFrame(
         {