Skip to content

BUG/PERF: Series.combine passes Series on duplicate index labels - #67446

Open
rhshadrach wants to merge 6 commits into
pandas-dev:mainfrom
rhshadrach:perf_series_combine
Open

BUG/PERF: Series.combine passes Series on duplicate index labels#67446
rhshadrach wants to merge 6 commits into
pandas-dev:mainfrom
rhshadrach:perf_series_combine

Conversation

@rhshadrach

@rhshadrach rhshadrach commented Aug 30, 2026

Copy link
Copy Markdown
Member

Generated by Claude Fable 5. Reviewed and refined by me.

When Series.combine gets an index with duplicate labels, it starts passing Series to the UDF.

a = pd.Series([1, 2, 3], index=["a", "a", "b"])
b = pd.Series([10, 20], index=["a", "b"])

def func(x, y):
    print(f'x={type(x)}, y={type(y)}')
    return x + y

a.combine(b, func)
# x=<class 'pandas.Series'>, y=<class 'numpy.int64'>
# x=<class 'pandas.Series'>, y=<class 'numpy.int64'>
# x=<class 'numpy.int64'>, y=<class 'numpy.int64'>

The docstring for func states:

Function that takes two scalars as inputs and returns an element.

By moving to this implementation, we also have significant perf improvements.

┌─────────────────────────────┬────────┬────────┐
│            case             │  main  │ branch │
├─────────────────────────────┼────────┼────────┤
│ n=3                         │ 100 µs │ 113 µs │
├─────────────────────────────┼────────┼────────┤
│ n=100                       │ 561 µs │ 242 µs │
├─────────────────────────────┼────────┼────────┤
│ 100k half-overlap           │ 392 ms │ 45 ms  │
├─────────────────────────────┼────────┼────────┤
│ 100k identical index        │ 177 ms │ 26 ms  │
├─────────────────────────────┼────────┼────────┤
│ 100k with one duplicate     │ 3.2 s  │ 76 ms  │
├─────────────────────────────┼────────┼────────┤
│ 100k, heavy dups both sides │ 33.3 s │ 77 ms  │
└─────────────────────────────┴────────┴────────┘

@rhshadrach rhshadrach added Bug Performance Memory or execution speed performance combine/combine_first/update NDFrame.combine, combine_first, update labels Aug 30, 2026
@rhshadrach rhshadrach added this to the 3.1 milestone Aug 30, 2026

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

Does this change any of these cases and are the changes intentional?

left = pd.Series([1, 2], index=pd.date_range("2020", periods=2))
right = pd.Series([10, 20], index=pd.date_range("2020", periods=2, tz="UTC"))
left.combine(right, lambda x, y: x + y, fill_value=0)

mi1 = pd.MultiIndex.from_tuples([("a", 1), ("b", 2)], names=["x", "y"])
mi2 = pd.MultiIndex.from_tuples([("a", 1), ("c", 3)], names=["p", "q"])
left = pd.Series([1, 2], index=mi1)
right = pd.Series([10, 20], index=mi2)
left.combine(right, lambda x, y: x + y, fill_value=0)

left = pd.Series([1, 2], index=mi1)
right = pd.Series([10, 20], index=["a", "b"])
left.combine(right, lambda x, y: x + y)

@rhshadrach

rhshadrach commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

Thanks @jbrockmendel. Series.combine has always followed union rather than join semantics, so I think the fact that each of your examples raise can be considered a bug in this PR as it was implemented. I believe the union semantics also dictate how duplicates should be treated: pairwise and not cartesian.

If I'm missing something and we feel the duplicate case is ambiguous, we could instead raise. That could technically be breaking behavior. No one has complained about duplicate behavior on the issue tracker at all and I'm wondering if that's due in part to this method not seeing much use. But there could be someone out there using today's dups behavior as-is. While implementing duplicate-handling behavior is 100% defensible as a bugfix here, raising a ValueError is still defensible but I think less so.

Benchmarks in OP have been updated.

Comment thread pandas/tests/indexes/test_indexing.py Outdated
tm.assert_numpy_array_equal(result, expected)


def test_pairwise_indexer():

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.

Test class for this?

- Bug in :meth:`DataFrame.stack` raising a bare ``AssertionError`` or an ``IndexError`` when ``level`` contained duplicate entries, including duplicates produced by resolving level names or negative level numbers; it now raises an informative ``ValueError`` (:issue:`66588`)
- Bug in :meth:`DataFrame.unstack` and :meth:`Series.unstack` with ``sort=False`` placing values under the wrong row labels, or collapsing distinct index combinations into a single row (:issue:`62816`)
- Bug in :meth:`Index.union` where the result could be unsorted when both inputs were monotonic increasing but disjoint, when ``sort`` was not ``False`` (:issue:`54646`)
- Fixed bug in :meth:`Series.sort_values` where ``ignore_index=True`` had no effect on an already-sorted Series (:issue:`65833`)

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.

was this deleted on purpose?

left = Series([1, 2, 3], index=["a", "a", "b"])
right = Series([10, 20], index=["a", "b"])
result = left.combine(right, lambda x, y: x + y, fill_value=0)
expected = Series([11, 2, 23], index=["a", "a", "b"])

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.

is it obvious that this should be [11, 2, 23] and not [11, 12, 23]?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is the join-vs-union behavior I alluded to in #67446 (comment). From what I can tell, the history has always intended it to be a union here, and combine certainly doesn't follow pandas join-semantics in any cases. That makes it obvious to me it should be [11, 2, 23], but I can't say I feel 100% confident that maybe some of this wasn't purposefully decided.

Assuming we're still good with union-behavior, I'll add a comment here.

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.

ok, thanks for taking a look

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug combine/combine_first/update NDFrame.combine, combine_first, update Performance Memory or execution speed performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants