Repository: modin-project/modin

Issue:
BUG: groupby fails if dataframe has at least one empty partition
### Modin version checks

- [X] I have checked that this issue has not already been reported.

- [X] I have confirmed this bug exists on the latest released version of Modin.

- [X] I have confirmed this bug exists on the main branch of Modin. (In order to do this you can follow [this guide](https://modin.readthedocs.io/en/stable/getting_started/installation.html#installing-from-the-github-master-branch).)


### Reproducible Example

```python
import pandas
from modin.test.storage_formats.pandas.test_internals import construct_modin_df_by_scheme

md_df = construct_modin_df_by_scheme(
    pandas_df=pandas.DataFrame({"a": [1, 1, 2, 2], "b": [3, 4, 5, 6]}),
    partitioning_scheme={"row_lengths": [2, 2], "column_widths": [2]}
)
partitions = md_df._query_compiler._modin_frame._partitions

print("==== Original frame ====")
print(f"1st partition content:\n{partitions[0, 0].get()}\n")
print(f"2nd partition content:\n{partitions[1, 0].get()}\n")

md_res = md_df.query("a > 1")
partitions = md_res._query_compiler._modin_frame._partitions

print("==== Query result ====")
# First partition appears to be empty because it doesn't match the query's filter
print(f"1st partition content:\n{partitions[0, 0].get()}\n")
print(f"2nd partition content:\n{partitions[1, 0].get()}\n")

grp_obj = md_res.groupby("a")

group_frame_partitioning = grp_obj._df._query_compiler._modin_frame._partitions.shape
by_column_partitioning = grp_obj._by._modin_frame._partitions.shape

print("==== Groupby object ====")
# The first empty partition is still present in the source frame
print(f"{group_frame_partitioning=}")
# The first empty partition was filtered out from the 'by' column
print(f"{by_column_partitioning=}")

print(grp_obj.count()) # index error due to partitioning missmatching
```


### Issue Description

(see under the 'error logs' spoiler for the reproducer's output)

[Lazy filtering of empty partitions](https://github.com/modin-project/modin/blob/810072cfaf2d7c1fad584b14855574db1b9066b7/modin/core/dataframe/pandas/dataframe/dataframe.py#L207) appears to act badly in cases where partitions broadcasting is required for such semi-filtered frames.

Here in the reproducer, we have a source frame `md_res` having one empty partition (in the result of the `.query()`) and its projection as a column to group on (`grp_obj._by`) that do not have empty partitions as [empties were filtered out when making this projection](https://github.com/modin-project/modin/blob/810072cfaf2d7c1fad584b14855574db1b9066b7/modin/core/dataframe/pandas/dataframe/dataframe.py#L871-L878).

As we don't verify partitioning when broadcasting projections of the frame to the frame itself (we believe that they must be partitioned identically) the broadcasting during groupby results into an error right here because `len(rt_axis_parts) == 1 (without empty partition)` and `len(left) == 2 (includes an empty partition)`:
https://github.com/modin-project/modin/blob/810072cfaf2d7c1fad584b14855574db1b9066b7/modin/core/dataframe/pandas/partitioning/partition_manager.py#L372-L387

### Expected Behavior

It's expected to work properly :)

BTW, this problem has a simple workaround found by @Egor-Krivov. Users can manually trigger filtering empty partitions out by calling an `.iloc` with an indexer bigger than the frame's length:
```python
md_res = md_df.query("a > 1")
print(md_res._query_compiler._modin_frame._partitions.shape) # (2, 1) <-- contains an empty partition

md_res = md_res.iloc[:len(md_res), :]
print(md_res._query_compiler._modin_frame._partitions.shape) # (1, 1) <-- all empty partitions were filtered out
print(md_res.groupby("a").count()) # works perfectly fine
```

### Error Logs

<details>

```python-traceback

==== Original frame ====
1st partition content:
   a  b
0  1  3
1  1  4

2nd partition content:
   a  b
2  2  5
3  2  6

==== Query result ====
1st partition content:
Empty DataFrame
Columns: [a, b]
Index: []

2nd partition content:
   a  b
2  2  5
3  2  6

==== Groupby object ====
group_frame_partitioning=(2, 1)
by_column_partitioning=(1, 1)
Traceback (most recent call last):
  File "t2.py", line 46, in <module>
    print(grp_obj.count()) # index error due to partitioning missmatching
  File "/localdisk/dchigare/repos/modin/modin/logging/logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "/localdisk/dchigare/repos/modin/modin/pandas/groupby.py", line 868, in count
    result = self._wrap_aggregation(
  File "/localdisk/dchigare/repos/modin/modin/logging/logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "/localdisk/dchigare/repos/modin/modin/pandas/groupby.py", line 1150, in _wrap_aggregation
    query_compiler=qc_method(
  File "/localdisk/dchigare/repos/modin/modin/logging/logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "/localdisk/dchigare/repos/modin/modin/core/dataframe/algebra/groupby.py", line 68, in <lambda>
    return lambda *args, **kwargs: cls.caller(
  File "/localdisk/dchigare/repos/modin/modin/core/dataframe/algebra/groupby.py", line 357, in caller
    new_modin_frame = query_compiler._modin_frame.groupby_reduce(
  File "/localdisk/dchigare/repos/modin/modin/logging/logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "/localdisk/dchigare/repos/modin/modin/core/dataframe/pandas/dataframe/dataframe.py", line 126, in run_f_on_minimally_updated_metadata
    result = f(self, *args, **kwargs)
  File "/localdisk/dchigare/repos/modin/modin/core/dataframe/pandas/dataframe/dataframe.py", line 3107, in groupby_reduce
    new_partitions = self._partition_mgr_cls.groupby_reduce(
  File "/localdisk/dchigare/repos/modin/modin/logging/logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "/localdisk/dchigare/repos/modin/modin/core/dataframe/pandas/partitioning/partition_manager.py", line 244, in groupby_reduce
    mapped_partitions = cls.broadcast_apply(
  File "/localdisk/dchigare/repos/modin/modin/logging/logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "/localdisk/dchigare/repos/modin/modin/core/dataframe/pandas/partitioning/partition_manager.py", line 58, in wait
    result = func(cls, *args, **kwargs)
  File "/localdisk/dchigare/repos/modin/modin/core/dataframe/pandas/partitioning/partition_manager.py", line 373, in broadcast_apply
    [
  File "/localdisk/dchigare/repos/modin/modin/core/dataframe/pandas/partitioning/partition_manager.py", line 374, in <listcomp>
    [
  File "/localdisk/dchigare/repos/modin/modin/core/dataframe/pandas/partitioning/partition_manager.py", line 380, in <listcomp>
    else rt_axis_parts[row_idx].list_of_blocks
IndexError: list index out of range

```

</details>


### Installed Versions

<details>

Replace this line with the output of pd.show_versions()

</details>



Discussion:
Still suffering from this one

Resolving patch:
diff --git a/modin/core/dataframe/base/dataframe/dataframe.py b/modin/core/dataframe/base/dataframe/dataframe.py
--- a/modin/core/dataframe/base/dataframe/dataframe.py
+++ b/modin/core/dataframe/base/dataframe/dataframe.py
@@ -209,7 +209,7 @@ def window(
 
         Notes
         -----
-        The user-defined reduce function must reduce each window’s column
+        The user-defined reduce function must reduce each window's column
         (row if axis=1) down to a single value.
         """
         pass
@@ -467,7 +467,7 @@ def from_labels(self) -> "ModinDataframe":
 
         Notes
         -----
-        In the case that the dataframe has hierarchical labels, all label "levels” are inserted into the dataframe
+        In the case that the dataframe has hierarchical labels, all label "levels" are inserted into the dataframe
         in the order they occur in the labels, with the outermost being in position 0.
         """
         pass
diff --git a/modin/core/dataframe/pandas/dataframe/dataframe.py b/modin/core/dataframe/pandas/dataframe/dataframe.py
--- a/modin/core/dataframe/pandas/dataframe/dataframe.py
+++ b/modin/core/dataframe/pandas/dataframe/dataframe.py
@@ -3612,6 +3612,9 @@ def groupby_reduce(
                 self._get_dict_of_block_index(axis ^ 1, numeric_indices).keys()
             )
 
+        if by_parts is not None:
+            # inplace operation
+            self._filter_empties(compute_metadata=False)
         new_partitions = self._partition_mgr_cls.groupby_reduce(
             axis, self._partitions, by_parts, map_func, reduce_func, apply_indices
         )
diff --git a/modin/core/dataframe/pandas/partitioning/partition_manager.py b/modin/core/dataframe/pandas/partitioning/partition_manager.py
--- a/modin/core/dataframe/pandas/partitioning/partition_manager.py
+++ b/modin/core/dataframe/pandas/partitioning/partition_manager.py
@@ -241,6 +241,13 @@ def groupby_reduce(
             )
 
         if by is not None:
+            # need to make sure that the partitioning of the following objects
+            # coincides in the required axis, because `partition_manager.broadcast_apply`
+            # doesn't call `_copartition` unlike `modin_frame.broadcast_apply`
+            assert partitions.shape[axis] == by.shape[axis], (
+                f"the number of partitions along {axis=} is not equal: "
+                + f"{partitions.shape[axis]} != {by.shape[axis]}"
+            )
             mapped_partitions = cls.broadcast_apply(
                 axis, map_func, left=partitions, right=by
             )


Test patch:
diff --git a/modin/test/storage_formats/pandas/test_internals.py b/modin/test/storage_formats/pandas/test_internals.py
--- a/modin/test/storage_formats/pandas/test_internals.py
+++ b/modin/test/storage_formats/pandas/test_internals.py
@@ -726,6 +726,18 @@ def test_merge_partitioning(
     )
 
 
+def test_groupby_with_empty_partition():
+    # see #5461 for details
+    md_df = construct_modin_df_by_scheme(
+        pandas_df=pandas.DataFrame({"a": [1, 1, 2, 2], "b": [3, 4, 5, 6]}),
+        partitioning_scheme={"row_lengths": [2, 2], "column_widths": [2]},
+    )
+    md_res = md_df.query("a > 1")
+    grp_obj = md_res.groupby("a")
+    # check index error due to partitioning missmatching
+    grp_obj.count()
+
+
 @pytest.mark.parametrize("set_num_partitions", [2], indirect=True)
 def test_repartitioning(set_num_partitions):
     """