Skip to content

Fix MultiIndex indexing/.loc to match pandas - #22966

Merged
rapids-bot[bot] merged 21 commits into
NVIDIA:mainfrom
galipremsagar:midx_fixes
Jul 14, 2026
Merged

Fix MultiIndex indexing/.loc to match pandas#22966
rapids-bot[bot] merged 21 commits into
NVIDIA:mainfrom
galipremsagar:midx_fixes

Conversation

@galipremsagar

@galipremsagar galipremsagar commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes a range of MultiIndex .loc/__getitem__ incompatibilities surfaced by the cudf.pandas tests/indexing/multiindex suite (72 → 14 failures, no cuDF regressions). Behavior changes that would affect the classic fast path are gated on mode.pandas_compatible.

  • MultiIndex.__contains__: handle partial/full tuple keys (delegate to get_loc)
  • .loc row lookup (pandas-compatible): cartesian-product of per-level keys, de-duplicated labels, pandas-matching result order, and KeyError on missing labels/combinations
  • A single complete column label returns a Series; Series.loc/single-tuple .loc drop scalar-selected index levels
  • Reject set indexers with TypeError; raise KeyError on a missing scalar row label instead of falling back to positional indexing
  • NotImplementedError for strided/reversed MultiIndex label slices
  • Plumb per-level column dtypes through the column accessor so empty MultiIndex columns keep their per-level dtype
  • stack materializes levels in first-appearance order so a later to_pandas/unstack no longer lexicographically reorders the pivoted axis

Also drops the now-passing entries from the cudf.pandas xfail list and adds cuDF unit tests.

The argsort (np.intp return) and DataFrame.rename MultiIndex fixes from the original branch have been split into separate PRs.

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@copy-pr-bot

copy-pr-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@github-actions github-actions Bot added Python Affects Python cuDF API. cudf.pandas Issues specific to cudf.pandas labels Jun 24, 2026
@GPUtester GPUtester moved this to In Progress in cuDF Python Jun 24, 2026
@galipremsagar galipremsagar changed the title fix Fix MultiIndex indexing/.loc to match pandas Jun 24, 2026
@galipremsagar galipremsagar added bug Something isn't working breaking Breaking change labels Jun 24, 2026
@galipremsagar
galipremsagar marked this pull request as ready for review June 24, 2026 14:42
@galipremsagar
galipremsagar requested a review from a team as a code owner June 24, 2026 14:42
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

cuDF updates MultiIndex .loc handling, membership checks, dtype metadata propagation, and pandas-testing expectations, alongside new tests for DataFrame and Series MultiIndex behavior.

Changes

MultiIndex .loc and metadata parity

Layer / File(s) Summary
MultiIndex lookup core
python/cudf/cudf/core/multiindex.py
Updates MultiIndex validity-mask construction, tuple downcasting, strided-slice rejection, set rejection, and membership checks in the core lookup path.
DataFrame and Series .loc routing
python/cudf/cudf/core/dataframe.py, python/cudf/cudf/core/series.py
Adds the row-label error sentinel, per-level tuple retry path, and MultiIndex row-major selection changes for DataFrame and Series .loc.
Column metadata and ordering
python/cudf/cudf/core/dataframe.py, python/cudf/cudf/core/indexed_frame.py
Threads MultiIndex column level dtypes through construction and copy paths, updates rename(level=...), and changes stack level materialization order.
Behavior validation
python/cudf/cudf/tests/dataframe/indexing/test_loc.py, python/cudf/cudf/tests/series/indexing/test_loc.py, python/cudf/cudf/tests/dataframe/methods/test_rename.py, python/cudf/cudf/tests/indexes/multiindex/methods/test_contains.py
Adds MultiIndex .loc, membership, rename, and dtype-preservation coverage for DataFrame and Series cases.
Known-failure mapping updates
python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
Removes and replaces expected-failure entries across MultiIndex indexing, sorting, stack/unstack, and datetimelike cases.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • rapidsai/cudf#22912: Touches the same MultiIndex .loc tuple-handling and row-selection code paths.
  • rapidsai/cudf#22904: Also updates cudf.pandas known-failure mappings for MultiIndex indexing behavior.

Suggested labels: 3 - Ready for Review

Suggested reviewers: TomAugspurger, vyasr, brandon-b-miller

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: aligning MultiIndex indexing and .loc behavior with pandas.
Description check ✅ Passed The description matches the changeset and covers the main MultiIndex behavior fixes plus tests and xfail updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/cudf/cudf/core/dataframe.py`:
- Around line 4169-4172: The MultiIndex level rename path in DataFrame.rename is
missing the all-null replacement handling that the generic index path already
uses. Update the ca[ca_label] = level_values._column.find_and_replace call in
dataframe.py to pass the same is_all_na flag used elsewhere in this rename
logic, so rename(index={old: None}, level=...) behaves consistently for all-null
replacements.
- Around line 280-289: The row selection logic in DataFrame indexing is
collapsing a single match to a scalar too early for MultiIndex rows. Update the
branch around the existing row label handling in cudf.DataFrame so that the
`row_is_full_label` check only treats a scalar `row_arg` as a full label when
the index is not a MultiIndex or when all MultiIndex levels are explicitly
provided; for partial MultiIndex row keys, keep the result as a length-1 Series.
Preserve the current behavior in the `result = result[result._column_names[0]]`
path, but gate the final `element_indexing(0)` collapse behind the stricter
full-label condition.

In `@python/cudf/cudf/core/multiindex.py`:
- Around line 982-995: The all-scalar MultiIndex selection path in multiindex.py
is incorrectly treating duplicate matches as the empty-Series case when
len(result) > 1, which drops valid rows. Update the logic in the scalar-collapse
branch around the len(keep) == 0 handling so that duplicate-row results are
preserved instead of falling through to the empty Series return; use the
existing result object from the MultiIndex lookup and ensure full scalar keys
return all matched rows consistently.
- Around line 802-824: In the MultiIndex lookup path inside the row-tuple
handling logic, all-wildcard keys like a tuple of slice(None) currently produce
an empty positions/lookup_dict and fall through to a 0-column merge instead of
returning the full row selection. Update the branch that builds lookup_dict so
that when every element of row_tuple is a wildcard, it short-circuits to an
identity selection in the MultiIndex access path (for example, by returning the
original rows or equivalent full-row mask) before calling the merge logic. Use
the existing row_tuple, positions, and lookup_dict flow to locate the fix.

In `@python/cudf/cudf/core/series.py`:
- Around line 307-315: The full-scalar tuple path in Series.loc is collapsing
duplicate matches by unconditionally taking result.iloc[0], which silently drops
rows for non-unique MultiIndex keys. Update the lookup logic in the Series
indexing code around the tuple/scalar check so it only collapses to a scalar
when the match is truly a single row, and preserves the full Series when the
full key resolves to multiple rows. Use the existing Series.loc / result
handling and the tuple-is-scalar branch to locate the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 63410b90-e8f5-401e-a76d-21d8967bf738

📥 Commits

Reviewing files that changed from the base of the PR and between 409fbef and 9b0576f.

📒 Files selected for processing (11)
  • python/cudf/cudf/core/dataframe.py
  • python/cudf/cudf/core/frame.py
  • python/cudf/cudf/core/indexed_frame.py
  • python/cudf/cudf/core/multiindex.py
  • python/cudf/cudf/core/series.py
  • python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
  • python/cudf/cudf/tests/dataframe/indexing/test_loc.py
  • python/cudf/cudf/tests/dataframe/methods/test_rename.py
  • python/cudf/cudf/tests/indexes/multiindex/methods/test_argsort.py
  • python/cudf/cudf/tests/indexes/multiindex/methods/test_contains.py
  • python/cudf/cudf/tests/series/indexing/test_loc.py
💤 Files with no reviewable changes (1)
  • python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py

Comment thread python/cudf/cudf/core/dataframe.py
Comment thread python/cudf/cudf/core/dataframe.py
Comment thread python/cudf/cudf/core/multiindex.py
Comment thread python/cudf/cudf/core/multiindex.py
Comment thread python/cudf/cudf/core/series.py
@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test a3c5e57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/cudf/cudf/core/dataframe.py (1)

268-276: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

HIGH: Treat full MultiIndex column tuples as single labels here.

This special full-row-tuple-list path only downcasts scalar column keys, so df.loc[[row_tuple], full_column_tuple] with MultiIndex columns can return a one-column DataFrame instead of the pandas-compatible Series.

Proposed fix
-                    if (
-                        isinstance(arg, tuple)
-                        and len(arg) > 1
-                        and is_scalar(arg[1])
-                    ):
+                    if isinstance(arg, tuple) and len(arg) > 1:
+                        col_key = arg[1]
+                        col_nlevels = self._frame._data.nlevels
+                        col_is_single_label = (
+                            is_scalar(col_key)
+                            if col_nlevels == 1
+                            else (
+                                isinstance(col_key, tuple)
+                                and len(col_key) == col_nlevels
+                                and all(is_scalar(x) for x in col_key)
+                            )
+                        )
+                    else:
+                        col_is_single_label = False
+                    if col_is_single_label:
                         # A scalar column key yields a single column, so
                         # downcast the result to a Series.
-                        return result[arg[1]]
+                        return result[result._column_names[0]]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cudf/cudf/core/dataframe.py` around lines 268 - 276, The tuple-key
downcast in DataFrame.loc handling only checks is_scalar(arg[1]), so full
MultiIndex column tuples are treated as multi-column keys instead of single
labels. Update the special case in dataframe.py around the result[arg[1]] path
to recognize a full column tuple as a single column label when the columns are a
MultiIndex, and downcast to the Series result accordingly. Keep the change
localized to the row-tuple-list branch that already handles scalar column keys.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py`:
- Line 5369: The entry for
tests/indexes/test_setops.py::TestSetOps::test_symmetric_difference[multi] is
incorrectly listed in NODEIDS_TO_SKIP even though it xpasses with cudf.pandas.
Remove this nodeid from the skip mapping in pandas-testing-plugin.py and place
it in the xpass/xfail tracking path used for pandas xfails that pass under
cudf.pandas, keeping the existing reason string consistent with the other
tracking entries.

---

Outside diff comments:
In `@python/cudf/cudf/core/dataframe.py`:
- Around line 268-276: The tuple-key downcast in DataFrame.loc handling only
checks is_scalar(arg[1]), so full MultiIndex column tuples are treated as
multi-column keys instead of single labels. Update the special case in
dataframe.py around the result[arg[1]] path to recognize a full column tuple as
a single column label when the columns are a MultiIndex, and downcast to the
Series result accordingly. Keep the change localized to the row-tuple-list
branch that already handles scalar column keys.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 82f279b4-50f1-4bca-af71-7c3eda87d3da

📥 Commits

Reviewing files that changed from the base of the PR and between 87729e1 and a3c5e57.

📒 Files selected for processing (3)
  • python/cudf/cudf/core/dataframe.py
  • python/cudf/cudf/core/multiindex.py
  • python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/cudf/cudf/core/multiindex.py

Comment thread python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py

@mroeschke mroeschke left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a first pass. Still need to fully digest all the MultiIndex changes here. If any fixes are independent and could be split into another PR that would be helpful

Comment thread python/cudf/cudf/core/dataframe.py Outdated
Comment thread python/cudf/cudf/tests/series/indexing/test_loc.py Outdated
Comment thread python/cudf/cudf/core/frame.py Outdated
Comment thread python/cudf/cudf/core/multiindex.py Outdated
Comment thread python/cudf/cudf/core/multiindex.py Outdated
Comment thread python/cudf/cudf/core/multiindex.py Outdated
Comment thread python/cudf/cudf/core/multiindex.py
@copy-pr-bot

copy-pr-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@galipremsagar

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Split out two independent fixes into their own PRs: argsort dtype in #23061 and DataFrame.rename in #23062. The remaining MultiIndex .loc and contains changes are interdependent (contains delegates to get_loc, which uses the updated _compute_validity_mask), so they stay here. Inline comments addressed above.

# Conflicts:
#	python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test 667d2f5

@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test 94189ac

@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test 18605de

@galipremsagar
galipremsagar requested a review from mroeschke July 1, 2026 22:49
@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test 840f4f2

rapids-bot Bot pushed a commit that referenced this pull request Jul 2, 2026
Split out of #22966 per review feedback.

`Frame.argsort` (and `Series.argsort`/`Index.argsort`, which delegate to it) now returns an `np.intp` (int64) positional indexer, matching numpy's and pandas' `argsort`. Previously cuDF returned an `int32` gather-map dtype, which broke dtype-strict comparisons (e.g. `assert_numpy_array_equal`) against pandas.

This is applied unconditionally (not gated on `mode.pandas_compatible`) per review. The `argsort`/`sort_values(return_indexer=True)` docstrings are updated to the new dtype, and the now-passing `cudf.pandas` xfail entries are removed.

Authors:
  - GALI PREM SAGAR (https://github.com/galipremsagar)

Approvers:
  - Matthew Murray (https://github.com/Matt711)

URL: #23061
rapids-bot Bot pushed a commit that referenced this pull request Jul 2, 2026
Split out of #22966 per review feedback.

`DataFrame.rename(index=..., level=...)` on a MultiIndex with an **unnamed** level previously inserted a spurious extra level instead of overwriting the target level: the level *name* (`None` for an unnamed level) was used as the `ColumnAccessor` key. It now resolves to the positional ColumnAccessor label via `MultiIndex._level_to_ca_label`, restores the original level names afterward, and forwards `all_nan` to `find_and_replace` so all-null replacements behave like the non-level path.

Adds a cuDF unit test and removes the now-passing `cudf.pandas` xfail entry.

Authors:
  - GALI PREM SAGAR (https://github.com/galipremsagar)

Approvers:
  - Vyas Ramasubramani (https://github.com/vyasr)

URL: #23062
@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test a2ff7bc

# Conflicts:
#	python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test ae14ee8

@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test 87bc09f

@galipremsagar

Copy link
Copy Markdown
Contributor Author

/merge

@galipremsagar galipremsagar added the 5 - Ready to Merge Testing and reviews complete, ready to merge label Jul 14, 2026
@rapids-bot
rapids-bot Bot merged commit 015e090 into NVIDIA:main Jul 14, 2026
127 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in cuDF Python Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

5 - Ready to Merge Testing and reviews complete, ready to merge breaking Breaking change bug Something isn't working cudf.pandas Issues specific to cudf.pandas Python Affects Python cuDF API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants