Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion python/cudf/cudf/core/indexed_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -2921,7 +2921,23 @@ def sort_index(
if ignore_index:
out = out.reset_index(drop=True)
else:
labels = sorted(self._column_names, reverse=not ascending)
# The column labels are host-side pandas metadata, so have
# pandas compute the sorted column order: a Series holding the
# original positions, indexed by the column labels, sorted by
# its index yields the positional indexer while inheriting
# pandas' exact level resolution and validation, per-level
# ascending, sort_remaining, and na_position semantics.
pd_columns = self._data.to_pandas_index
indexer = pd.Series(
range(len(pd_columns)), index=pd_columns
).sort_index(
level=level,
ascending=ascending,
sort_remaining=sort_remaining,
na_position=na_position,
kind="stable",
)
labels = [self._column_names[i] for i in indexer]
result_columns = (self._data[label] for label in labels)
if ignore_index:
ca = ColumnAccessor(
Expand Down
1 change: 0 additions & 1 deletion python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1713,7 +1713,6 @@ def pytest_unconfigure(config):
"tests/frame/test_stack_unstack.py::TestStackUnstackMultiLevel::test_stack[True]": "TODO: Add a reason for failure",
"tests/frame/test_stack_unstack.py::TestStackUnstackMultiLevel::test_stack_level_name[False]": "TODO: Add a reason for failure",
"tests/frame/test_stack_unstack.py::TestStackUnstackMultiLevel::test_stack_level_name[True]": "TODO: Add a reason for failure",
"tests/frame/test_stack_unstack.py::TestStackUnstackMultiLevel::test_stack_mixed_dtype[True]": "TODO: Add a reason for failure",
"tests/frame/test_stack_unstack.py::TestStackUnstackMultiLevel::test_stack_multiple_bug[False]": "TODO: Add a reason for failure",
"tests/frame/test_stack_unstack.py::TestStackUnstackMultiLevel::test_stack_multiple_bug[True]": "TODO: Add a reason for failure",
"tests/frame/test_stack_unstack.py::TestStackUnstackMultiLevel::test_stack_multiple_out_of_bounds[False]": "TODO: Add a reason for failure",
Expand Down
124 changes: 123 additions & 1 deletion python/cudf/cudf/tests/dataframe/methods/test_sort_index.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
import itertools

Expand Down Expand Up @@ -163,3 +163,125 @@ def test_df_cat_sort_index():
expect = df.to_pandas().set_index("a").sort_index()

assert_eq(got, expect)


@pytest.mark.parametrize("level", [0, 1, "c1", ["c2"], ["c2", "c1"], [1, 0]])
@pytest.mark.parametrize("sort_remaining", [True, False])
@pytest.mark.parametrize("ascending", [True, False])
@pytest.mark.parametrize("na_position", ["first", "last"])
def test_sort_index_axis_1_multiindex_level(
level, sort_remaining, ascending, na_position
):
# axis=1 previously ignored level= and sort_remaining= silently
pdf = pd.DataFrame(
[[1, 2, 3, 4]],
columns=pd.MultiIndex.from_tuples(
[("b", 2), ("a", 1), (np.nan, 3), ("a", 4)], names=["c1", "c2"]
),
)
gdf = cudf.DataFrame(pdf)

expect = pdf.sort_index(
axis=1,
level=level,
sort_remaining=sort_remaining,
ascending=ascending,
na_position=na_position,
)
got = gdf.sort_index(
axis=1,
level=level,
sort_remaining=sort_remaining,
ascending=ascending,
na_position=na_position,
)
assert_eq(expect, got)


def test_sort_index_axis_1_level_out_of_bounds():
pdf = pd.DataFrame(
[[1, 2]],
columns=pd.MultiIndex.from_tuples([("a", 1), ("b", 2)]),
)
gdf = cudf.DataFrame(pdf)
with pytest.raises(IndexError):
gdf.sort_index(axis=1, level=5)


@pytest.mark.parametrize(
"level, ascending",
[
(["c1", "c2"], [True, False]),
(["c2", "c1"], [False, True]),
([0, 1], [False, False]),
],
)
def test_sort_index_axis_1_per_level_ascending(level, ascending):
pdf = pd.DataFrame(
[[1, 2, 3, 4]],
columns=pd.MultiIndex.from_tuples(
[("b", 2), ("a", 1), ("b", 3), ("a", 4)], names=["c1", "c2"]
),
)
gdf = cudf.DataFrame(pdf)

expect = pdf.sort_index(axis=1, level=level, ascending=ascending)
got = gdf.sort_index(axis=1, level=level, ascending=ascending)
assert_eq(expect, got)


def test_sort_index_axis_1_ascending_length_mismatch_raises():
pdf = pd.DataFrame(
[[1, 2]],
columns=pd.MultiIndex.from_tuples([("a", 1), ("b", 2)]),
)
gdf = cudf.DataFrame(pdf)
with pytest.raises(ValueError):
pdf.sort_index(axis=1, level=[0, 1], ascending=[True])
with pytest.raises(ValueError):
gdf.sort_index(axis=1, level=[0, 1], ascending=[True])


def test_sort_index_axis_1_unknown_level_name_raises():
pdf = pd.DataFrame(
[[1, 2]],
columns=pd.MultiIndex.from_tuples(
[("a", 1), ("b", 2)], names=["c1", "c2"]
),
)
gdf = cudf.DataFrame(pdf)
with pytest.raises(KeyError):
pdf.sort_index(axis=1, level="nope")
with pytest.raises(KeyError):
gdf.sort_index(axis=1, level="nope")


@pytest.mark.parametrize("level", [0, "cols"])
@pytest.mark.parametrize("ascending", [True, False])
def test_sort_index_axis_1_flat_columns_level(level, ascending):
# a flat columns axis accepts level 0 / its own name like pandas
pdf = pd.DataFrame(
[[1, 2, 3]], columns=pd.Index(["b", "c", "a"], name="cols")
)
gdf = cudf.DataFrame(pdf)

expect = pdf.sort_index(axis=1, level=level, ascending=ascending)
got = gdf.sort_index(axis=1, level=level, ascending=ascending)
assert_eq(expect, got)


@pytest.mark.parametrize("na_position", ["first", "last"])
@pytest.mark.parametrize("ascending", [True, False])
def test_sort_index_axis_1_flat_nan_labels_na_position(na_position, ascending):
# the plain (no level) axis=1 path previously used python sorted(),
# which cannot honor na_position for NaN labels
pdf = pd.DataFrame(
[[1, 2, 3]], columns=pd.Index(["b", np.nan, "a"], dtype="object")
)
gdf = cudf.DataFrame(pdf)

expect = pdf.sort_index(
axis=1, ascending=ascending, na_position=na_position
)
got = gdf.sort_index(axis=1, ascending=ascending, na_position=na_position)
assert_eq(expect, got)
Loading