Repository: modin-project/modin

Issue:
BUG: Series.equals with all-NA entries
### 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
ser = pd.Series([0, 1, None], dtype="uint8[pyarrow]")
ser2 = pd.Series([None, None, None], dtype="uint8[pyarrow]")

>>> ser.equals(ser2)
True
```


### Issue Description

Should be False

### Expected Behavior

NA

### Error Logs

<details>

```python-traceback

Replace this line with the error backtrace (if applicable).

```

</details>


### Installed Versions

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



Discussion:

Patch:
diff --git a/modin/core/dataframe/algebra/binary.py b/modin/core/dataframe/algebra/binary.py
--- a/modin/core/dataframe/algebra/binary.py
+++ b/modin/core/dataframe/algebra/binary.py
@@ -382,6 +382,7 @@ def caller(
                             lambda x, y: func(x, y, *args, **kwargs),
                             [other._modin_frame],
                             join_type=join_type,
+                            labels=labels,
                             dtypes=dtypes,
                         ),
                         shape_hint=shape_hint,
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
@@ -3280,6 +3280,7 @@ def n_ary_op(
         right_frames: list,
         join_type="outer",
         copartition_along_columns=True,
+        labels="replace",
         dtypes=None,
     ):
         """
@@ -3296,6 +3297,9 @@ def n_ary_op(
         copartition_along_columns : bool, default: True
             Whether to perform copartitioning along columns or not.
             For some ops this isn't needed (e.g., `fillna`).
+        labels : {"replace", "drop"}, default: "replace"
+            Whether use labels from joined DataFrame or drop altogether to make
+            them be computed lazily later.
         dtypes : series, default: None
             Dtypes of the resultant dataframe, this argument will be
             received if the resultant dtypes of n-opary operation is precomputed.
@@ -3346,6 +3350,8 @@ def n_ary_op(
                 left_parts, op, list_of_right_parts
             )
         )
+        if labels == "drop":
+            joined_index = joined_columns = row_lengths = column_widths = None
 
         return self.__constructor__(
             new_frame,
diff --git a/modin/core/storage_formats/base/query_compiler.py b/modin/core/storage_formats/base/query_compiler.py
--- a/modin/core/storage_formats/base/query_compiler.py
+++ b/modin/core/storage_formats/base/query_compiler.py
@@ -579,6 +579,10 @@ def combine_first(self, other, **kwargs):  # noqa: PR02
     def eq(self, other, **kwargs):  # noqa: PR02
         return BinaryDefault.register(pandas.DataFrame.eq)(self, other=other, **kwargs)
 
+    @doc_utils.add_refer_to("DataFrame.equals")
+    def equals(self, other):  # noqa: PR01, RT01
+        return BinaryDefault.register(pandas.DataFrame.equals)(self, other=other)
+
     @doc_utils.doc_binary_method(operation="integer division", sign="//")
     def floordiv(self, other, **kwargs):  # noqa: PR02
         return BinaryDefault.register(pandas.DataFrame.floordiv)(
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
@@ -390,6 +390,12 @@ def to_numpy(self, **kwargs):
     combine = Binary.register(pandas.DataFrame.combine, infer_dtypes="common_cast")
     combine_first = Binary.register(pandas.DataFrame.combine_first, infer_dtypes="bool")
     eq = Binary.register(pandas.DataFrame.eq, infer_dtypes="bool")
+    equals = Binary.register(
+        lambda df, other: pandas.DataFrame([[df.equals(other)]]),
+        join_type=None,
+        labels="drop",
+        infer_dtypes="bool",
+    )
     floordiv = Binary.register(pandas.DataFrame.floordiv, infer_dtypes="common_cast")
     ge = Binary.register(pandas.DataFrame.ge, infer_dtypes="bool")
     gt = Binary.register(pandas.DataFrame.gt, infer_dtypes="bool")
diff --git a/modin/pandas/dataframe.py b/modin/pandas/dataframe.py
--- a/modin/pandas/dataframe.py
+++ b/modin/pandas/dataframe.py
@@ -759,11 +759,18 @@ def equals(self, other):  # noqa: PR01, RT01, D200
         if isinstance(other, pandas.DataFrame):
             # Copy into a Modin DataFrame to simplify logic below
             other = self.__constructor__(other)
-        return (
-            self.index.equals(other.index)
-            and self.columns.equals(other.columns)
-            and self.eq(other).all().all()
+
+        if (
+            type(self) != type(other)
+            or not self.index.equals(other.index)
+            or not self.columns.equals(other.columns)
+        ):
+            return False
+
+        result = self.__constructor__(
+            query_compiler=self._query_compiler.equals(other._query_compiler)
         )
+        return result.all(axis=None)
 
     def _update_var_dicts_in_kwargs(self, expr, kwargs):
         """
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -926,11 +926,26 @@ def equals(self, other):  # noqa: PR01, RT01, D200
         """
         Test whether two objects contain the same elements.
         """
-        return (
-            self.name == other.name
-            and self.index.equals(other.index)
-            and self.eq(other).all()
-        )
+        if isinstance(other, pandas.Series):
+            # Copy into a Modin Series to simplify logic below
+            other = self.__constructor__(other)
+
+        if type(self) != type(other) or not self.index.equals(other.index):
+            return False
+
+        old_name_self = self.name
+        old_name_other = other.name
+        try:
+            self.name = "temp_name_for_equals_op"
+            other.name = "temp_name_for_equals_op"
+            # this function should return only scalar
+            res = self.__constructor__(
+                query_compiler=self._query_compiler.equals(other._query_compiler)
+            )
+        finally:
+            self.name = old_name_self
+            other.name = old_name_other
+        return res.all()
 
     def explode(self, ignore_index: bool = False):  # noqa: PR01, RT01, D200
         """


Test patch:
diff --git a/modin/pandas/test/dataframe/test_binary.py b/modin/pandas/test/dataframe/test_binary.py
--- a/modin/pandas/test/dataframe/test_binary.py
+++ b/modin/pandas/test/dataframe/test_binary.py
@@ -13,6 +13,7 @@
 
 import pytest
 import pandas
+import numpy as np
 import matplotlib
 import modin.pandas as pd
 
@@ -223,28 +224,81 @@ def test_multi_level_comparison(data, op):
         getattr(modin_df_multi_level, op)(modin_df_multi_level, axis=0, level=1)
 
 
-def test_equals():
-    frame_data = {"col1": [2.9, 3, 3, 3], "col2": [2, 3, 4, 1]}
-    modin_df1 = pd.DataFrame(frame_data)
-    modin_df2 = pd.DataFrame(frame_data)
-
-    assert modin_df1.equals(modin_df2)
-
-    df_equals(modin_df1, modin_df2)
-    df_equals(modin_df1, pd.DataFrame(modin_df1))
+@pytest.mark.parametrize(
+    "frame1_data,frame2_data,expected_pandas_equals",
+    [
+        pytest.param({}, {}, True, id="two_empty_dataframes"),
+        pytest.param([[1]], [[0]], False, id="single_unequal_values"),
+        pytest.param([[None]], [[None]], True, id="single_none_values"),
+        pytest.param([[np.NaN]], [[np.NaN]], True, id="single_nan_values"),
+        pytest.param({1: [10]}, {1.0: [10]}, True, id="different_column_types"),
+        pytest.param({1: [10]}, {2: [10]}, False, id="different_columns"),
+        pytest.param(
+            pandas.DataFrame({1: [10]}, index=[1]),
+            pandas.DataFrame({1: [10]}, index=[1.0]),
+            True,
+            id="different_index_types",
+        ),
+        pytest.param(
+            pandas.DataFrame({1: [10]}, index=[1]),
+            pandas.DataFrame({1: [10]}, index=[2]),
+            False,
+            id="different_indexes",
+        ),
+        pytest.param({1: [10]}, {1: [10.0]}, False, id="different_value_types"),
+        pytest.param(
+            [[1, 2], [3, 4]],
+            [[1, 2], [3, 4]],
+            True,
+            id="equal_two_by_two_dataframes",
+        ),
+        pytest.param(
+            [[1, 2], [3, 4]],
+            [[5, 2], [3, 4]],
+            False,
+            id="unequal_two_by_two_dataframes",
+        ),
+        pytest.param(
+            [[1, 1]],
+            [[1]],
+            False,
+            id="different_row_lengths",
+        ),
+        pytest.param(
+            [[1], [1]],
+            [[1]],
+            False,
+            id="different_column_lengths",
+        ),
+    ],
+)
+def test_equals(frame1_data, frame2_data, expected_pandas_equals):
+    modin_df1 = pd.DataFrame(frame1_data)
+    pandas_df1 = pandas.DataFrame(frame1_data)
+    modin_df2 = pd.DataFrame(frame2_data)
+    pandas_df2 = pandas.DataFrame(frame2_data)
+
+    pandas_equals = pandas_df1.equals(pandas_df2)
+    assert pandas_equals == expected_pandas_equals, (
+        "Test expected pandas to say the dataframes were"
+        + f"{'' if expected_pandas_equals else ' not'} equal, but they were"
+        + f"{' not' if expected_pandas_equals else ''} equal."
+    )
 
-    frame_data = {"col1": [2.9, 3, 3, 3], "col2": [2, 3, 5, 1]}
-    modin_df3 = pd.DataFrame(frame_data, index=list("abcd"))
+    assert modin_df1.equals(modin_df2) == pandas_equals
+    assert modin_df1.equals(pandas_df2) == pandas_equals
 
-    assert not modin_df1.equals(modin_df3)
 
-    with pytest.raises(AssertionError):
-        df_equals(modin_df3, modin_df1)
+def test_equals_several_partitions():
+    modin_series1 = pd.concat([pd.DataFrame([0, 1]), pd.DataFrame([None, 1])])
+    modin_series2 = pd.concat([pd.DataFrame([0, 1]), pd.DataFrame([1, None])])
+    assert not modin_series1.equals(modin_series2)
 
-    with pytest.raises(AssertionError):
-        df_equals(modin_df3, modin_df2)
 
-    assert modin_df1.equals(modin_df2._query_compiler.to_pandas())
+def test_equals_with_nans():
+    df1 = pd.DataFrame([0, 1, None], dtype="uint8[pyarrow]")
+    df2 = pd.DataFrame([None, None, None], dtype="uint8[pyarrow]")
+    assert not df1.equals(df2)
 
 
 @pytest.mark.parametrize("is_more_other_partitions", [True, False])
diff --git a/modin/pandas/test/test_series.py b/modin/pandas/test/test_series.py
--- a/modin/pandas/test/test_series.py
+++ b/modin/pandas/test/test_series.py
@@ -1897,26 +1897,74 @@ def test_eq(data):
     inter_df_math_helper(modin_series, pandas_series, "eq")
 
 
-def test_equals():
-    series_data = [2.9, 3, 3, 3]
-    modin_df1 = pd.Series(series_data)
-    modin_df2 = pd.Series(series_data)
+@pytest.mark.parametrize(
+    "series1_data,series2_data,expected_pandas_equals",
+    [
+        pytest.param([1], [0], False, id="single_unequal_values"),
+        pytest.param([None], [None], True, id="single_none_values"),
+        pytest.param(
+            pandas.Series(1, name="series1"),
+            pandas.Series(1, name="series2"),
+            True,
+            id="different_names",
+        ),
+        pytest.param(
+            pandas.Series([1], index=[1]),
+            pandas.Series([1], index=[1.0]),
+            True,
+            id="different_index_types",
+        ),
+        pytest.param(
+            pandas.Series([1], index=[1]),
+            pandas.Series([1], index=[2]),
+            False,
+            id="different_index_values",
+        ),
+        pytest.param([1], [1.0], False, id="different_value_types"),
+        pytest.param(
+            [1, 2],
+            [1, 2],
+            True,
+            id="equal_series_of_length_two",
+        ),
+        pytest.param(
+            [1, 2],
+            [1, 3],
+            False,
+            id="unequal_series_of_length_two",
+        ),
+        pytest.param(
+            [[1, 2]],
+            [[1]],
+            False,
+            id="different_lengths",
+        ),
+    ],
+)
+def test_equals(series1_data, series2_data, expected_pandas_equals):
+    modin_series1, pandas_df1 = create_test_series(series1_data)
+    modin_series2, pandas_df2 = create_test_series(series2_data)
 
-    assert modin_df1.equals(modin_df2)
-    assert modin_df1.equals(pd.Series(modin_df1))
-    df_equals(modin_df1, modin_df2)
-    df_equals(modin_df1, pd.Series(modin_df1))
+    pandas_equals = pandas_df1.equals(pandas_df2)
+    assert pandas_equals == expected_pandas_equals, (
+        "Test expected pandas to say the series were"
+        + f"{'' if expected_pandas_equals else ' not'} equal, but they were"
+        + f"{' not' if expected_pandas_equals else ''} equal."
+    )
+    assert modin_series1.equals(modin_series2) == pandas_equals
+    assert modin_series1.equals(pandas_df2) == pandas_equals
 
-    series_data = [2, 3, 5, 1]
-    modin_df3 = pd.Series(series_data, index=list("abcd"))
 
-    assert not modin_df1.equals(modin_df3)
+def test_equals_several_partitions():
+    modin_series1 = pd.concat([pd.Series([0, 1]), pd.Series([None, 1])])
+    modin_series2 = pd.concat([pd.Series([0, 1]), pd.Series([1, None])])
+    assert not modin_series1.equals(modin_series2)
 
-    with pytest.raises(AssertionError):
-        df_equals(modin_df3, modin_df1)
 
-    with pytest.raises(AssertionError):
-        df_equals(modin_df3, modin_df2)
+def test_equals_with_nans():
+    ser1 = pd.Series([0, 1, None], dtype="uint8[pyarrow]")
+    ser2 = pd.Series([None, None, None], dtype="uint8[pyarrow]")
+    assert not ser1.equals(ser2)
 
 
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)