Repository: pandas-dev/pandas
Pull request: ENH: Add ignore_index to sort_index
- [x] closes #30114
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry


@jreback 
I just looked back at #30114 and found out that i overlooked to add `ignore_index` to `sort_index`, and this might be the reason this issue is still left open. I added this to `sort_index` to close this issue.

Problem statement:
API: add ignore_index keyword to .sort_* & .drop_duplicates
I see lots of code that goes like this

```
In [17]: df = pd.DataFrame({'A': [1, 2, 3]})                                                                                                                                        

In [18]: df                                                                                                                                                                         
Out[18]: 
   A
0  1
1  2
2  3

In [19]: df.sort_values('A', ascending=False)                                                                                                                                       
Out[19]: 
   A
2  3
1  2
0  1

In [20]: df.sort_values('A', ascending=False).reset_index(drop=True)                                                                                                                
Out[20]: 
   A
0  3
1  2
2  1
```

might be nice from an API / consistency perspective to add a ``ignore_index=False|True`` keyword to ``.sort_values()``, and ``.drop_duplicates()`` that does the reset in-line; this would give consistency similar to a ``pd.concat([......], ignore_index=True)`` operation which users are very familiar

Hints from issue discussion:
take

Commit message: Add space to make docstring pass

Patch to review:
diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst
index 77c4ed6160dbe..3f3a158262c9b 100755
--- a/doc/source/whatsnew/v1.0.0.rst
+++ b/doc/source/whatsnew/v1.0.0.rst
@@ -221,7 +221,7 @@ Other enhancements
 - DataFrame constructor preserve `ExtensionArray` dtype with `ExtensionArray` (:issue:`11363`)
 - :meth:`DataFrame.sort_values` and :meth:`Series.sort_values` have gained ``ignore_index`` keyword to be able to reset index after sorting (:issue:`30114`)
 - :meth:`DataFrame.to_markdown` and :meth:`Series.to_markdown` added (:issue:`11052`)
-
+- :meth:`DataFrame.sort_index` and :meth:`Series.sort_index` have gained ``ignore_index`` keyword to reset index (:issue:`30114`)
 - :meth:`DataFrame.drop_duplicates` has gained ``ignore_index`` keyword to reset index (:issue:`30114`)
 
 Build Changes
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index 1de0d3b58dc5f..879c510e5b2b4 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -1984,7 +1984,7 @@ def to_feather(self, path):
     @Substitution(klass="DataFrame")
     @Appender(_shared_docs["to_markdown"])
     def to_markdown(
-        self, buf: Optional[IO[str]] = None, mode: Optional[str] = None, **kwargs,
+        self, buf: Optional[IO[str]] = None, mode: Optional[str] = None, **kwargs
     ) -> Optional[str]:
         kwargs.setdefault("headers", "keys")
         kwargs.setdefault("tablefmt", "pipe")
@@ -4827,6 +4827,7 @@ def sort_index(
         kind="quicksort",
         na_position="last",
         sort_remaining=True,
+        ignore_index=False,
     ):
 
         # TODO: this can be combined with Series.sort_index impl as
@@ -4877,6 +4878,9 @@ def sort_index(
         # reconstruct axis if needed
         new_data.axes[baxis] = new_data.axes[baxis]._sort_levels_monotonic()
 
+        if ignore_index:
+            new_data.axes[1] = ibase.default_index(len(indexer))
+
         if inplace:
             return self._update_inplace(new_data)
         else:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index 2b108d3997235..fab00065c3b46 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -4178,6 +4178,7 @@ def sort_index(
         kind: str = "quicksort",
         na_position: str = "last",
         sort_remaining: bool_t = True,
+        ignore_index: bool_t = False,
     ):
         """
         Sort object by labels (along an axis).
@@ -4204,6 +4205,10 @@ def sort_index(
         sort_remaining : bool, default True
             If True and sorting by level and index is multilevel, sort by other
             levels too (in order) after sorting by specified level.
+        ignore_index : bool, default False
+            If True, the resulting axis will be labeled 0, 1, …, n - 1.
+
+            .. versionadded:: 1.0.0
 
         Returns
         -------
diff --git a/pandas/core/series.py b/pandas/core/series.py
index 36e26e088935c..f9acfe87bad36 100644
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -1433,7 +1433,7 @@ def to_string(
     @Substitution(klass="Series")
     @Appender(_shared_docs["to_markdown"])
     def to_markdown(
-        self, buf: Optional[IO[str]] = None, mode: Optional[str] = None, **kwargs,
+        self, buf: Optional[IO[str]] = None, mode: Optional[str] = None, **kwargs
     ) -> Optional[str]:
         return self.to_frame().to_markdown(buf, mode, **kwargs)
 
@@ -2880,6 +2880,7 @@ def sort_index(
         kind="quicksort",
         na_position="last",
         sort_remaining=True,
+        ignore_index=False,
     ):
         """
         Sort Series by index labels.
@@ -2908,6 +2909,10 @@ def sort_index(
         sort_remaining : bool, default True
             If True and sorting by level and index is multilevel, sort by other
             levels too (in order) after sorting by specified level.
+        ignore_index : bool, default False
+            If True, the resulting axis will be labeled 0, 1, …, n - 1.
+
+            .. versionadded:: 1.0.0
 
         Returns
         -------
@@ -3035,6 +3040,9 @@ def sort_index(
         new_values = self._values.take(indexer)
         result = self._constructor(new_values, index=new_index)
 
+        if ignore_index:
+            result.index = ibase.default_index(len(result))
+
         if inplace:
             self._update_inplace(result)
         else:
@@ -4395,9 +4403,7 @@ def to_period(self, freq=None, copy=True):
     hist = pandas.plotting.hist_series
 
 
-Series._setup_axes(
-    ["index"], docs={"index": "The index (axis labels) of the Series."},
-)
+Series._setup_axes(["index"], docs={"index": "The index (axis labels) of the Series."})
 Series._add_numeric_operations()
 Series._add_series_or_dataframe_operations()
 
diff --git a/pandas/tests/frame/methods/test_sort_index.py b/pandas/tests/frame/methods/test_sort_index.py
index 4f311bbaa8eb9..2c1a7540f0a0c 100644
--- a/pandas/tests/frame/methods/test_sort_index.py
+++ b/pandas/tests/frame/methods/test_sort_index.py
@@ -229,3 +229,84 @@ def test_sort_index_intervalindex(self):
         )
         result = result.columns.levels[1].categories
         tm.assert_index_equal(result, expected)
+
+    @pytest.mark.parametrize(
+        "original_dict, sorted_dict, ascending, ignore_index, output_index",
+        [
+            ({"A": [1, 2, 3]}, {"A": [3, 2, 1]}, False, True, [0, 1, 2]),
+            ({"A": [1, 2, 3]}, {"A": [1, 2, 3]}, True, True, [0, 1, 2]),
+            ({"A": [1, 2, 3]}, {"A": [3, 2, 1]}, False, False, [2, 1, 0]),
+            ({"A": [1, 2, 3]}, {"A": [1, 2, 3]}, True, False, [0, 1, 2]),
+        ],
+    )
+    def test_sort_index_ignore_index(
+        self, original_dict, sorted_dict, ascending, ignore_index, output_index
+    ):
+        # GH 30114
+        df = DataFrame(original_dict)
+        expected_df = DataFrame(sorted_dict, index=output_index)
+
+        sorted_df = df.sort_index(ascending=ascending, ignore_index=ignore_index)
+        tm.assert_frame_equal(sorted_df, expected_df)
+        tm.assert_frame_equal(df, DataFrame(original_dict))
+
+        # Test when inplace is True
+        copied_df = df.copy()
+        copied_df.sort_index(
+            ascending=ascending, ignore_index=ignore_index, inplace=True
+        )
+        tm.assert_frame_equal(copied_df, expected_df)
+        tm.assert_frame_equal(df, DataFrame(original_dict))
+
+    @pytest.mark.parametrize(
+        "original_dict, sorted_dict, ascending, ignore_index, output_index",
+        [
+            (
+                {"M1": [1, 2], "M2": [3, 4]},
+                {"M1": [1, 2], "M2": [3, 4]},
+                True,
+                True,
+                [0, 1],
+            ),
+            (
+                {"M1": [1, 2], "M2": [3, 4]},
+                {"M1": [2, 1], "M2": [4, 3]},
+                False,
+                True,
+                [0, 1],
+            ),
+            (
+                {"M1": [1, 2], "M2": [3, 4]},
+                {"M1": [1, 2], "M2": [3, 4]},
+                True,
+                False,
+                MultiIndex.from_tuples([[2, 1], [3, 4]], names=list("AB")),
+            ),
+            (
+                {"M1": [1, 2], "M2": [3, 4]},
+                {"M1": [2, 1], "M2": [4, 3]},
+                False,
+                False,
+                MultiIndex.from_tuples([[3, 4], [2, 1]], names=list("AB")),
+            ),
+        ],
+    )
+    def test_sort_index_ignore_index_multi_index(
+        self, original_dict, sorted_dict, ascending, ignore_index, output_index
+    ):
+        # GH 30114, this is to test ignore_index on MulitIndex of index
+        mi = MultiIndex.from_tuples([[2, 1], [3, 4]], names=list("AB"))
+        df = DataFrame(original_dict, index=mi)
+        expected_df = DataFrame(sorted_dict, index=output_index)
+
+        sorted_df = df.sort_index(ascending=ascending, ignore_index=ignore_index)
+        tm.assert_frame_equal(sorted_df, expected_df)
+        tm.assert_frame_equal(df, DataFrame(original_dict, index=mi))
+
+        # Test when inplace is True
+        copied_df = df.copy()
+        copied_df.sort_index(
+            ascending=ascending, ignore_index=ignore_index, inplace=True
+        )
+        tm.assert_frame_equal(copied_df, expected_df)
+        tm.assert_frame_equal(df, DataFrame(original_dict, index=mi))
diff --git a/pandas/tests/series/methods/test_sort_index.py b/pandas/tests/series/methods/test_sort_index.py
index ab15b8c814029..57096de283915 100644
--- a/pandas/tests/series/methods/test_sort_index.py
+++ b/pandas/tests/series/methods/test_sort_index.py
@@ -135,3 +135,35 @@ def test_sort_index_intervals(self):
             [3, 2, 1, np.nan], IntervalIndex.from_arrays([3, 2, 1, 0], [4, 3, 2, 1])
         )
         tm.assert_series_equal(result, expected)
+
+    @pytest.mark.parametrize(
+        "original_list, sorted_list, ascending, ignore_index, output_index",
+        [
+            ([2, 3, 6, 1], [2, 3, 6, 1], True, True, [0, 1, 2, 3]),
+            ([2, 3, 6, 1], [2, 3, 6, 1], True, False, [0, 1, 2, 3]),
+            ([2, 3, 6, 1], [1, 6, 3, 2], False, True, [0, 1, 2, 3]),
+            ([2, 3, 6, 1], [1, 6, 3, 2], False, False, [3, 2, 1, 0]),
+        ],
+    )
+    def test_sort_index_ignore_index(
+        self, original_list, sorted_list, ascending, ignore_index, output_index
+    ):
+        # GH 30114
+        # GH 30114
+        sr = Series(original_list)
+        expected = Series(sorted_list, index=output_index)
+
+        # Test when inplace is False
+        sorted_sr = sr.sort_index(ascending=ascending, ignore_index=ignore_index)
+        tm.assert_series_equal(sorted_sr, expected)
+
+        tm.assert_series_equal(sr, Series(original_list))
+
+        # Test when inplace is True
+        copied_sr = sr.copy()
+        copied_sr.sort_index(
+            ascending=ascending, ignore_index=ignore_index, inplace=True
+        )
+        tm.assert_series_equal(copied_sr, expected)
+
+        tm.assert_series_equal(sr, Series(original_list))


Code review comments:
diff_hunk: @@ -135,3 +135,35 @@ def test_sort_index_intervals(self):
             [3, 2, 1, np.nan], IntervalIndex.from_arrays([3, 2, 1, 0], [4, 3, 2, 1])
         )
         tm.assert_series_equal(result, expected)
+
+    @pytest.mark.parametrize(
+        "original_list, sorted_list, ascending, ignore_index, output_index",
+        [
+            ([2, 3, 6, 1], [2, 3, 6, 1], True, True, [0, 1, 2, 3]),
+            ([2, 3, 6, 1], [2, 3, 6, 1], True, False, [0, 1, 2, 3]),
+            ([2, 3, 6, 1], [1, 6, 3, 2], False, True, [0, 1, 2, 3]),
+            ([2, 3, 6, 1], [1, 6, 3, 2], False, False, [3, 2, 1, 0]),
+        ],
+    )
+    def test_sort_index_ignore_index(
+        self, original_list, sorted_list, ascending, ignore_index, output_index
+    ):
+        # GH 30114
+        # GH 30114
path: pandas/tests/series/methods/test_sort_index.py
text: @user1:
dup

@author:
oh, sorry, removed!

diff_hunk: @@ -135,3 +135,35 @@ def test_sort_index_intervals(self):
             [3, 2, 1, np.nan], IntervalIndex.from_arrays([3, 2, 1, 0], [4, 3, 2, 1])
         )
         tm.assert_series_equal(result, expected)
+
+    @pytest.mark.parametrize(
+        "original_list, sorted_list, ascending, ignore_index, output_index",
+        [
+            ([2, 3, 6, 1], [2, 3, 6, 1], True, True, [0, 1, 2, 3]),
+            ([2, 3, 6, 1], [2, 3, 6, 1], True, False, [0, 1, 2, 3]),
+            ([2, 3, 6, 1], [1, 6, 3, 2], False, True, [0, 1, 2, 3]),
+            ([2, 3, 6, 1], [1, 6, 3, 2], False, False, [3, 2, 1, 0]),
+        ],
+    )
+    def test_sort_index_ignore_index(
+        self, original_list, sorted_list, ascending, ignore_index, output_index
+    ):
+        # GH 30114
+        # GH 30114
+        sr = Series(original_list)
+        expected = Series(sorted_list, index=output_index)
+
+        # Test when inplace is False
path: pandas/tests/series/methods/test_sort_index.py
text: @user1:
can inplace be parameterised easily

@author:
emm, i think in this case, if using parameterization, might not look very clean and less readable

@user2:
I think this should be parametrized - what do you think makes it less readable?

@user3:
ok with trying parameterize as a followup, as for .sort_values and .duplicated these tests look very much like this.

@author:
emm, okay! i thought with `inplace` being `Ture`, we do not need to do something like `df = df.sort_index(inplace=True)`, but `False` needs to be like this, and testing object will be different, one is testing if original changes, another is to test if assigned new df is changed. 

Sorry to bring up discussion here, it definitely can be parametrized, was just my preference on such case. I will parametrize all three methods in the follow-up PR for sure!

diff_hunk: @@ -135,3 +135,35 @@ def test_sort_index_intervals(self):
             [3, 2, 1, np.nan], IntervalIndex.from_arrays([3, 2, 1, 0], [4, 3, 2, 1])
         )
         tm.assert_series_equal(result, expected)
+
+    @pytest.mark.parametrize(
+        "original_list, sorted_list, ascending, ignore_index, output_index",
+        [
+            ([2, 3, 6, 1], [2, 3, 6, 1], True, True, [0, 1, 2, 3]),
+            ([2, 3, 6, 1], [2, 3, 6, 1], True, False, [0, 1, 2, 3]),
+            ([2, 3, 6, 1], [1, 6, 3, 2], False, True, [0, 1, 2, 3]),
+            ([2, 3, 6, 1], [1, 6, 3, 2], False, False, [3, 2, 1, 0]),
+        ],
+    )
+    def test_sort_index_ignore_index(
+        self, original_list, sorted_list, ascending, ignore_index, output_index
+    ):
+        # GH 30114
+        # GH 30114
+        sr = Series(original_list)
path: pandas/tests/series/methods/test_sort_index.py
text: @user1:
not seen sr used, s or ser?

@author:
changed to `ser`

diff_hunk: @@ -2880,6 +2880,7 @@ def sort_index(
         kind="quicksort",
         na_position="last",
         sort_remaining=True,
+        ignore_index=False,
path: pandas/core/series.py
text: @user1:
can you add type annotations when adding kwargs

@author:
added!

diff_hunk: @@ -4827,6 +4827,7 @@ def sort_index(
         kind="quicksort",
         na_position="last",
         sort_remaining=True,
+        ignore_index=False,
path: pandas/core/frame.py
text: @user1:
Can we type any new args?

@author:
yes, added!