Skip to content

Commit bc37ea2

Browse files
WillAydjreback
authored andcommitted
Fix Inconsistent MultiIndex Sorting (#21043)
1 parent af2b609 commit bc37ea2

File tree

5 files changed

+53
-17
lines changed

5 files changed

+53
-17
lines changed

pandas/core/frame.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -4454,17 +4454,17 @@ def sort_index(self, axis=0, level=None, ascending=True, inplace=False,
44544454
axis = self._get_axis_number(axis)
44554455
labels = self._get_axis(axis)
44564456

4457-
if level:
4457+
# make sure that the axis is lexsorted to start
4458+
# if not we need to reconstruct to get the correct indexer
4459+
labels = labels._sort_levels_monotonic()
4460+
if level is not None:
44584461

44594462
new_axis, indexer = labels.sortlevel(level, ascending=ascending,
44604463
sort_remaining=sort_remaining)
44614464

44624465
elif isinstance(labels, MultiIndex):
44634466
from pandas.core.sorting import lexsort_indexer
44644467

4465-
# make sure that the axis is lexsorted to start
4466-
# if not we need to reconstruct to get the correct indexer
4467-
labels = labels._sort_levels_monotonic()
44684468
indexer = lexsort_indexer(labels._get_labels_for_sorting(),
44694469
orders=ascending,
44704470
na_position=na_position)

pandas/core/series.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2617,7 +2617,7 @@ def sort_index(self, axis=0, level=None, ascending=True, inplace=False,
26172617
axis = self._get_axis_number(axis)
26182618
index = self.index
26192619

2620-
if level:
2620+
if level is not None:
26212621
new_index, indexer = index.sortlevel(level, ascending=ascending,
26222622
sort_remaining=sort_remaining)
26232623
elif isinstance(index, MultiIndex):

pandas/tests/frame/test_reshape.py

+17
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,23 @@ def test_stack_preserve_categorical_dtype(self):
861861

862862
tm.assert_series_equal(result, expected)
863863

864+
@pytest.mark.parametrize("level", [0, 'baz'])
865+
def test_unstack_swaplevel_sortlevel(self, level):
866+
# GH 20994
867+
mi = pd.MultiIndex.from_product([[0], ['d', 'c']],
868+
names=['bar', 'baz'])
869+
df = pd.DataFrame([[0, 2], [1, 3]], index=mi, columns=['B', 'A'])
870+
df.columns.name = 'foo'
871+
872+
expected = pd.DataFrame([
873+
[3, 1, 2, 0]], columns=pd.MultiIndex.from_tuples([
874+
('c', 'A'), ('c', 'B'), ('d', 'A'), ('d', 'B')], names=[
875+
'baz', 'foo']))
876+
expected.index.name = 'bar'
877+
878+
result = df.unstack().swaplevel(axis=1).sort_index(axis=1, level=level)
879+
tm.assert_frame_equal(result, expected)
880+
864881

865882
def test_unstack_fill_frame_object():
866883
# GH12815 Test unstacking with object.

pandas/tests/frame/test_sorting.py

+26-8
Original file line numberDiff line numberDiff line change
@@ -550,18 +550,36 @@ def test_sort_index(self):
550550
expected = frame.iloc[:, ::-1]
551551
assert_frame_equal(result, expected)
552552

553-
def test_sort_index_multiindex(self):
553+
@pytest.mark.parametrize("level", ['A', 0]) # GH 21052
554+
def test_sort_index_multiindex(self, level):
554555
# GH13496
555556

556557
# sort rows by specified level of multi-index
557-
mi = MultiIndex.from_tuples([[2, 1, 3], [1, 1, 1]], names=list('ABC'))
558-
df = DataFrame([[1, 2], [3, 4]], mi)
558+
mi = MultiIndex.from_tuples([
559+
[2, 1, 3], [2, 1, 2], [1, 1, 1]], names=list('ABC'))
560+
df = DataFrame([[1, 2], [3, 4], [5, 6]], index=mi)
561+
562+
expected_mi = MultiIndex.from_tuples([
563+
[1, 1, 1],
564+
[2, 1, 2],
565+
[2, 1, 3]], names=list('ABC'))
566+
expected = pd.DataFrame([
567+
[5, 6],
568+
[3, 4],
569+
[1, 2]], index=expected_mi)
570+
result = df.sort_index(level=level)
571+
assert_frame_equal(result, expected)
559572

560-
# MI sort, but no level: sort_level has no effect
561-
mi = MultiIndex.from_tuples([[1, 1, 3], [1, 1, 1]], names=list('ABC'))
562-
df = DataFrame([[1, 2], [3, 4]], mi)
563-
result = df.sort_index(sort_remaining=False)
564-
expected = df.sort_index()
573+
# sort_remaining=False
574+
expected_mi = MultiIndex.from_tuples([
575+
[1, 1, 1],
576+
[2, 1, 3],
577+
[2, 1, 2]], names=list('ABC'))
578+
expected = pd.DataFrame([
579+
[5, 6],
580+
[1, 2],
581+
[3, 4]], index=expected_mi)
582+
result = df.sort_index(level=level, sort_remaining=False)
565583
assert_frame_equal(result, expected)
566584

567585
def test_sort_index_intervalindex(self):

pandas/tests/series/test_sorting.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -141,19 +141,20 @@ def test_sort_index_inplace(self):
141141
assert result is None
142142
tm.assert_series_equal(random_order, self.ts)
143143

144-
def test_sort_index_multiindex(self):
144+
@pytest.mark.parametrize("level", ['A', 0]) # GH 21052
145+
def test_sort_index_multiindex(self, level):
145146

146147
mi = MultiIndex.from_tuples([[1, 1, 3], [1, 1, 1]], names=list('ABC'))
147148
s = Series([1, 2], mi)
148149
backwards = s.iloc[[1, 0]]
149150

150151
# implicit sort_remaining=True
151-
res = s.sort_index(level='A')
152+
res = s.sort_index(level=level)
152153
assert_series_equal(backwards, res)
153154

154155
# GH13496
155-
# rows share same level='A': sort has no effect without remaining lvls
156-
res = s.sort_index(level='A', sort_remaining=False)
156+
# sort has no effect without remaining lvls
157+
res = s.sort_index(level=level, sort_remaining=False)
157158
assert_series_equal(s, res)
158159

159160
def test_sort_index_kind(self):

0 commit comments

Comments
 (0)