Skip to content

PERF: use Arrow native path in duration component calculation for Arrow-backed array - #63470

Merged
jbrockmendel merged 36 commits into
pandas-dev:mainfrom
fangchenli:perf/duration-components-arrow-native
Jun 19, 2026
Merged

jbrockmendel merged 36 commits into
pandas-dev:mainfrom
fangchenli:perf/duration-components-arrow-native

Conversation

@fangchenli

@fangchenli fangchenli commented Dec 23, 2025 •

Copy link
Copy Markdown
Member

Computes Series.dt duration components (days, seconds, microseconds, nanoseconds, components, ...) for ArrowDtype durations directly with PyArrow compute kernels, instead of converting to a NumPy TimedeltaArray and deriving each field from TimedeltaArray.components.

Accessor Before (ms) After (ms) Speedup
.dt.days 16096.7 207.7 77.5x
.dt.seconds 16100.2 155.8 103.3x
.dt.microseconds 16395.5 247.0 66.4x
.dt.nanoseconds 16317.9 251.2 65.0x
.dt.components 116180.1 531.5 218.6x

Methodology: n=10,000,000 random durations spanning ~±115 days, including negatives (so the after path runs the general floor-division branch, not the all-non-negative fast path). Best of 5 cold calls — the _dt_day_remainder cache is cleared before each call. Before is the pre-PR path (_to_timedeltaarray().components); after is the public Series.dt.<accessor>. On the before side, .dt.components costs ~7× a single field because the old implementation built the DataFrame one component at a time, each re-converting the array; on the after side it computes _dt_day_remainder once and reuses it across the sub-day fields (~2× vs. recomputing per field). Run on an M1 MacBook Pro — absolute numbers are machine-dependent, but the speedups should hold.

This comment was marked as spam.

@fangchenli
fangchenli marked this pull request as ready for review December 23, 2025 23:13
@fangchenli fangchenli added Performance Memory or execution speed performance Arrow pyarrow functionality labels Dec 24, 2025
@jbrockmendel

Copy link
Copy Markdown
Member

Could someone verify this improvement on a more powerful machine?

Sure. What script do you want to benchmark?

Comment thread pandas/core/arrays/arrow/array.py Outdated
Comment thread pandas/core/indexes/accessors.py Outdated
@jbrockmendel

jbrockmendel commented May 12, 2026 •

Copy link
Copy Markdown
Member

pls add a whatsnew for the perf improvement

also maybe the fix to setting index for .components

Comment thread pandas/core/arrays/arrow/array.py Outdated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@fangchenli
fangchenli requested a review from jbrockmendel June 4, 2026 16:29
Comment thread pandas/core/arrays/arrow/array.py
Comment thread pandas/tests/extension/test_arrow.py
fangchenli and others added 2 commits June 10, 2026 22:02
@fangchenli
fangchenli requested a review from jbrockmendel June 11, 2026 06:08
Comment thread pandas/tests/extension/test_arrow.py
fangchenli and others added 4 commits June 16, 2026 09:52
Series.dt.microseconds on a coarser duration unit (e.g. duration[ms])
short-circuited to 0 instead of scaling the sub-second portion up to
microseconds, so duration[ms].dt.microseconds returned 0 rather than the
total microseconds. Compute it by scaling the sub-second remainder to
microseconds in either direction, and extend
test_dt_timedelta_components_different_units to check the direct
.dt.[days, seconds, microseconds, nanoseconds] accessors against the
NumPy-backed result at every unit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread pandas/core/arrays/arrow/array.py
fangchenli and others added 2 commits June 17, 2026 08:44
…mainder

sort() replaces _pa_array in place, bypassing __setitem__, so the cached
_dt_day_remainder (and any other cache_readonly) must be cleared there too,
or duration components would reflect the pre-sort order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jbrockmendel

Copy link
Copy Markdown
Member

LGTM ping on green

@jbrockmendel
jbrockmendel merged commit 644bf59 into pandas-dev:main Jun 19, 2026
46 checks passed
@jbrockmendel

Copy link
Copy Markdown
Member

Thanks @fangchenli

hamdanal pushed a commit to hamdanal/pandas that referenced this pull request Jun 27, 2026
…ow-backed array (pandas-dev#63470)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +208 to +227
def floor_div_int64(
arr: pa.Array | pa.ChunkedArray, divisor: int
) -> pa.Array | pa.ChunkedArray:
"""
Floor division for an int64 (chunked) array, handling negatives correctly.

PyArrow's ``pc.divide`` truncates toward zero, but we need floor division
(toward negative infinity) to match pandas behavior for negative durations.
Uses a fast path for all-non-negative arrays (the common case).
"""
min_scalar = pc.min(arr)
if not min_scalar.is_valid or min_scalar.as_py() >= 0:
# Fast path: truncation equals floor when all values are non-negative
return pc.divide(arr, divisor)
# Truncated division (toward zero) and remainder with the dividend's sign
trunc_div = pc.divide(arr, divisor)
remainder = pc.subtract(arr, pc.multiply(trunc_div, divisor))
# floor(a/b) = trunc(a/b) - 1 when a < 0 and remainder != 0
needs_adjustment = pc.and_(pc.less(arr, 0), pc.not_equal(remainder, 0))
return pc.subtract(trunc_div, pc.if_else(needs_adjustment, 1, 0))

@rok rok Jul 28, 2026 •

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.

Why not use pc.floor_temporal instead @fangchenli ?

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.

pc.floor_temporal has no duration kernel and is much slower in non-negative cases. But it would be nice if Arrow could handle all those.

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.

Duration work is in progress and I expect it to be in the next release apache/arrow#50675.

What do you mean by non-negative cases? That sounds like something we should fix in arrow.

Also please ping me if there are other missing functionalities in pyarrow kernels, especially temporal!

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 merged apache/arrow#50675 and you can expect it in the next release.

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.

Thanks! I'll do more testing to narrow down the slow cases I observed earlier.

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

Labels

Arrow pyarrow functionality Performance Memory or execution speed performance Timedelta Timedelta data type

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: .dt.seconds Discrepancy Between NumPy and PyArrow Timedelta Dtypes

4 participants