Repository: modin-project/modin

Issue:
BUG: `.sort_values(ascending=False)` gives incorrect result when using reshuffling implementation
### 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 modin.config as cfg

cfg.NPartitions.put(4)

import modin.pandas as pd

part1 = pd.DataFrame({"a": [1, 2, 3, 4]})
part2 = pd.DataFrame({"a": [5, 6, 7, 8]})

df = pd.concat([part1, part2])

assert df._query_compiler._modin_frame._partitions.shape == (2, 1)

print(df.sort_values("a", ascending=False))
# Output:
#    a
# 3  4
# 2  3
# 1  2
# 0  1
# 3  8
# 2  7
# 1  6
# 0  5
```


### Issue Description

The result is incorrect because we're not reversing partitions order when `ascending=False`.

### Expected Behavior

The expected result would be properly sorted data:
```python
   a
3  8
2  7
1  6
0  5
3  4
2  3
1  2
0  1
```

### 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:
Okay, the problem seems to only occur when the number of pivot values is equal to one. In this case, we're skipping this branch that is intended to set the descending order for partitions:
https://github.com/modin-project/modin/blob/e2fd6ddce0943d42d802180d5359733ad2272ac4/modin/core/dataframe/pandas/dataframe/utils.py#L260-L265

@RehanSD do you aware of why there's a `len(pivots) != 1` check? I removed it and everything started to work just fine.

Resolving patch:
diff --git a/modin/core/dataframe/pandas/dataframe/utils.py b/modin/core/dataframe/pandas/dataframe/utils.py
--- a/modin/core/dataframe/pandas/dataframe/utils.py
+++ b/modin/core/dataframe/pandas/dataframe/utils.py
@@ -261,7 +261,7 @@ def split_partitions_using_pivots_for_sort(
         # When we only have one unique value in our pivots, `np.digitize` assumes that the pivots
         # are sorted in ascending order, and gives us results based off of that assumption - so if
         # we actually want to sort in descending order, we need to swap the new indices.
-        if not ascending and len(np.unique(pivots)) == 1 and len(pivots) != 1:
+        if not ascending and len(np.unique(pivots)) == 1:
             groupby_col = len(pivots) - groupby_col
     else:
         groupby_col = np.searchsorted(pivots, cols_to_digitize.squeeze(), side="right")


Test patch:
diff --git a/modin/pandas/test/dataframe/test_join_sort.py b/modin/pandas/test/dataframe/test_join_sort.py
--- a/modin/pandas/test/dataframe/test_join_sort.py
+++ b/modin/pandas/test/dataframe/test_join_sort.py
@@ -37,7 +37,7 @@
     extra_test_parameters,
     default_to_pandas_ignore_string,
 )
-from modin.config import NPartitions, Engine
+from modin.config import NPartitions, Engine, StorageFormat
 from modin.test.test_utils import warns_that_defaulting_to_pandas
 
 NPartitions.put(4)
@@ -572,6 +572,22 @@ def test_sort_values(
     )
 
 
+def test_sort_values_descending_with_only_two_bins():
+    # test case from https://github.com/modin-project/modin/issues/5781
+    part1 = pd.DataFrame({"a": [1, 2, 3, 4]})
+    part2 = pd.DataFrame({"a": [5, 6, 7, 8]})
+
+    modin_df = pd.concat([part1, part2])
+    pandas_df = modin_df._to_pandas()
+
+    if StorageFormat.get() == "Pandas":
+        assert modin_df._query_compiler._modin_frame._partitions.shape == (2, 1)
+
+    eval_general(
+        modin_df, pandas_df, lambda df: df.sort_values(by="a", ascending=False)
+    )
+
+
 def test_sort_overpartitioned_df():
     # First we test when the final df will have only 1 row and column partition.
     data = [[4, 5, 6], [1, 2, 3]]
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
@@ -757,3 +757,81 @@ def test_repartitioning(set_num_partitions):
     assert res.column_widths == [4]
     df_equals(res._partitions[0, 0].to_pandas(), pandas_df)
     df_equals(res.to_pandas(), pandas_df)
+
+
+@pytest.mark.parametrize("col_name", ["numeric_col", "non_numeric_col"])
+@pytest.mark.parametrize("ascending", [True, False])
+@pytest.mark.parametrize("num_pivots", [3, 2, 1])
+@pytest.mark.parametrize("all_pivots_are_unique", [True, False])
+def test_split_partitions_kernel(
+    col_name, ascending, num_pivots, all_pivots_are_unique
+):
+    """
+    This test verifies proper work of the `split_partitions_using_pivots_for_sort` function
+    used in partitions reshuffling.
+
+    The function being tested splits the passed dataframe into parts according
+    to the 'pivots' indicating boundary values for the parts.
+
+    Parameters
+    ----------
+    col_name : {"numeric_col", "non_numeric_col"}
+        The tested function takes a key column name to which the pivot values belong.
+        The function may behave differently depending on the type of that column.
+    ascending : {True, False}
+        The split parts are returned either in ascending or descending order.
+        This parameter helps us to test both of the cases.
+    num_pivots : {3, 2, 1}
+        The function's behavior may depend on the number of boundary values being passed.
+    all_pivots_are_unique : {True, False}
+        Duplicate pivot values cause empty partitions to be produced. This parameter helps
+        to verify that the function still behaves correctly in such cases.
+    """
+    from modin.core.dataframe.pandas.dataframe.utils import (
+        split_partitions_using_pivots_for_sort,
+    )
+
+    random_state = np.random.RandomState(42)
+
+    df = pandas.DataFrame(
+        {
+            "numeric_col": range(9),
+            "non_numeric_col": list("abcdefghi"),
+        }
+    )
+    min_val, max_val = df[col_name].iloc[0], df[col_name].iloc[-1]
+
+    # Selecting random boundary values for the key column
+    pivots = random_state.choice(df[col_name], num_pivots, replace=False)
+    if not all_pivots_are_unique:
+        # Making the 'pivots' contain only duplicate values
+        pivots = np.repeat(pivots[0], num_pivots)
+    # The tested function assumes that we pass pivots in the ascending order
+    pivots = np.sort(pivots)
+
+    # Randomly reordering rows in the dataframe
+    df = df.reindex(random_state.permutation(df.index))
+    bins = split_partitions_using_pivots_for_sort(df, df, col_name, pivots, ascending)
+
+    # Building reference bounds to make the result verification simpler
+    bounds = np.concatenate([[min_val], pivots, [max_val]])
+    if not ascending:
+        # If the order is descending we want bounds to be in the descending order as well:
+        # Ex: bounds = [0, 2, 5, 10] for ascending and [10, 5, 2, 0] for descending.
+        bounds = bounds[::-1]
+
+    for idx, part in enumerate(bins):
+        if ascending:
+            # Check that each part is in the range of 'bound[i] <= part <= bound[i + 1]'
+            # Example, if the `pivots` were [2, 5] and the min/max values for the colum are min=0, max=10
+            # Then each part satisfies: 0 <= part[0] <= 2; 2 <= part[1] <= 5; 5 <= part[2] <= 10
+            assert (
+                (bounds[idx] <= part[col_name]) & (part[col_name] <= bounds[idx + 1])
+            ).all()
+        else:
+            # Check that each part is in the range of 'bound[i + 1] <= part <= bound[i]'
+            # Example, if the `pivots` were [2, 5] and the min/max values for the colum are min=0, max=10
+            # Then each part satisfies: 5 <= part[0] <= 10; 2 <= part[1] <= 5; 0 <= part[2] <= 2
+            assert (
+                (bounds[idx + 1] <= part[col_name]) & (part[col_name] <= bounds[idx])
+            ).all()