Skip to content

Fix DataFrame.stack level resolution, ordering, and dtype preservation - #23370

Merged
rapids-bot[bot] merged 5 commits into
NVIDIA:release/26.08from
galipremsagar:dataframe-stack-pandas-semantics
Jul 23, 2026
Merged

Fix DataFrame.stack level resolution, ordering, and dtype preservation#23370
rapids-bot[bot] merged 5 commits into
NVIDIA:release/26.08from
galipremsagar:dataframe-stack-pandas-semantics

Conversation

@galipremsagar

Copy link
Copy Markdown
Contributor

Description

Split out of #23255 (3/6), superseding it. Depends on #23365 (MultiIndex column fidelity) — 14 of the 28 un-xfailed pandas-tests need both fixes, so this PR's pandas-tests job goes green once #23365 merges.

  • Resolve level positionally: integer column-level names no longer collide with level positions (pandas' Index.get_level_values resolves integers by name first, so frames with integer level names returned data from the wrong level).
  • Validate out-of-bounds integer levels (IndexError) and duplicated level names (ValueError) with pandas' messages; negative out-of-bounds levels previously wrapped around silently.
  • Build the stacked level keys from the column MultiIndex's own levels/codes so per-level dtypes survive: int64 levels with missing entries no longer upcast to float64, and categorical levels stay categorical through the pylibcudf tile step (which only sees codes).
  • Emit stacked keys in appearance order, matching pandas. This replaces the argsort-based reordering, which misaligned column data for non-involution column permutations (e.g. a 3-cycle) and NaN keys; pandas legacy stack sorts multi-level keys by level codes, not values.
  • Attach pandas-faithful levels/codes to the result index eagerly (the original index contributes its own levels/codes; flat indexes and the tiled level get appearance-order factorization) so a later unstack restores the original row/column order; the legacy dropna path preserves them by masking codes instead of gathering the index.

Fixes 28 pandas-tests; their xfail entries are removed. Three classic categorical unstack params are un-xfailed (fixed by this change together with #23365). Attribution verified per node id against isolated builds: 14 pass with only this change, 14 need this plus #23365.

Checklist

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

- Resolve level positionally: integer column-level names no longer
  collide with level positions (pandas' Index.get_level_values resolves
  integers by name first, returning data from the wrong level).
- Validate out-of-bounds integer levels (IndexError) and duplicated
  level names (ValueError) with pandas' messages; negative out-of-bounds
  levels previously wrapped around silently.
- Build stacked level keys from the column MultiIndex's own levels and
  codes so per-level dtypes survive: int64 levels with missing entries
  no longer upcast to float64 and categorical levels stay categorical
  through the pylibcudf tile step.
- Emit stacked keys in appearance order like pandas, replacing the
  argsort-based reordering that misaligned column data for
  non-involution column permutations and NaN keys; pandas legacy stack
  sorts multi-level keys by level codes, not values.
- Attach pandas-faithful levels/codes to the result index eagerly so a
  later unstack restores the original row/column order; the legacy
  dropna path preserves them by masking the codes instead of gathering.

Fixes 28 pandas-tests; their xfail entries are removed. Three classic
categorical unstack params are un-xfailed (fixed by this change plus
the MultiIndex-column-fidelity PR). 14 of the 28 also require the
fidelity PR; merge that first.
@galipremsagar
galipremsagar requested a review from a team as a code owner July 21, 2026 14:23
@copy-pr-bot

copy-pr-bot Bot commented Jul 21, 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.

@github-actions github-actions Bot added Python Affects Python cuDF API. cudf.pandas Issues specific to cudf.pandas labels Jul 21, 2026
@GPUtester GPUtester moved this to In Progress in cuDF Python Jul 21, 2026
@galipremsagar
galipremsagar requested a review from vyasr July 21, 2026 14:26
@galipremsagar galipremsagar added the 3 - Ready for Review Ready for review by team label Jul 21, 2026
@galipremsagar galipremsagar added bug Something isn't working breaking Breaking change labels Jul 21, 2026
@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test f8dcf59

@copy-pr-bot

copy-pr-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

/okay to test f8dcf59

@galipremsagar, there was an error processing your request: E2

See the following link for more information: https://docs.gha-runners.nvidia.com/cpr/e/2/

@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test 9ceceb7

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 29420ab7-f20b-4fdb-a859-5cddc2740d22

📥 Commits

Reviewing files that changed from the base of the PR and between d9f89b0 and c119803.

📒 Files selected for processing (3)
  • python/cudf/cudf/core/dataframe.py
  • python/cudf/cudf/core/indexed_frame.py
  • python/cudf/cudf/tests/reshape/test_stack.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/cudf/cudf/core/dataframe.py

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved DataFrame stacking and unstacking with stricter level validation and pandas-compatible handling of negative, positional, duplicate, and out-of-range levels.
    • Preserved data types, missing values, index levels, and ordering more reliably when reshaping MultiIndex data.
    • Fixed MultiIndex preservation when filtering rows with boolean masks.
    • Categorical column indexes are now supported during unstacking.

Walkthrough

DataFrame.stack now normalizes and validates levels, preserves MultiIndex dtypes and ordering, and applies explicit missing-value handling. Related MultiIndex validation, reshape tests, and pandas-testing mappings are updated.

Changes

Stack and reshape compatibility

Layer / File(s) Summary
Stack level resolution and MultiIndex construction
python/cudf/cudf/core/dataframe.py, python/cudf/cudf/core/multiindex.py
Stack level validation and normalization now follow pandas-compatible behavior, while output levels and codes preserve dtypes and scalar values.
Stack grouping and result assembly
python/cudf/cudf/core/dataframe.py, python/cudf/cudf/core/indexed_frame.py
Grouping, ordering, column-key construction, and dropna handling now use explicit levels, codes, and masks, including MultiIndex preservation during masking.
Reshape regression coverage and expectations
python/cudf/cudf/tests/reshape/test_stack.py, python/cudf/cudf/tests/reshape/test_unstack.py, python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
Tests cover level resolution, validation, ordering, and categorical unstack behavior; related failure mappings are narrowed or removed.

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

Possibly related PRs

  • rapidsai/cudf#22966: Both modify DataFrame.stack and MultiIndex level/code materialization.
  • rapidsai/cudf#23255: Both update stack level normalization and bounds behavior.
  • rapidsai/cudf#23368: The MultiIndex level validation changes overlap with the related unstack/pivot reshaping path.

Suggested labels: improvement

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

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 accurately summarizes the main DataFrame.stack changes around level resolution, ordering, and dtype preservation.
Description check ✅ Passed The description matches the changeset and objectives, covering stack fixes, dtype preservation, ordering, and xfail removals.
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.

@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test d9f89b0

@galipremsagar
galipremsagar requested a review from Matt711 July 22, 2026 22:29

@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.

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)

8314-8340: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use dropna=False in this groupby
unnamed_level_values.unique() keeps NaN-containing keys, but groupby(..., sort=False) drops them by default. That can break partial stacks with unnamed column levels containing -1 codes by leaving stacked and keys out of sync at zip(..., strict=True). Add a regression test for a partial stack with missing unnamed-level entries.

🤖 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 8314 - 8340, Update the
groupby call in unnamed_group_generator to use dropna=False so NaN-containing
unnamed level keys are retained and remain aligned with
unnamed_level_values.unique() during the strict zip. Add a regression test
covering a partial stack with missing unnamed-level entries, verifying the
resulting stacked columns and keys stay synchronized.
🤖 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.

Outside diff comments:
In `@python/cudf/cudf/core/dataframe.py`:
- Around line 8314-8340: Update the groupby call in unnamed_group_generator to
use dropna=False so NaN-containing unnamed level keys are retained and remain
aligned with unnamed_level_values.unique() during the strict zip. Add a
regression test covering a partial stack with missing unnamed-level entries,
verifying the resulting stacked columns and keys stay synchronized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: cb6e34ed-f212-48da-8b69-a4914228de8c

📥 Commits

Reviewing files that changed from the base of the PR and between 9ceceb7 and d9f89b0.

📒 Files selected for processing (2)
  • python/cudf/cudf/core/dataframe.py
  • python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
💤 Files with no reviewable changes (1)
  • python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py

@Matt711 Matt711 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Minor suggestions, otherwise looks good

Comment thread python/cudf/cudf/core/dataframe.py Outdated
Comment thread python/cudf/cudf/core/dataframe.py
Comment thread python/cudf/cudf/tests/reshape/test_stack.py Outdated
Comment thread python/cudf/cudf/core/dataframe.py Outdated
Comment thread python/cudf/cudf/core/dataframe.py Outdated
- Use MultiIndex._simple_new with pre-built levels/codes instead of
  _from_data + post-assignment, per mroeschke's suggestion
- Move levels/codes propagation into _apply_boolean_mask so any
  frame with a pre-set MultiIndex index preserves level fidelity
  through boolean-mask operations (simplifies the stack dropna path)
- Add dropna=False to the groupby in unnamed_group_generator so
  NaN-containing unnamed-level keys are retained and stay aligned
  with unique() during the strict zip
- Tighten keys type annotation from Any to list[tuple[Any, ...]]
- Add match= arguments to pytest.raises calls in test_stack.py
@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test c119803

@galipremsagar galipremsagar added 5 - Ready to Merge Testing and reviews complete, ready to merge and removed 3 - Ready for Review Ready for review by team labels Jul 23, 2026
@galipremsagar

Copy link
Copy Markdown
Contributor Author

/merge

@rapids-bot
rapids-bot Bot merged commit 55317dc into NVIDIA:release/26.08 Jul 23, 2026
128 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in cuDF Python Jul 23, 2026
rapids-bot Bot pushed a commit that referenced this pull request Jul 23, 2026
Split out of #23255 (4/6). **Depends on #23365 (fidelity), the stack PR (#23370), and #23366 (groupby agg)** — 8 of the 29 un-xfailed pandas-tests need those fixes too, so this PR's pandas-tests job goes green once they merge.

- Order result rows/columns by the removed level's codes (level order preserved, missing keys first) instead of sorted values with nulls last, by encoding the integer code columns instead of the level values.
- Propagate the source frame's column-axis level names into the result instead of hardcoding `None`; also fixes the `ValueError: Length of names must match number of levels` crash when unstacking MultiIndex-column frames.
- Promote integer source columns to float64 when the reshape introduces missing cells (pandas' block semantics), gated on `mode.pandas_compatible`; `pivot_table`/`crosstab` opt out via a module-private `_unstack` parameter when `fill_value` fills the cells afterwards.
- Preserve unused categories of the removed level in the result's column levels (pandas GH 17845); also fixes a libcudf `Column sizes don't match` crash for indexes with unused categorical categories.
- Validate the level on flat-index frames (`KeyError`) and duplicated index names (`ValueError`) like pandas; `pivot` with `values=` drops the original columns-axis names.

Fixes 29 pandas-tests; their xfail entries are removed, three remaining `test_stack_unstack.py` entries get real failure reasons, and two classic categorical unstack params are un-xfailed. Attribution verified per node id against isolated builds: 21 pass with only this change, 4 need the stack PR, 2 need stack+fidelity, 2 need the groupby-agg PR.

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

Approvers:
  - Matthew Roeschke (https://github.com/mroeschke)

URL: #23368
galipremsagar added a commit to galipremsagar/cudf that referenced this pull request Jul 27, 2026
Resolves a conflict in MultiIndex._level_index_from_level, which main
rewrote in NVIDIA#23370 to fix the same negative-level wraparound. Keeps
main's `norm` formulation and layers on the two things pandas'
MultiIndex._get_level_number does that neither side had: distinct
wording for underflow ("-3 is not a valid level number") versus
overflow ("not 3"), and the ValueError for an ambiguous duplicate
level name.

Also restores _check_duplicate_level_names, which this branch had
removed as unused. Main added two callers of it in the meantime
(DataFrame.stack and reshape's unstack), and the textual merge took
the deletion without conflicting.
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

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants