Repository: modin-project/modin

Issue:
BUG: `sort_values` raises TypeError: '<' not supported between instances of 'float' and 'str'
### 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
# Test taken from pandas
import modin.pandas as pd
import numpy as np
from modin.pandas.test.utils import df_equals

def test_sort_values_key_string():
    df = pd.DataFrame(np.array([["hello", "goodbye"], ["hello", "Hello"]]))

    result = df.sort_values(1)
    expected = df[::-1]
    df_equals(result, expected)

    result = df.sort_values([0, 1], key=lambda col: col.str.lower())
    df_equals(result, df)

    result = df.sort_values(
        [0, 1], key=lambda col: col.str.lower(), ascending=False
    )
    expected = df.sort_values(1, key=lambda col: col.str.lower(), ascending=False)
    df_equals(result, expected)

if __name__ == "__main__":
    test_sort_values_key_string()
```


### Issue Description

Behavior different from pandas.

### Expected Behavior

Same behavior.

### Error Logs

<details>

```python-traceback

Traceback (most recent call last):
  File "...\test_sort_values_3.py", line 22, in <module>
    test_sort_values_key_string()
  File "...\test_sort_values_3.py", line 8, in test_sort_values_key_string
    result = df.sort_values(1)
  File "...\modin\logging\logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "...\modin\pandas\base.py", line 2920, in sort_values
    result = self._query_compiler.sort_rows_by_column_values(
  File "...\modin\logging\logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "...\modin\core\storage_formats\pandas\query_compiler.py", line 3455, in sort_rows_by_column_values
    new_modin_frame = self._modin_frame.sort_by(
  File "...\modin\logging\logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "...\modin\core\dataframe\pandas\dataframe\dataframe.py", line 126, in run_f_on_minimally_updated_metadata
    result = f(self, *args, **kwargs)
  File "...\modin\core\dataframe\pandas\dataframe\dataframe.py", line 2157, in sort_by
    ) = self._compute_axis_labels_and_lengths(axis.value, new_partitions)
  File "...\modin\logging\logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "...\modin\core\dataframe\pandas\dataframe\dataframe.py", line 459, in _compute_axis_labels_and_lengths
    new_index, internal_idx = self._partition_mgr_cls.get_indices(axis, partitions)
  File "...\modin\logging\logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "...\modin\core\dataframe\pandas\partitioning\partition_manager.py", line 923, in get_indices
    new_idx = cls.get_objects_from_partitions(new_idx)
  File "...\modin\logging\logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "...\modin\core\execution\dask\implementations\pandas_on_dask\partitioning\partition_manager.py", line 60, in get_objects_from_partitions
    return DaskWrapper.materialize(
  File "...\modin\core\execution\dask\common\engine_wrapper.py", line 108, in materialize      
    return client.gather(future)
  File "...\Miniconda3\envs\modin\lib\site-packages\distributed\client.py", line 2291, in gather
    return self.sync(
  File "...\Miniconda3\envs\modin\lib\site-packages\distributed\utils.py", line 339, in sync   
    return sync(
  File "...\Miniconda3\envs\modin\lib\site-packages\distributed\utils.py", line 406, in sync   
    raise exc.with_traceback(tb)
  File "...\Miniconda3\envs\modin\lib\site-packages\distributed\utils.py", line 379, in f      
    result = yield future
  File "...\Miniconda3\envs\modin\lib\site-packages\tornado\gen.py", line 762, in run
    value = future.result()
  File "...\Miniconda3\envs\modin\lib\site-packages\distributed\client.py", line 2154, in _gather
    raise exception.with_traceback(traceback)
  File "...\modin\core\dataframe\pandas\dataframe\utils.py", line 75, in split_fn
    return split_partitions_using_pivots_for_sort(
  File "...\modin\core\dataframe\pandas\dataframe\utils.py", line 267, in split_partitions_using_pivots_for_sort
    groupby_col = np.searchsorted(pivots, cols_to_digitize.squeeze(), side="right")
  File "<__array_function__ internals>", line 180, in searchsorted
  File "...\Miniconda3\envs\modin\lib\site-packages\numpy\core\fromnumeric.py", line 1387, in searchsorted
    return _wrapfunc(a, 'searchsorted', v, side=side, sorter=sorter)
  File "...\Miniconda3\envs\modin\lib\site-packages\numpy\core\fromnumeric.py", line 66, in _wrapfunc
    return _wrapit(obj, method, *args, **kwds)
  File "...\Miniconda3\envs\modin\lib\site-packages\numpy\core\fromnumeric.py", line 43, in _wrapit
    result = getattr(asarray(obj), method)(*args, **kwds)
TypeError: '<' not supported between instances of 'float' and 'str'

```

</details>


### Installed Versions

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



Discussion:

Resolving patch:
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
@@ -2048,6 +2048,10 @@ def sort_function(df):
                 f"Algebra sort only implemented row-wise. {axis.name} sort not implemented yet!"
             )
 
+        # If there's only one row partition can simply apply sorting row-wise without the need to reshuffle
+        if self._partitions.shape[0] == 1:
+            return self.apply_full_axis(axis=1, func=sort_function)
+
         # If this df is empty, we don't want to try and shuffle or sort.
         if len(self.axes[0]) == 0 or len(self.axes[1]) == 0:
             return self.copy()
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
@@ -200,10 +200,10 @@ def pick_pivots_from_samples_for_sort(
     num_quantiles = ideal_num_new_partitions
     quantiles = [i / num_quantiles for i in range(1, num_quantiles)]
     # If we only desire 1 partition, we need to ensure that we're not trying to find quantiles
-    # from an empty list of pivots. We use `np.nan` as a dummy value.
+    # from an empty list of pivots.
     if len(quantiles) > 0:
         return _find_quantiles(all_pivots, quantiles, method)
-    return np.array([np.nan])
+    return np.array([])
 
 
 def split_partitions_using_pivots_for_sort(
@@ -242,6 +242,9 @@ def split_partitions_using_pivots_for_sort(
     tuple[pandas.DataFrame]
         A tuple of the splits from this partition.
     """
+    if len(pivots) == 0:
+        # We can return the dataframe with zero changes if there were no pivots passed
+        return (df,)
     # If `ascending=False` and we are dealing with a numeric dtype, we can pass in a reversed list
     # of pivots, and `np.digitize` will work correctly. For object dtypes, we use `np.searchsorted`
     # which breaks when we reverse the pivots.
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
@@ -1588,22 +1588,28 @@ def shuffle_partitions(
             partition.force_materialization().list_of_block_partitions[0]
             for partition in cls.row_partitions(partitions)
         ]
-        # Gather together all of the sub-partitions
-        split_row_partitions = np.array(
-            [
-                partition.split(
-                    shuffle_functions.split_function, len(pivots) + 1, pivots
-                )
-                for partition in row_partitions
-            ]
-        ).T
-        # We need to convert every partition that came from the splits into a full-axis column partition.
-        new_partitions = [
-            [
-                cls._column_partitions_class(row_partition, full_axis=False).apply(
-                    final_shuffle_func
-                )
+        if len(pivots):
+            # Gather together all of the sub-partitions
+            split_row_partitions = np.array(
+                [
+                    partition.split(
+                        shuffle_functions.split_function, len(pivots) + 1, pivots
+                    )
+                    for partition in row_partitions
+                ]
+            ).T
+            # We need to convert every partition that came from the splits into a full-axis column partition.
+            new_partitions = [
+                [
+                    cls._column_partitions_class(row_partition, full_axis=False).apply(
+                        final_shuffle_func
+                    )
+                ]
+                for row_partition in split_row_partitions
             ]
-            for row_partition in split_row_partitions
-        ]
-        return np.array(new_partitions)
+            return np.array(new_partitions)
+        else:
+            # If there are not pivots we can simply apply the function row-wise
+            return np.array(
+                [[row_part.apply(final_shuffle_func)] for row_part in row_partitions]
+            )


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
@@ -591,6 +591,21 @@ def test_sort_values_descending_with_only_two_bins():
     )
 
 
+@pytest.mark.parametrize("ascending", [True, False])
+def test_sort_values_with_one_partition(ascending):
+    # Test case from https://github.com/modin-project/modin/issues/5859
+    modin_df, pandas_df = create_test_dfs(
+        np.array([["hello", "goodbye"], ["hello", "Hello"]])
+    )
+
+    if StorageFormat.get() == "Pandas":
+        assert modin_df._query_compiler._modin_frame._partitions.shape == (1, 1)
+
+    eval_general(
+        modin_df, pandas_df, lambda df: df.sort_values(by=1, ascending=ascending)
+    )
+
+
 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
@@ -835,3 +835,69 @@ def test_split_partitions_kernel(
             assert (
                 (bounds[idx + 1] <= part[col_name]) & (part[col_name] <= bounds[idx])
             ).all()
+
+
+@pytest.mark.parametrize("col_name", ["numeric_col", "non_numeric_col"])
+@pytest.mark.parametrize("ascending", [True, False])
+def test_split_partitions_with_empty_pivots(col_name, ascending):
+    """
+    This test verifies that the splitting function performs correctly when an empty pivots list is passed.
+    The expected behavior is to return a single split consisting of the exact copy of the input dataframe.
+    """
+    from modin.core.dataframe.pandas.dataframe.utils import (
+        split_partitions_using_pivots_for_sort,
+    )
+
+    df = pandas.DataFrame(
+        {
+            "numeric_col": range(9),
+            "non_numeric_col": list("abcdefghi"),
+        }
+    )
+
+    result = split_partitions_using_pivots_for_sort(
+        df, df, col_name, pivots=[], ascending=ascending
+    )
+    # We're expecting to recieve a single split here
+    assert isinstance(result, tuple)
+    assert len(result) == 1
+    assert result[0].equals(df)
+
+
+@pytest.mark.parametrize("ascending", [True, False])
+def test_shuffle_partitions_with_empty_pivots(ascending):
+    """
+    This test verifies that the `PartitionMgr.shuffle_partitions` method can handle empty pivots list.
+    """
+    modin_frame = pd.DataFrame(
+        np.array([["hello", "goodbye"], ["hello", "Hello"]])
+    )._query_compiler._modin_frame
+
+    assert modin_frame._partitions.shape == (1, 1)
+
+    from modin.core.dataframe.pandas.dataframe.utils import (
+        build_sort_functions,
+    )
+
+    column_name = modin_frame.columns[1]
+
+    shuffle_functions = build_sort_functions(
+        # These are the parameters we pass in the `.sort_by()` implementation
+        modin_frame,
+        column=column_name,
+        method="inverted_cdf",
+        ascending=ascending,
+        ideal_num_new_partitions=1,
+    )
+
+    new_partitions = modin_frame._partition_mgr_cls.shuffle_partitions(
+        modin_frame._partitions,
+        index=0,
+        shuffle_functions=shuffle_functions,
+        final_shuffle_func=lambda df: df.sort_values(column_name),
+    )
+    ref = modin_frame.to_pandas().sort_values(column_name)
+    res = new_partitions[0, 0].get()
+
+    assert new_partitions.shape == (1, 1)
+    assert ref.equals(res)