Repository: modin-project/modin

Issue:
Does the Fold operator allow a function to change the shape of partitions?
On the [Operators Module Description page](https://modin.readthedocs.io/en/stable/flow/modin/core/dataframe/algebra.html), the Map operator has a note that "map function should not change the shape of the partitions."

The Fold operator has no such note, but when I try to run an example that changes the shape, it doesn't work:

```python
import pandas
import modin.pandas as pd

modin_df = pd.DataFrame(pandas.DataFrame({"a": range(0, 1_000), "b": range(500, 1_500)}))

from modin.core.storage_formats import PandasQueryCompiler
from modin.core.dataframe.algebra import Fold

PandasQueryCompiler.filter_func = Fold.register(lambda df: df[df.index % 2 == 0])


def filter_modin_dataframe(df):
    return df.__constructor__(
        query_compiler=df._query_compiler.filter_func(
            fold_axis=1,
        )
    )


pd.DataFrame.filter_dataframe = filter_modin_dataframe

filtered_df = modin_df.filter_dataframe()

print(filtered_df)
```

fails with `IndexError: positional indexers are out-of-bounds`.

Is this intentional? If so, what would be the recommended way to do a fold-like operation, applying a function that requires knowledge of an entire axis, and creating a DataFrame from the resulting partitions?

Note that in this example I am only changing the shape along the axis I am folding on, so it's impossible for this to cause an illogical outcome such as some rows having more columns than others. I don't see a reason why the opposite shouldn't be allowed as well, as long as it doesn't result in such an illogical outcome.


Discussion:
By the way, the equivalent functionality in Dask Dataframe is [`.map_partitions`](https://docs.dask.org/en/stable/generated/dask.dataframe.DataFrame.map_partitions.html), which because Dask has 1D partitions corresponds to a Modin Fold with fold_axis=1. It says in the Dask docs that "the index and divisions are assumed to remain unchanged" but it doesn't actually stop you from changing them.

Here is an example of how the code above can work with Dask:

```python
if __name__ == "__main__":
    from distributed import Client

    client = Client(n_workers=2, threads_per_worker=1)

    import pandas
    import dask.dataframe as dd

    dask_df = dd.from_pandas(
        pandas.DataFrame({"a": range(0, 1_000), "b": range(500, 1_500)}), npartitions=2
    )

    filtered_df = dask_df.map_partitions(lambda df: df[df.index % 2 == 0])

    print(filtered_df.compute())
```
I found this workaround, though it uses private, undocumented attributes:

```python
import pandas
import modin.pandas as pd

modin_df = pd.DataFrame(pandas.DataFrame({"a": range(0, 1_000), "b": range(500, 1_500)}))

from modin.core.storage_formats import PandasQueryCompiler

filtered_df = pd.DataFrame(
    query_compiler=PandasQueryCompiler(
        modin_df._query_compiler._modin_frame.apply_full_axis(
            axis=1, func=lambda df: df[df.index % 2 == 0]
        )
    )
)

print(filtered_df)
```

Note: I am of course aware that none of this necessary to get only the even-numbered rows of a dataframe, that is just a stand-in for an arbitrary computation that maps DataFrame -> DataFrame with a different shape.
> The Fold operator has no such note, but when I try to run an example that changes the shape, it doesn't work:

Hi @zmbc! Good observation.

This doesn't work because we are unconditionally copying the index metadata, relying on the shape of the result being unchanged. If they are not copied, then your example with the Fold operator starts working.
https://github.com/modin-project/modin/blob/29d9da056a4e4766314c4ae9ea62b33e0d52e662/modin/core/dataframe/pandas/dataframe/dataframe.py#L2118-L2124

I guess we could implement this operator by adding an additional flag that would control when the metadata should be copied. @dchigarev what do you think?
Ah, I see. Is that the same reason why the Map operator requires that the shape doesn't change?

I do think it could be really useful to have operators that are more flexible (but of course slower), like Map or Fold but allowed to change the shape of each partition. Copying the metadata is a nice optimization when the shape doesn't change, but it doesn't seem like it would be absolutely critical to performance in most cases.
> Ah, I see. Is that the same reason why the Map operator requires that the shape doesn't change?

Looks like that.

> I do think it could be really useful to have operators that are more flexible (but of course slower), like Map or Fold but allowed to change the shape of each partition. Copying the metadata is a nice optimization when the shape doesn't change, but it doesn't seem like it would be absolutely critical to performance in most cases.

Agree.

It looks like filter operator is suitable here. @anmyachev, do you think we should lift it up to the algebra module?
> It looks like filter operator is suitable here. @anmyachev, do you think we should lift it up to the algebra module?

Could you write in more detail? It seems to me that it is possible to implement operators `Map` and `Fold` more flexible, as @zmbc wrote about.
`PandasQueryCompiler.filter_func = Fold.register(lambda df: df[df.index % 2 == 0])` is a normal filter operator. We have filter method at the Modin Dataframe layer but we don't have it in algebra module so we can add it there. As to `Map` and `Fold` operators, I wouldn't like to change their definition since they have already been documented well in different articles/papers.
@YarShev the `df[df.index % 2 == 0]` was just a toy example. In my actual use case, I want to do an operation that filters _and_ changes values _and_ adds/drops columns _and_ changes dtypes.
> As to Map and Fold operators, I wouldn't like to change their definition since they have already been documented well in different articles/papers.

@YarShev regarding `Fold` operator, at the algebra level we did not mention anywhere that it should preserve the shape of a dataframe (I didn’t find this in Devin’s articles either). Therefore, by allowing it to change shape, we give users flexibility and do not contradict what we have already written.

> I guess we could implement this operator by adding an additional flag that would control when the metadata should be copied.

I still think this approach might be acceptable here.
We have a note in the docstring: "The data shape is not changed (length and width of the table).". However, I agree with you and we can expand Fold operator to be more flexible.

Resolving patch:
diff --git a/modin/core/dataframe/algebra/fold.py b/modin/core/dataframe/algebra/fold.py
--- a/modin/core/dataframe/algebra/fold.py
+++ b/modin/core/dataframe/algebra/fold.py
@@ -30,7 +30,7 @@ class Fold(Operator):
 
     @classmethod
     def register(
-        cls, fold_function: Callable[..., pandas.DataFrame]
+        cls, fold_function: Callable[..., pandas.DataFrame], shape_preserved=False
     ) -> Callable[..., PandasQueryCompiler]:
         """
         Build Fold operator that will be performed across rows/columns.
@@ -39,6 +39,9 @@ def register(
         ----------
         fold_function : callable(pandas.DataFrame, *args, **kwargs) -> pandas.DataFrame
             Function to apply across rows/columns.
+        shape_preserved : bool, default: False
+            Whether the shape of the dataframe is preserved or not
+            after applying a function.
 
         Returns
         -------
@@ -50,6 +53,8 @@ def caller(
             query_compiler: PandasQueryCompiler,
             fold_axis: Optional[int] = None,
             *args: tuple,
+            new_index=None,
+            new_columns=None,
             **kwargs: dict,
         ) -> PandasQueryCompiler:
             """
@@ -64,6 +69,10 @@ def caller(
                 apply across full row partitions.
             *args : tuple
                 Additional arguments passed to `fold_function`.
+            new_index : list-like, optional
+                The index of the result.
+            new_columns : list-like, optional
+                The columns of the result.
             **kwargs: dict
                 Additional keyword arguments passed to `fold_function`.
 
@@ -77,6 +86,9 @@ def caller(
                 query_compiler._modin_frame.fold(
                     cls.validate_axis(fold_axis),
                     lambda x: fold_function(x, *args, **kwargs),
+                    new_index=new_index,
+                    new_columns=new_columns,
+                    shape_preserved=shape_preserved,
                 )
             )
 
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
@@ -2321,7 +2321,7 @@ def window(
         pass
 
     @lazy_metadata_decorator(apply_axis="both")
-    def fold(self, axis, func, new_columns=None):
+    def fold(self, axis, func, new_index=None, new_columns=None, shape_preserved=False):
         """
         Perform a function across an entire axis.
 
@@ -2331,37 +2331,38 @@ def fold(self, axis, func, new_columns=None):
             The axis to apply over.
         func : callable
             The function to apply.
+        new_index : list-like, optional
+            The index of the result.
         new_columns : list-like, optional
             The columns of the result.
-            Must be the same length as the columns' length of `self`.
-            The column labels of `self` may change during an operation so
-            we may want to pass the new column labels in (e.g., see `cat.codes`).
+        shape_preserved : bool, default: False
+            Whether the shape of the dataframe is preserved or not
+            after applying a function.
 
         Returns
         -------
         PandasDataframe
             A new dataframe.
-
-        Notes
-        -----
-        The data shape is not changed (length and width of the table).
         """
-        if new_columns is not None:
-            if self.has_materialized_columns:
-                assert len(self.columns) == len(
-                    new_columns
-                ), "The length of `new_columns` doesn't match the columns' length of `self`"
-            self.set_columns_cache(new_columns)
+        new_row_lengths = None
+        new_column_widths = None
+        if shape_preserved:
+            if new_index is None:
+                new_index = self.copy_index_cache(copy_lengths=True)
+            if new_columns is None:
+                new_columns = self.copy_columns_cache(copy_lengths=True)
+            new_row_lengths = self._row_lengths_cache
+            new_column_widths = self._column_widths_cache
 
         new_partitions = self._partition_mgr_cls.map_axis_partitions(
             axis, self._partitions, func, keep_partitioning=True
         )
         return self.__constructor__(
             new_partitions,
-            self.copy_index_cache(copy_lengths=True),
-            self.copy_columns_cache(copy_lengths=True),
-            self._row_lengths_cache,
-            self._column_widths_cache,
+            new_index,
+            new_columns,
+            row_lengths=new_row_lengths,
+            column_widths=new_column_widths,
             pandas_backend=self._pandas_backend,
         )
 
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
@@ -1344,49 +1344,57 @@ def expanding_aggregate(self, axis, expanding_args, func, *args, **kwargs):
     expanding_sum = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).sum(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     expanding_min = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).min(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     expanding_max = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).max(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     expanding_mean = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).mean(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     expanding_median = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).median(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     expanding_var = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).var(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     expanding_std = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).std(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     expanding_count = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).count(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     def expanding_cov(
@@ -1427,7 +1435,8 @@ def expanding_cov(
                 (df.squeeze(axis=1) if squeeze_self else df)
                 .expanding(*expanding_args)
                 .cov(*args, **kwargs)
-            )
+            ),
+            shape_preserved=True,
         )(
             self,
             fold_axis,
@@ -1477,7 +1486,8 @@ def expanding_corr(
                 (df.squeeze(axis=1) if squeeze_self else df)
                 .expanding(*expanding_args)
                 .corr(*args, **kwargs)
-            )
+            ),
+            shape_preserved=True,
         )(
             self,
             fold_axis,
@@ -1492,107 +1502,127 @@ def expanding_corr(
     expanding_quantile = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).quantile(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     expanding_sem = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).sem(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     expanding_kurt = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).kurt(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     expanding_skew = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).skew(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     expanding_rank = Fold.register(
         lambda df, expanding_args, *args, **kwargs: pandas.DataFrame(
             df.expanding(*expanding_args).rank(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
 
     window_mean = Fold.register(
         lambda df, rolling_kwargs, *args, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).mean(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     window_sum = Fold.register(
         lambda df, rolling_kwargs, *args, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).sum(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     window_var = Fold.register(
         lambda df, rolling_kwargs, ddof, *args, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).var(ddof=ddof, *args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     window_std = Fold.register(
         lambda df, rolling_kwargs, ddof, *args, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).std(ddof=ddof, *args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     rolling_count = Fold.register(
         lambda df, rolling_kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).count()
-        )
+        ),
+        shape_preserved=True,
     )
     rolling_sum = Fold.register(
         lambda df, rolling_kwargs, *args, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).sum(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     rolling_sem = Fold.register(
         lambda df, rolling_kwargs, *args, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).sem(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     rolling_mean = Fold.register(
         lambda df, rolling_kwargs, *args, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).mean(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     rolling_median = Fold.register(
         lambda df, rolling_kwargs, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).median(**kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     rolling_var = Fold.register(
         lambda df, rolling_kwargs, ddof, *args, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).var(ddof=ddof, *args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     rolling_std = Fold.register(
         lambda df, rolling_kwargs, ddof, *args, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).std(ddof=ddof, *args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     rolling_min = Fold.register(
         lambda df, rolling_kwargs, *args, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).min(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     rolling_max = Fold.register(
         lambda df, rolling_kwargs, *args, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).max(*args, **kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     rolling_skew = Fold.register(
         lambda df, rolling_kwargs, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).skew(**kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     rolling_kurt = Fold.register(
         lambda df, rolling_kwargs, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).kurt(**kwargs)
-        )
+        ),
+        shape_preserved=True,
     )
     rolling_apply = Fold.register(
         lambda df, rolling_kwargs, func, raw, engine, engine_kwargs, args, kwargs: pandas.DataFrame(
@@ -1603,15 +1633,17 @@ def expanding_corr(
                 engine_kwargs=engine_kwargs,
                 args=args,
                 kwargs=kwargs,
-            )
-        )
+            ),
+        ),
+        shape_preserved=True,
     )
     rolling_quantile = Fold.register(
         lambda df, rolling_kwargs, q, interpolation, **kwargs: pandas.DataFrame(
             df.rolling(**rolling_kwargs).quantile(
                 q=q, interpolation=interpolation, **kwargs
-            )
-        )
+            ),
+        ),
+        shape_preserved=True,
     )
     rolling_rank = Fold.register(
         lambda df, rolling_kwargs, method, ascending, pct, numeric_only, **kwargs: pandas.DataFrame(
@@ -1621,8 +1653,9 @@ def expanding_corr(
                 pct=pct,
                 numeric_only=numeric_only,
                 **kwargs,
-            )
-        )
+            ),
+        ),
+        shape_preserved=True,
     )
 
     def rolling_corr(self, axis, rolling_kwargs, other, pairwise, *args, **kwargs):
@@ -1638,7 +1671,8 @@ def rolling_corr(self, axis, rolling_kwargs, other, pairwise, *args, **kwargs):
                     df.rolling(**rolling_kwargs).corr(
                         other=other, pairwise=pairwise, *args, **kwargs
                     )
-                )
+                ),
+                shape_preserved=True,
             )(self, axis)
 
     def rolling_cov(self, axis, rolling_kwargs, other, pairwise, ddof, **kwargs):
@@ -1654,7 +1688,8 @@ def rolling_cov(self, axis, rolling_kwargs, other, pairwise, ddof, **kwargs):
                     df.rolling(**rolling_kwargs).cov(
                         other=other, pairwise=pairwise, ddof=ddof, **kwargs
                     )
-                )
+                ),
+                shape_preserved=True,
             )(self, axis)
 
     def rolling_aggregate(self, axis, rolling_kwargs, func, *args, **kwargs):
@@ -1878,7 +1913,7 @@ def convert_dtypes(
         convert_floating: bool = True,
         dtype_backend: str = "numpy_nullable",
     ):
-        result = Fold.register(pandas.DataFrame.convert_dtypes)(
+        result = Fold.register(pandas.DataFrame.convert_dtypes, shape_preserved=True)(
             self,
             infer_objects=infer_objects,
             convert_string=convert_string,
@@ -2258,11 +2293,11 @@ def describe_builder(df, internal_indices=[]):  # pragma: no cover
     # that is being operated on. This means that we have to put all of that
     # data in the same place.
 
-    cummax = Fold.register(pandas.DataFrame.cummax)
-    cummin = Fold.register(pandas.DataFrame.cummin)
-    cumsum = Fold.register(pandas.DataFrame.cumsum)
-    cumprod = Fold.register(pandas.DataFrame.cumprod)
-    _diff = Fold.register(pandas.DataFrame.diff)
+    cummax = Fold.register(pandas.DataFrame.cummax, shape_preserved=True)
+    cummin = Fold.register(pandas.DataFrame.cummin, shape_preserved=True)
+    cumsum = Fold.register(pandas.DataFrame.cumsum, shape_preserved=True)
+    cumprod = Fold.register(pandas.DataFrame.cumprod, shape_preserved=True)
+    _diff = Fold.register(pandas.DataFrame.diff, shape_preserved=True)
 
     def diff(self, axis, periods):
         return self._diff(fold_axis=axis, axis=axis, periods=periods)
@@ -2276,7 +2311,9 @@ def clip(self, lower, upper, **kwargs):
         kwargs["lower"] = lower
         axis = kwargs.get("axis", 0)
         if is_list_like(lower) or is_list_like(upper):
-            new_modin_frame = self._modin_frame.fold(axis, lambda df: df.clip(**kwargs))
+            new_modin_frame = self._modin_frame.fold(
+                axis, lambda df: df.clip(**kwargs), shape_preserved=True
+            )
         else:
             new_modin_frame = self._modin_frame.map(lambda df: df.clip(**kwargs))
         return self.__constructor__(new_modin_frame)
@@ -2637,7 +2674,7 @@ def fillna(df):
                 return df.fillna(value=value, **kwargs)
 
         if full_axis:
-            new_modin_frame = self._modin_frame.fold(axis, fillna)
+            new_modin_frame = self._modin_frame.fold(axis, fillna, shape_preserved=True)
         else:
             new_modin_frame = self._modin_frame.map(fillna, dtypes=new_dtypes)
         return self.__constructor__(new_modin_frame)


Test patch:
diff --git a/modin/tests/core/storage_formats/pandas/test_internals.py b/modin/tests/core/storage_formats/pandas/test_internals.py
--- a/modin/tests/core/storage_formats/pandas/test_internals.py
+++ b/modin/tests/core/storage_formats/pandas/test_internals.py
@@ -26,8 +26,10 @@
     MinPartitionSize,
     NPartitions,
     RangePartitioning,
+    StorageFormat,
     context,
 )
+from modin.core.dataframe.algebra import Fold
 from modin.core.dataframe.pandas.dataframe.dataframe import PandasDataframe
 from modin.core.dataframe.pandas.dataframe.utils import ColumnInfo, ShuffleSortFunctions
 from modin.core.dataframe.pandas.metadata import (
@@ -36,6 +38,7 @@
     ModinDtypes,
 )
 from modin.core.execution.utils import remote_function
+from modin.core.storage_formats import PandasQueryCompiler
 from modin.core.storage_formats.pandas.utils import split_result_of_axis_func_pandas
 from modin.distributed.dataframe.pandas import from_partitions
 from modin.tests.pandas.utils import (
@@ -2705,3 +2708,52 @@ def map_func(df, first_arg, extra_arg=0):
             assert np.all(
                 result_partitions[i][0].to_numpy() == 3
             ), "Invalid map function result."
+
+
+@pytest.mark.skipif(
+    StorageFormat.get() == "Hdk",
+    reason="HDK is deprecated and doesn't allow to register a custom function.",
+)
+def test_fold_operator():
+    new_index = list(range(500, 1000))
+    new_columns = ["b"]
+
+    initial_df = pandas.DataFrame({"a": range(0, 1000)})
+    modin_df = pd.DataFrame(initial_df)
+    expected_df = pandas.DataFrame(
+        list(range(0, 1000, 2)), index=new_index, columns=new_columns
+    )
+
+    def filter_func(df):
+        result = df[df.index % 2 == 0]
+        result.index = new_index
+        result.columns = new_columns
+        return result
+
+    PandasQueryCompiler.filter_func = Fold.register(filter_func)
+
+    def filter_modin_dataframe1(df):
+        return df.__constructor__(
+            query_compiler=df._query_compiler.filter_func(
+                fold_axis=0,
+                new_index=new_index,
+                new_columns=new_columns,
+            )
+        )
+
+    pd.DataFrame.filter_dataframe1 = filter_modin_dataframe1
+
+    filtered_df = modin_df.filter_dataframe1()
+
+    df_equals(filtered_df, expected_df)
+
+    def filter_modin_dataframe2(df):
+        return df.__constructor__(
+            query_compiler=df._query_compiler.filter_func(fold_axis=0)
+        )
+
+    pd.DataFrame.filter_dataframe2 = filter_modin_dataframe2
+
+    filtered_df = modin_df.filter_dataframe2()
+
+    df_equals(filtered_df, expected_df)