Repository: modin-project/modin

Issue:
`groupby.mean`-like operations in Modin are significantly slower than in pandas on narrow data
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: all
- **Modin version** (`modin.__version__`):  bb7805cb1ba4e78bc6bfc65616ea1bffa387e146
- **Python version**: 3.8.11
- **Code we can use to reproduce**:

<details><summary>Benchmark</summary>

```python
import time

import numpy as np
import pandas as pd

import modin.pandas as mpd
import modin.config as cfg
import ray

cfg.BenchmarkMode.put(True)
ray.init()

def generate_data(path, nrows=5000, n_random_cols=100):
    data = {
        "Int": np.random.randint(0, 100, nrows),
        "Float": np.random.rand(nrows),
    }
    data_random = {f"col{i}":  np.random.rand(nrows) for i in range(n_random_cols)}
    data.update(data_random)
    df = mpd.DataFrame(data)
    #df.to_csv(path, index=False)
    return df

if __name__ == "__main__":
    path = "data.csv"

    mdf = generate_data(path, nrows=1_000_000, n_random_cols=9998)

    pdf = mdf._to_pandas()

    # df = df._to_pandas()
    print(f"Original shape: {pdf.shape}")

    t = time.time()
    pdf = pdf.groupby(["Int"], as_index=False).mean()
    print(f"pandas groupby.mean time: {time.time() - t} s")
    print(pdf.shape)

    t = time.time()
    mdf = mdf.groupby(["Int"], as_index=False).mean()
    print(f"modin groupby.mean time: {time.time() - t} s")
    print(mdf.shape)
```

</details>


<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->

### Describe the problem
<!-- Describe the problem clearly here. -->

`groupby.mean`-like operations in Modin are significantly slower than in pandas on narrow data

The results for Ray execution engine are follows:

1) The table with increasing number of rows, number of columns is fixed.

| Shape(rows, cols) | (100k, 32) |(1m, 32) | (10m, 32) | (100m, 32) |
| ----------- | ----------- | ----------- | ----------- |----------- |
| modin      | 1.3476 |0.6313| 2.5042|25.3606|
| pandas | 0.01814 |0.1025|0.7786|8.6838|

2) The table with increasing number of columns, number of rows is fixed.

| Shape(rows, cols) | (1m, 32) | (1m, 100) |(1m, 1k)| (1m, 10k)| 
| ----------- | ----------- | ----------- | ----------- | ----------- | 
| modin      | 0.67139 |0.5610|1.42168|23.5843|
| pandas | 0.1111 |0.22501|2.1197|55.1141|





Discussion:
Related ones #1901, #3585.

Resolving patch:
diff --git a/modin/core/storage_formats/pandas/query_compiler.py b/modin/core/storage_formats/pandas/query_compiler.py
--- a/modin/core/storage_formats/pandas/query_compiler.py
+++ b/modin/core/storage_formats/pandas/query_compiler.py
@@ -28,6 +28,7 @@
     is_list_like,
     is_numeric_dtype,
     is_datetime_or_timedelta_dtype,
+    is_datetime64_any_dtype,
 )
 from pandas.core.base import DataError
 from collections.abc import Iterable
@@ -2487,6 +2488,79 @@ def _callable_func(self, func, axis, *args, **kwargs):
     groupby_prod = GroupByReduce.register("prod")
     groupby_sum = GroupByReduce.register("sum")
 
+    def groupby_mean(self, by, axis, groupby_kwargs, agg_args, agg_kwargs, drop=False):
+        numeric_only = agg_kwargs.get("numeric_only", False)
+        datetime_cols = (
+            {
+                col: dtype
+                for col, dtype in zip(self.dtypes.index, self.dtypes)
+                if is_datetime64_any_dtype(dtype)
+            }
+            if not numeric_only
+            else dict()
+        )
+
+        if len(datetime_cols) > 0:
+            datetime_qc = self.getitem_array(datetime_cols)
+            if datetime_qc.isna().any().any(axis=1).to_pandas().squeeze():
+                return super().groupby_mean(
+                    by=by,
+                    axis=axis,
+                    groupby_kwargs=groupby_kwargs,
+                    agg_args=agg_args,
+                    agg_kwargs=agg_kwargs,
+                    drop=drop,
+                )
+
+        qc_with_converted_datetime_cols = (
+            self.astype({col: "int64" for col in datetime_cols.keys()})
+            if len(datetime_cols) > 0
+            else self
+        )
+
+        def _groupby_mean_reduce(dfgb, **kwargs):
+            """
+            Compute mean value in each group using sums/counts values within reduce phase.
+
+            Parameters
+            ----------
+            dfgb : pandas.DataFrameGroupBy
+                GroupBy object for column-partition.
+            **kwargs : dict
+                Additional keyword parameters to be passed in ``pandas.DataFrameGroupBy.sum``.
+
+            Returns
+            -------
+            pandas.DataFrame
+                A pandas Dataframe with mean values in each column of each group.
+            """
+            sums_counts_df = dfgb.sum(**kwargs)
+            sum_df = sums_counts_df.iloc[:, : len(sums_counts_df.columns) // 2]
+            count_df = sums_counts_df.iloc[:, len(sums_counts_df.columns) // 2 :]
+            return sum_df / count_df
+
+        result = GroupByReduce.register(
+            lambda dfgb, **kwargs: pandas.concat(
+                [dfgb.sum(**kwargs), dfgb.count()],
+                axis=1,
+                copy=False,
+            ),
+            _groupby_mean_reduce,
+            default_to_pandas_func=lambda dfgb, **kwargs: dfgb.mean(**kwargs),
+        )(
+            query_compiler=qc_with_converted_datetime_cols,
+            by=by,
+            axis=axis,
+            groupby_kwargs=groupby_kwargs,
+            agg_args=agg_args,
+            agg_kwargs=agg_kwargs,
+            drop=drop,
+        )
+
+        if len(datetime_cols) > 0:
+            result = result.astype({col: dtype for col, dtype in datetime_cols.items()})
+        return result
+
     def groupby_size(
         self,
         by,
diff --git a/modin/pandas/groupby.py b/modin/pandas/groupby.py
--- a/modin/pandas/groupby.py
+++ b/modin/pandas/groupby.py
@@ -136,6 +136,7 @@ def mean(self, numeric_only=None):
             self._wrap_aggregation(
                 type(self._query_compiler).groupby_mean,
                 numeric_only=numeric_only,
+                agg_kwargs=dict(numeric_only=numeric_only),
             )
         )
 


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
@@ -1516,7 +1516,20 @@ def test_dict_agg_rename_mi_columns(
 def test_agg_exceptions(operation):
     N = 256
     fill_data = [
-        ("nan_column", [None, np.datetime64("2010")] * (N // 2)),
+        (
+            "nan_column",
+            [
+                np.datetime64("2010"),
+                None,
+                np.datetime64("2007"),
+                np.datetime64("2010"),
+                np.datetime64("2006"),
+                np.datetime64("2012"),
+                None,
+                np.datetime64("2011"),
+            ]
+            * (N // 8),
+        ),
         (
             "date_column",
             [