Skip to content

Fix GroupBy.apply result assembly, UDF closure side effects, and empty-frame dtypes - #23272

Merged
rapids-bot[bot] merged 8 commits into
NVIDIA:release/26.08from
galipremsagar:groupby-apply-fixes
Jul 18, 2026
Merged

Fix GroupBy.apply result assembly, UDF closure side effects, and empty-frame dtypes#23272
rapids-bot[bot] merged 8 commits into
NVIDIA:release/26.08from
galipremsagar:groupby-apply-fixes

Conversation

@galipremsagar

Copy link
Copy Markdown
Contributor

Description

Running pandas' own test suite under cudf.pandas, tests/groupby/test_apply.py had 28 failing tests. This PR fixes 26 of them (137 tests: 135 pass; the 2 remaining are inherent and keep documented plugin entries) and removes the fixed xfail entries from the pandas-testing plugin.

cudf.pandas: UDF closure side effects were silently discarded

_transform_arg rebuilt lists and dicts even when no element needed proxy conversion. The rebuilt container fails _replace_closurevars' identity check, so user functions were rebuilt around a copy of their closed-over mutable containers — a UDF like lambda g: names.append(g.name) appended into a throwaway copy on both the fast attempt and the pandas fallback, and the user's list stayed empty. The list/dict branches are now identity-preserving when unchanged, mirroring the existing object-ndarray branch.

GroupBy.apply result assembly (pandas parity, all verified empirically on 3.0.3)

  • All-None DataFrameGroupBy results return an empty frame keeping the value columns and dtypes (pandas GH9684/GH57775).
  • Series results sharing an identical index stack into one row per group with columns given by the common index; a consistent Series name becomes the columns-axis name (GH6124). Series results with differing indexes concatenate lengthwise under the group keys (GH8467). This replaces row-count heuristics that mislabeled columns and mis-shaped results.
  • Transform results (chunks indexed like their input) restore the original row order regardless of sort, like pandas' _concat_objects; the final sort_index is removed since group-keyed results are already emitted in sorted key order and pandas preserves the UDF's within-group row order (GH52444).
  • include_groups=True raises ValueError, matching pandas 3.0.

Supporting fixes

  • DataFrame({"a": []}) defaults untyped empty sequences to float64 like pandas' constructor (numpy's empty-array default); Series([]) stays object.
  • reset_index derives the result columns dtype via pandas' Index.insert provenance instead of re-inferring from the merged labels, and Series.reset_index resolves the value-column name before resetting (pandas' to_frame(name).reset_index() semantics).
  • as_column routes stdlib datetime/timedelta elements through the pandas object path, so mixed datetime+non-datetime lists raise MixedTypeError instead of silently coercing.
  • Removed a stale workaround in test_groupby_apply_return_col_from_df and a stale conditional xfail in test_dataframe_assign_scalar — both now match pandas exactly.

Validation

  • pandas-tests tests/groupby/test_apply.py: 28 failed → 135/137 pass (2 inherent, documented).
  • pandas-tests reset_index/constructors/groupby neighbors: only pre-existing known failures.
  • cuDF classic: groupby suites clean including under NO_EXTERNAL_ONLY_APIS=1; dataframe/series/reshape/indexes/input_output sweep (70k+ tests) green.
  • cudf.pandas proxy unit tests pass.

Checklist

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

@galipremsagar
galipremsagar requested a review from a team as a code owner July 15, 2026 12:02
@copy-pr-bot

copy-pr-bot Bot commented Jul 15, 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 15, 2026
@galipremsagar galipremsagar added bug Something isn't working non-breaking Non-breaking change labels Jul 15, 2026
@GPUtester GPUtester moved this to In Progress in cuDF Python Jul 15, 2026
@coderabbitai

coderabbitai Bot commented Jul 15, 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

Changes

Pandas compatibility behavior

Layer / File(s) Summary
Construction and index-reset semantics
python/cudf/cudf/core/column/column.py, python/cudf/cudf/core/dataframe.py, python/cudf/cudf/core/indexed_frame.py, python/cudf/cudf/core/series.py, python/cudf/cudf/tests/dataframe/test_constructors.py
Datetime-like iterable conversion, empty mapping dtype inference, and reset-index label/value handling are aligned with pandas behavior.
Label metadata and quantile selection
python/cudf/cudf/core/dataframe.py, python/cudf/cudf/tests/dataframe/methods/test_rename.py, python/cudf/cudf/tests/dataframe/methods/test_reductions.py
Column-label dtype metadata is recomputed after renaming, and DataFrame.quantile filters requested columns before computation.
GroupBy.apply result assembly
python/cudf/cudf/core/groupby/groupby.py, python/cudf/cudf/tests/groupby/test_apply.py, python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
GroupBy apply processing now covers empty results, Series-returning functions, transform ordering, include_groups validation, final ordering, and updated compatibility reasons.
Container identity preservation
python/cudf/cudf/pandas/fast_slow_proxy.py, python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py, python/cudf/benchmarks/internal/bench_fast_slow_proxy.py
List, tuple, and dictionary transformations preserve unchanged containers by identity and rebuild containers containing transformed proxy values, with coverage and benchmarks.

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

Possibly related PRs

Suggested reviewers: rjzamora, mroeschke, brandon-b-miller, wence-

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
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.
Title check ✅ Passed The title clearly summarizes the main changes around GroupBy.apply, UDF identity preservation, and dtype handling.
Description check ✅ Passed The description matches the changeset and explains the groupby, proxy, constructor, and reset_index fixes.
✨ 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: 3

🧹 Nitpick comments (1)
python/cudf/cudf/pandas/fast_slow_proxy.py (1)

1398-1414: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression coverage for both identity-preserving branches.

The supplied downstream test covers object-dtype ndarray identity, but not the new list/dict behavior. Add tests for empty and single-element containers, nested unchanged containers, proxy-containing containers that must rebuild, key/value transformations, and supported container subclasses. Add the required unit benchmark for this bug-fix contribution.

As per coding guidelines, bug-fix contributions require unit tests and unit benchmarks, with Python changes validated through the repository’s pre-commit checks.

Also applies to: 1486-1498

🤖 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/pandas/fast_slow_proxy.py` around lines 1398 - 1414, Add
regression tests for the container transformation logic in _transform_arg,
covering empty and single-element lists/dicts, nested unchanged containers,
proxy-containing containers that rebuild, key and value transformations, and
supported container subclasses. Add the required unit benchmark for this bug
fix, and run the repository’s Python pre-commit checks to validate the changes.

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/core/groupby/groupby.py`:
- Around line 2418-2422: Update the include_groups parameter documentation in
the groupby apply docstring to remove the outdated statement that True attempts
to apply func to groupings. Ensure the docstring consistently states that only
False is accepted and True raises ValueError, matching the validation in the
groupby apply implementation.
- Around line 2324-2335: Update the transform-like UDF branch in the groupby
result assembly to validate each group’s output size against the corresponding
expected size using the existing offsets, rather than comparing only the
aggregate chunk length to len(self.obj). Only construct and assign the
MultiIndex when all per-group sizes match, preserving correct row alignment for
unevenly shrinking or expanding groups.

In `@python/cudf/cudf/pandas/fast_slow_proxy.py`:
- Around line 1398-1414: Update the transformed iterable construction in
_transform_arg so list subclasses receive the transformed values through the
original generator-style constructor input rather than a concrete list. Preserve
the unchanged-object identity return and ensure subclass-specific iterable
construction behavior remains intact.

---

Nitpick comments:
In `@python/cudf/cudf/pandas/fast_slow_proxy.py`:
- Around line 1398-1414: Add regression tests for the container transformation
logic in _transform_arg, covering empty and single-element lists/dicts, nested
unchanged containers, proxy-containing containers that rebuild, key and value
transformations, and supported container subclasses. Add the required unit
benchmark for this bug fix, and run the repository’s Python pre-commit checks to
validate the changes.
🪄 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: 72ad9afb-4a31-4e75-a223-dad629c342ec

📥 Commits

Reviewing files that changed from the base of the PR and between 4700d36 and a4025bc.

📒 Files selected for processing (9)
  • python/cudf/cudf/core/column/column.py
  • python/cudf/cudf/core/dataframe.py
  • python/cudf/cudf/core/groupby/groupby.py
  • python/cudf/cudf/core/indexed_frame.py
  • python/cudf/cudf/core/series.py
  • python/cudf/cudf/pandas/fast_slow_proxy.py
  • python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
  • python/cudf/cudf/tests/dataframe/indexing/test_setitem.py
  • python/cudf/cudf/tests/groupby/test_apply.py
💤 Files with no reviewable changes (1)
  • python/cudf/cudf/tests/groupby/test_apply.py

Comment thread python/cudf/cudf/core/groupby/groupby.py Outdated
Comment thread python/cudf/cudf/core/groupby/groupby.py Outdated
Comment thread python/cudf/cudf/pandas/fast_slow_proxy.py Outdated
@galipremsagar

Copy link
Copy Markdown
Contributor Author

Merged in upstream/main and resolved the two conflicts:

  • pandas-testing-plugin: kept this PR's tests/groupby/test_apply.py cleanup (main's side re-baselined reasons for entries this PR removes; both sides had dropped the test_empty_df entries).
  • dataframe.py (the empty-column float64 coercion this PR and Fix DataFrame.quantile and Series.quantile to match pandas #23058 both added): kept (list, tuple, Iterator) and dropped range from the check. pandas converts range via np.arange, so pd.DataFrame({'a': range(0)}) is int64, and as_column already matches via from_range — main's range inclusion was coercing that to float64. Added constructor tests pinning the empty iterator (float64) and empty range (int64) dtypes.

Re-validated after the merge + review changes: pandas-tests groupby/test_apply.py 135 passed / 2 xfailed; frame test_quantile.py + test_constructors.py clean under the plugin; cudf classic groupby suite (also with NO_EXTERNAL_ONLY_APIS=1) has no new failures vs main; dataframe+series sweep 49k tests green; test_fast_slow_proxy.py / test_cudf_pandas.py pass.

@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test 731dc29

@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 (2)
python/cudf/cudf/pandas/fast_slow_proxy.py (1)

1505-1508: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Compare mapping entries without invoking overridden mapping methods.

zip(transformed_dict, arg) invokes arg.__iter__, and arg[old_k] invokes arg.__getitem__. An unchanged dict subclass overriding either can now fail during identity detection even though the preceding arg.items() transformation succeeded. Compare .items() pairs directly and add a regression test.

Proposed fix
         if len(transformed_dict) == len(arg) and all(
-            new_k is old_k and transformed_dict[new_k] is arg[old_k]
-            for new_k, old_k in zip(transformed_dict, arg, strict=True)
+            new_k is old_k and new_v is old_v
+            for (new_k, new_v), (old_k, old_v) in zip(
+                transformed_dict.items(), arg.items(), strict=True
+            )
         ):
🤖 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/pandas/fast_slow_proxy.py` around lines 1505 - 1508, Update
the identity check in the surrounding transformation logic to compare the
transformed mapping’s items directly with the original entries, avoiding
iteration and item access through overridden mapping methods on arg. Preserve
the existing key and value identity checks, and add a regression test covering
an unchanged dict subclass that overrides __iter__ or __getitem__.
python/cudf/cudf/core/dataframe.py (1)

6577-6603: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Honor columns in the method="table" path. This branch still passes every data_df._columns to libcudf and returns every column, while columns is only applied in the per-column branch. df.quantile(columns=["a"], method="table") should restrict the computation to the requested subset; add a regression test.

🤖 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 6577 - 6603, Update the
method="table" branch to compute and return only the columns specified by
columns, using the selected column names and corresponding data_df._columns when
building the libcudf table and result mapping; preserve all-column behavior when
columns is unspecified. Add a regression test confirming
df.quantile(columns=["a"], method="table") excludes other columns.

Source: Coding guidelines

🧹 Nitpick comments (2)
python/cudf/cudf/tests/groupby/test_apply.py (1)

766-767: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Pin include_groups=False in both new regression cases.

These tests should exercise the same explicit pandas-compatibility contract as the surrounding test. Relying on each library’s default can make the reference and actual calls diverge across supported pandas versions; pass include_groups=False to both sides.

Proposed change
-    expected = pdf.groupby("k").apply(make_swap_sizes(pd.Series))
-    actual = gdf.groupby("k").apply(make_swap_sizes(cudf.Series))
+    expected = pdf.groupby("k").apply(
+        make_swap_sizes(pd.Series), include_groups=False
+    )
+    actual = gdf.groupby("k").apply(
+        make_swap_sizes(cudf.Series), include_groups=False
+    )

-    expected = pdf.groupby("k").apply(make_fresh_index(pd.Series))
-    actual = gdf.groupby("k").apply(make_fresh_index(cudf.Series))
+    expected = pdf.groupby("k").apply(
+        make_fresh_index(pd.Series), include_groups=False
+    )
+    actual = gdf.groupby("k").apply(
+        make_fresh_index(cudf.Series), include_groups=False
+    )

Also applies to: 784-785

🤖 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/tests/groupby/test_apply.py` around lines 766 - 767, Update
both new regression cases around the groupby apply calls, including the lines
using expected and actual, to pass include_groups=False explicitly to pandas and
cuDF GroupBy.apply. Keep the callback and other arguments unchanged, and apply
the same explicit option to both reference and actual calls.
python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py (1)

694-702: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the list-subclass constructor contract.

This only checks output type; it would still pass if Line 1417 passed transformed_list instead of an iterator. Record the constructor argument type and assert the rebuilt subclass received a non-list iterable.

Proposed test hardening
     class MyList(list):
-        pass
+        def __init__(self, values=()):
+            self.received_materialized_list = isinstance(values, list)
+            super().__init__(values)

     my_list = MyList([1, x])
     result = transform(my_list)
     assert result is not my_list
     assert type(result) is MyList
+    assert not result.received_materialized_list
     assert type(result[1]) is expected_type

As per coding guidelines, “Add unit tests and unit benchmarks for feature and bug-fix contributions.”

🤖 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_pandas_tests/test_fast_slow_proxy.py` around lines 694 -
702, Harden the MyList case in the transform test by recording the argument
received by its constructor and asserting it is a non-list iterable. Update the
subclass used near transform(my_list) to capture construction input while
preserving the existing assertions for a distinct MyList result and transformed
element type.

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/fast_slow_proxy.py`:
- Around line 1414-1417: Add a unit benchmark covering the container
transformation path around the recursive proxy logic that returns
type(arg)(iter(transformed_list)). Benchmark both unchanged containers and
containers containing proxy values, measuring transformation performance and
guarding against regressions from materialization and identity scanning.

---

Outside diff comments:
In `@python/cudf/cudf/core/dataframe.py`:
- Around line 6577-6603: Update the method="table" branch to compute and return
only the columns specified by columns, using the selected column names and
corresponding data_df._columns when building the libcudf table and result
mapping; preserve all-column behavior when columns is unspecified. Add a
regression test confirming df.quantile(columns=["a"], method="table") excludes
other columns.

In `@python/cudf/cudf/pandas/fast_slow_proxy.py`:
- Around line 1505-1508: Update the identity check in the surrounding
transformation logic to compare the transformed mapping’s items directly with
the original entries, avoiding iteration and item access through overridden
mapping methods on arg. Preserve the existing key and value identity checks, and
add a regression test covering an unchanged dict subclass that overrides
__iter__ or __getitem__.

---

Nitpick comments:
In `@python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py`:
- Around line 694-702: Harden the MyList case in the transform test by recording
the argument received by its constructor and asserting it is a non-list
iterable. Update the subclass used near transform(my_list) to capture
construction input while preserving the existing assertions for a distinct
MyList result and transformed element type.

In `@python/cudf/cudf/tests/groupby/test_apply.py`:
- Around line 766-767: Update both new regression cases around the groupby apply
calls, including the lines using expected and actual, to pass
include_groups=False explicitly to pandas and cuDF GroupBy.apply. Keep the
callback and other arguments unchanged, and apply the same explicit option to
both reference and actual calls.
🪄 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: 69d66ed9-894c-41ea-b20c-0ae3fb24e03c

📥 Commits

Reviewing files that changed from the base of the PR and between a4025bc and 8a0636d.

📒 Files selected for processing (8)
  • python/cudf/cudf/core/dataframe.py
  • python/cudf/cudf/core/groupby/groupby.py
  • python/cudf/cudf/core/series.py
  • python/cudf/cudf/pandas/fast_slow_proxy.py
  • python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
  • python/cudf/cudf/tests/dataframe/test_constructors.py
  • python/cudf/cudf/tests/groupby/test_apply.py
  • python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py
💤 Files with no reviewable changes (1)
  • python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/cudf/cudf/core/series.py
  • python/cudf/cudf/core/groupby/groupby.py

Comment thread python/cudf/cudf/pandas/fast_slow_proxy.py
Comment thread python/cudf/cudf/core/groupby/groupby.py Outdated
Comment thread python/cudf/cudf/core/indexed_frame.py
@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test 804b3f9

@galipremsagar
galipremsagar requested a review from mroeschke July 15, 2026 20:27

@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 (2)
python/cudf/cudf/core/groupby/groupby.py (2)

2304-2346: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle mixed None/Series outputs explicitly. This branch assumes every chunk is a Series; if a later group returns None, .index will fail, and if the first result is None the mixed results fall through the generic concat path. Filter out None results before dispatching and add a regression test.

🤖 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/groupby/groupby.py` around lines 2304 - 2346, Update
the groupby result dispatch before the Series-index comparison to handle mixed
None/Series outputs explicitly: remove or separately track None chunks, use the
non-None Series results for index-based branching, and preserve the appropriate
None behavior when constructing the final result. Ensure the path does not
access `.index` on None or send a first-None mixed result through generic
concatenation, and add a regression test covering both result orderings.

Source: Coding guidelines


2333-2346: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve all returned index levels. This branch collapses result.index to result.index._column, so a UDF that returns a MultiIndex loses the extra levels and their names. Rebuild the inner index from every returned level instead.

🤖 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/groupby/groupby.py` around lines 2333 - 2346, The index
reconstruction in the groupby UDF result branch must preserve every level of a
returned MultiIndex. Update the logic around result.index and
MultiIndex._from_data to gather and include each returned index level and its
corresponding name, rather than using only result.index._column, while retaining
the grouping levels and row alignment.
🤖 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/groupby/groupby.py`:
- Around line 2304-2346: Update the groupby result dispatch before the
Series-index comparison to handle mixed None/Series outputs explicitly: remove
or separately track None chunks, use the non-None Series results for index-based
branching, and preserve the appropriate None behavior when constructing the
final result. Ensure the path does not access `.index` on None or send a
first-None mixed result through generic concatenation, and add a regression test
covering both result orderings.
- Around line 2333-2346: The index reconstruction in the groupby UDF result
branch must preserve every level of a returned MultiIndex. Update the logic
around result.index and MultiIndex._from_data to gather and include each
returned index level and its corresponding name, rather than using only
result.index._column, while retaining the grouping levels and row alignment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5add4463-9ec6-4507-b509-a0f534b9e2c7

📥 Commits

Reviewing files that changed from the base of the PR and between 731dc29 and 804b3f9.

📒 Files selected for processing (7)
  • python/cudf/benchmarks/internal/bench_fast_slow_proxy.py
  • python/cudf/cudf/core/dataframe.py
  • python/cudf/cudf/core/groupby/groupby.py
  • python/cudf/cudf/pandas/fast_slow_proxy.py
  • python/cudf/cudf/tests/dataframe/methods/test_reductions.py
  • python/cudf/cudf/tests/groupby/test_apply.py
  • python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/cudf/cudf/pandas/fast_slow_proxy.py
  • python/cudf/cudf/tests/groupby/test_apply.py

@galipremsagar galipremsagar added the 5 - Ready to Merge Testing and reviews complete, ready to merge label Jul 16, 2026
@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test 0c3de7a

…ame dtypes

Fixes 26 of the 28 failing tests in pandas' tests/groupby/test_apply.py
under cudf.pandas (137 tests: 26 fixed, 2 inherent) and removes the
fixed plugin entries; the two kept entries document why they cannot be
fixed (datetime.date type identity on the GPU round trip; CoW block
identity across independent fallback conversions).

cudf.pandas proxy:
* _transform_arg now preserves the identity of lists and dicts whose
  elements needed no conversion (as the object-ndarray branch already
  did). A user function may close over a mutable container and mutate
  it for its side effects (e.g. names.append(group.name) inside
  groupby.apply); copying the container silently discarded those side
  effects on both the fast attempt and the pandas fallback.

GroupBy.apply result assembly (pandas parity, verified empirically):
* All-None DataFrameGroupBy results return an empty frame keeping the
  value columns and dtypes (pandas GH9684/GH57775).
* Series results sharing an identical index stack into one row per
  group with columns given by the common index, propagating a
  consistent Series name to the columns axis (GH6124); Series results
  with differing indexes concatenate lengthwise under the group keys
  (GH8467). This replaces the row-count heuristics that mislabeled
  columns and mis-shaped results.
* Transform results (chunks indexed like their input) restore the
  original row order regardless of sort, like pandas'
  _concat_objects; the final sort_index is removed since group-keyed
  results are already emitted in sorted key order and pandas preserves
  the UDF's within-group row order (GH52444).
* include_groups=True raises ValueError, matching pandas 3.0.

Other:
* DataFrame({'a': []}) defaults untyped empty sequences to float64
  like pandas' constructor (numpy's empty-array default), unlike
  Series([]) which stays object.
* reset_index derives the result columns dtype via pandas'
  Index.insert provenance instead of re-inferring from merged labels,
  and Series.reset_index resolves the value-column name before
  resetting (pandas' to_frame(name).reset_index() semantics).
* as_column routes stdlib datetime/timedelta elements through the
  pandas object path so mixed datetime+non-datetime lists raise
  MixedTypeError instead of silently coercing.
* Remove a stale workaround in test_groupby_apply_return_col_from_df
  and a stale conditional xfail in test_dataframe_assign_scalar, both
  now matching pandas exactly.
…ntainer identity, docs

- GroupBy.apply: drop the aggregate-length "transform-like" branch. Its
  total-row-count check could pass coincidentally (uneven shrink/expand,
  or per-group-length matches with a rewritten index) and then stamp the
  wrong rows with grouped_values' index. The GH8467 concat-with-keys
  branch already implements pandas' _concat_objects(not_indexed_same=True)
  exactly (keys repeated per actual chunk length, UDF-returned index kept
  as the inner level) and produces identical results for true transforms,
  so the heuristic branch is removed rather than re-gated on offsets.
- Remove the stale include_groups=True sentence from the apply docstring.
- fast_slow_proxy._transform_arg: rebuilt list subclasses receive an
  iterator constructor argument again (not a materialized list), and
  unchanged plain tuples now preserve identity so containers enclosing
  them keep theirs (a closed-over dict holding a tuple was still copied).
- Tests: _transform_arg identity preservation and rebuild propagation
  (lists/dicts/tuples/list subclasses), GroupBy.apply misaligned and
  fresh-index Series results vs pandas, and DataFrame constructor dtypes
  for empty iterator (float64) and empty range (int64, matching pandas'
  np.arange conversion - the merge keeps Iterator and drops range from
  the float64 coercion accordingly).
… columns in table path, proxy benchmarks

- Use names.pop() for the single-name columns-axis rename (review
  suggestion).
- Compare dict entries via items() in _transform_arg's unchanged-identity
  scan so mapping subclasses overriding __iter__/__getitem__ still work;
  regression test included.
- DataFrame.quantile now honors the cudf-specific columns argument in the
  method="table" path by filtering the frame up front (previously only
  the per-column path filtered); regression test covers both methods.
- Pin include_groups=False explicitly in the two new groupby.apply
  regression tests.
- Add benchmarks for _transform_arg container transformation (unchanged
  identity path and proxy-rebuild path for lists and dicts).
@galipremsagar
galipremsagar changed the base branch from main to release/26.08 July 17, 2026 01:37
@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test 5f92031

@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test 27c4949

@galipremsagar
galipremsagar requested a review from mroeschke July 17, 2026 13:51
rapids-bot Bot pushed a commit that referenced this pull request Jul 17, 2026
The latest-deps conda CI jobs started failing on every PR (e.g. [this run on #23272](https://github.com/rapidsai/cudf/actions/runs/29585048860)) with pyarrow 25.0.0 in the environment — feather tests/doctests (`pyarrow.feather` deprecated as of 24), pylibcudf quantiles (`SortOptions(null_placement=)` deprecated in 25), ORC tests (out-of-ns-range timestamps now raise `ArrowInvalid` instead of silently overflowing), and the narwhals suite.

cudf pins `pyarrow>=19.0.0,<24` (#22229) in `dependencies.yaml` for the conda, requirements, and pyproject outputs — but the hand-maintained conda recipes only declare `pyarrow>=19.0.0` with **no upper bound** (`conda/recipes/cudf/recipe.yaml` run dependency and `conda/recipes/pylibcudf/recipe.yaml` run constraint). The conda test environments don't list pyarrow directly (only the oldest-deps matrix pins `pyarrow==19.*`), so the env solve takes the bound from the built packages, and with the recipes unbounded the solver picked pyarrow 25.0.0. This is also how the narwhals job got pyarrow 25: its env installs the built cudf conda package in the same solve.

This mirrors the `dependencies.yaml` bound into both recipes, which constrains every conda test environment that installs the built packages.

Note: the wheel jobs were unaffected because the pip/pyproject metadata carries the `<24` bound. The remaining failure in the linked run (`conda-python-other-tests`) was a runner infra flake (`nvidia-smi`: "No devices were found") — retry only.

For whenever the pin is actually lifted (#22229): the test-suite adaptations needed for pyarrow 24/25 (feather→`pyarrow.ipc` migration, per-sort-key `null_placement`, ORC timestamp-range handling, narwhals deselects) were worked out and verified in [9a3a288](galipremsagar@9a3a288019) (previous head of this branch).

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

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

URL: #23319
@galipremsagar

Copy link
Copy Markdown
Contributor Author

/okay to test af4beb8

…ply-fixes

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

Copy link
Copy Markdown
Contributor Author

/okay to test b85de3c

@galipremsagar

Copy link
Copy Markdown
Contributor Author

/merge

@rapids-bot
rapids-bot Bot merged commit ae95c7b into NVIDIA:release/26.08 Jul 18, 2026
233 of 239 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in cuDF Python Jul 18, 2026
davidwendt pushed a commit to wjxiz1992/cudf that referenced this pull request Jul 21, 2026
The latest-deps conda CI jobs started failing on every PR (e.g. [this run on NVIDIA#23272](https://github.com/rapidsai/cudf/actions/runs/29585048860)) with pyarrow 25.0.0 in the environment — feather tests/doctests (`pyarrow.feather` deprecated as of 24), pylibcudf quantiles (`SortOptions(null_placement=)` deprecated in 25), ORC tests (out-of-ns-range timestamps now raise `ArrowInvalid` instead of silently overflowing), and the narwhals suite.

cudf pins `pyarrow>=19.0.0,<24` (NVIDIA#22229) in `dependencies.yaml` for the conda, requirements, and pyproject outputs — but the hand-maintained conda recipes only declare `pyarrow>=19.0.0` with **no upper bound** (`conda/recipes/cudf/recipe.yaml` run dependency and `conda/recipes/pylibcudf/recipe.yaml` run constraint). The conda test environments don't list pyarrow directly (only the oldest-deps matrix pins `pyarrow==19.*`), so the env solve takes the bound from the built packages, and with the recipes unbounded the solver picked pyarrow 25.0.0. This is also how the narwhals job got pyarrow 25: its env installs the built cudf conda package in the same solve.

This mirrors the `dependencies.yaml` bound into both recipes, which constrains every conda test environment that installs the built packages.

Note: the wheel jobs were unaffected because the pip/pyproject metadata carries the `<24` bound. The remaining failure in the linked run (`conda-python-other-tests`) was a runner infra flake (`nvidia-smi`: "No devices were found") — retry only.

For whenever the pin is actually lifted (NVIDIA#22229): the test-suite adaptations needed for pyarrow 24/25 (feather→`pyarrow.ipc` migration, per-sort-key `null_placement`, ORC timestamp-range handling, narwhals deselects) were worked out and verified in [9a3a288](galipremsagar@9a3a288019) (previous head of this branch).

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

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

URL: NVIDIA#23319
davidwendt pushed a commit to wjxiz1992/cudf that referenced this pull request Jul 21, 2026
…y-frame dtypes (NVIDIA#23272)

Running pandas' own test suite under `cudf.pandas`, `tests/groupby/test_apply.py` had 28 failing tests. This PR fixes 26 of them (137 tests: 135 pass; the 2 remaining are inherent and keep documented plugin entries) and removes the fixed xfail entries from the pandas-testing plugin.

### cudf.pandas: UDF closure side effects were silently discarded

`_transform_arg` rebuilt lists and dicts even when no element needed proxy conversion. The rebuilt container fails `_replace_closurevars`' identity check, so user functions were rebuilt around a *copy* of their closed-over mutable containers — a UDF like `lambda g: names.append(g.name)` appended into a throwaway copy on both the fast attempt and the pandas fallback, and the user's list stayed empty. The list/dict branches are now identity-preserving when unchanged, mirroring the existing object-ndarray branch.

### `GroupBy.apply` result assembly (pandas parity, all verified empirically on 3.0.3)

- All-None `DataFrameGroupBy` results return an empty frame keeping the value columns and dtypes (pandas GH9684/GH57775).
- Series results sharing an identical index stack into one row per group with columns given by the common index; a consistent Series name becomes the columns-axis name (GH6124). Series results with differing indexes concatenate lengthwise under the group keys (GH8467). This replaces row-count heuristics that mislabeled columns and mis-shaped results.
- Transform results (chunks indexed like their input) restore the original row order regardless of `sort`, like pandas' `_concat_objects`; the final `sort_index` is removed since group-keyed results are already emitted in sorted key order and pandas preserves the UDF's within-group row order (GH52444).
- `include_groups=True` raises `ValueError`, matching pandas 3.0.

### Supporting fixes

- `DataFrame({"a": []})` defaults untyped empty sequences to float64 like pandas' constructor (numpy's empty-array default); `Series([])` stays object.
- `reset_index` derives the result columns dtype via pandas' `Index.insert` provenance instead of re-inferring from the merged labels, and `Series.reset_index` resolves the value-column name before resetting (pandas' `to_frame(name).reset_index()` semantics).
- `as_column` routes stdlib `datetime`/`timedelta` elements through the pandas object path, so mixed datetime+non-datetime lists raise `MixedTypeError` instead of silently coercing.
- Removed a stale workaround in `test_groupby_apply_return_col_from_df` and a stale conditional xfail in `test_dataframe_assign_scalar` — both now match pandas exactly.

### Validation

- pandas-tests `tests/groupby/test_apply.py`: 28 failed → 135/137 pass (2 inherent, documented).
- pandas-tests reset_index/constructors/groupby neighbors: only pre-existing known failures.
- cuDF classic: groupby suites clean including under `NO_EXTERNAL_ONLY_APIS=1`; dataframe/series/reshape/indexes/input_output sweep (70k+ tests) green.
- `cudf.pandas` proxy unit tests pass.

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

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

URL: NVIDIA#23272
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 bug Something isn't working cudf.pandas Issues specific to cudf.pandas non-breaking Non-breaking change Python Affects Python cuDF API.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants