Repository: pandas-dev/pandas
Pull request #57180 (2024-01-31 18:20:19)

Backport PR #57169 on branch 2.2.x (REGR: DataFrame.sort_index not producing stable sort)
Backport PR #57169: REGR: DataFrame.sort_index not producing stable sort


Patch:
diff --git a/doc/source/whatsnew/v2.2.1.rst b/doc/source/whatsnew/v2.2.1.rst
index 19b7e3493f..9002d9af2c 100644
--- a/doc/source/whatsnew/v2.2.1.rst
+++ b/doc/source/whatsnew/v2.2.1.rst
@@ -18,6 +18,7 @@ Fixed regressions
 - Fixed regression in :func:`merge_ordered` raising ``TypeError`` for ``fill_method="ffill"`` and ``how="left"`` (:issue:`57010`)
 - Fixed regression in :func:`wide_to_long` raising an ``AttributeError`` for string columns (:issue:`57066`)
 - Fixed regression in :meth:`DataFrame.loc` raising ``IndexError`` for non-unique, masked dtype indexes where result has more than 10,000 rows (:issue:`57027`)
+- Fixed regression in :meth:`DataFrame.sort_index` not producing a stable sort for a index with duplicates (:issue:`57151`)
 - Fixed regression in :meth:`DataFrame.to_dict` with ``orient='list'`` and datetime or timedelta types returning integers (:issue:`54824`)
 - Fixed regression in :meth:`DataFrameGroupBy.idxmin`, :meth:`DataFrameGroupBy.idxmax`, :meth:`SeriesGroupBy.idxmin`, :meth:`SeriesGroupBy.idxmax` ignoring the ``skipna`` argument (:issue:`57040`)
 - Fixed regression in :meth:`DataFrameGroupBy.idxmin`, :meth:`DataFrameGroupBy.idxmax`, :meth:`SeriesGroupBy.idxmin`, :meth:`SeriesGroupBy.idxmax` where values containing the minimum or maximum value for the dtype could produce incorrect results (:issue:`57040`)
@@ -29,6 +30,7 @@ Fixed regressions
 
 Bug fixes
 ~~~~~~~~~
+- Fixed bug in :func:`pandas.api.interchange.from_dataframe` which was raising for empty inputs (:issue:`56700`)
 - Fixed bug in :meth:`DataFrame.__getitem__` for empty :class:`DataFrame` with Copy-on-Write enabled (:issue:`57130`)
 
 .. ---------------------------------------------------------------------------
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
index 5e7f2e27f1..6f9078bf5a 100644
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -5914,17 +5914,14 @@ class Index(IndexOpsMixin, PandasObject):
         (Index([1000, 100, 10, 1], dtype='int64'), array([3, 1, 0, 2]))
         """
         if key is None and (
-            self.is_monotonic_increasing or self.is_monotonic_decreasing
+            (ascending and self.is_monotonic_increasing)
+            or (not ascending and self.is_monotonic_decreasing)
         ):
-            reverse = ascending != self.is_monotonic_increasing
-            sorted_index = self[::-1] if reverse else self.copy()
             if return_indexer:
                 indexer = np.arange(len(self), dtype=np.intp)
-                if reverse:
-                    indexer = indexer[::-1]
-                return sorted_index, indexer
+                return self.copy(), indexer
             else:
-                return sorted_index
+                return self.copy()
 
         # GH 35584. Sort missing values according to na_position kwarg
         # ignore na_position for MultiIndex
diff --git a/pandas/core/interchange/buffer.py b/pandas/core/interchange/buffer.py
index a54e4428bd..5c97fc17d7 100644
--- a/pandas/core/interchange/buffer.py
+++ b/pandas/core/interchange/buffer.py
@@ -23,7 +23,7 @@ class PandasBuffer(Buffer):
         """
         Handle only regular columns (= numpy arrays) for now.
         """
-        if not x.strides == (x.dtype.itemsize,):
+        if x.strides[0] and not x.strides == (x.dtype.itemsize,):
             # The protocol does not support strided buffers, so a copy is
             # necessary. If that's not allowed, we need to raise an exception.
             if allow_copy:


Test patch:
diff --git a/pandas/tests/frame/methods/test_sort_index.py b/pandas/tests/frame/methods/test_sort_index.py
index 49e292057e..830561a134 100644
--- a/pandas/tests/frame/methods/test_sort_index.py
+++ b/pandas/tests/frame/methods/test_sort_index.py
@@ -1002,3 +1002,27 @@ def test_axis_columns_ignore_index():
     result = df.sort_index(axis="columns", ignore_index=True)
     expected = DataFrame([[2, 1]])
     tm.assert_frame_equal(result, expected)
+
+
+def test_sort_index_stable_sort():
+    # GH 57151
+    df = DataFrame(
+        data=[
+            (Timestamp("2024-01-30 13:00:00"), 13.0),
+            (Timestamp("2024-01-30 13:00:00"), 13.1),
+            (Timestamp("2024-01-30 12:00:00"), 12.0),
+            (Timestamp("2024-01-30 12:00:00"), 12.1),
+        ],
+        columns=["dt", "value"],
+    ).set_index(["dt"])
+    result = df.sort_index(level="dt", kind="stable")
+    expected = DataFrame(
+        data=[
+            (Timestamp("2024-01-30 12:00:00"), 12.0),
+            (Timestamp("2024-01-30 12:00:00"), 12.1),
+            (Timestamp("2024-01-30 13:00:00"), 13.0),
+            (Timestamp("2024-01-30 13:00:00"), 13.1),
+        ],
+        columns=["dt", "value"],
+    ).set_index(["dt"])
+    tm.assert_frame_equal(result, expected)
diff --git a/pandas/tests/interchange/test_impl.py b/pandas/tests/interchange/test_impl.py
index c7b13f9fd7..c636523372 100644
--- a/pandas/tests/interchange/test_impl.py
+++ b/pandas/tests/interchange/test_impl.py
@@ -379,3 +379,12 @@ def test_large_string():
     result = pd.api.interchange.from_dataframe(df.__dataframe__())
     expected = pd.DataFrame({"a": ["x"]}, dtype="object")
     tm.assert_frame_equal(result, expected)
+
+
+def test_empty_dataframe():
+    # https://github.com/pandas-dev/pandas/issues/56700
+    df = pd.DataFrame({"a": []}, dtype="int8")
+    dfi = df.__dataframe__()
+    result = pd.api.interchange.from_dataframe(dfi, allow_copy=False)
+    expected = pd.DataFrame({"a": []}, dtype="int8")
+    tm.assert_frame_equal(result, expected)