Skip to content

Commit b08a22b

Browse files
authored
Merge branch 'main' into fix-io-benchmarks-12739
2 parents 854bf4e + acfb4e2 commit b08a22b

6 files changed

Lines changed: 116 additions & 71 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,6 @@ compile_commands.json
187187
# pytest artifacts
188188
rmm_log.txt
189189
python/cudf/cudf_pandas_tests/data/rmm_log.txt
190+
191+
# Quent traces
192+
logs/*.ndjson

python/cudf/cudf/core/index.py

Lines changed: 68 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
from cudf.core.dataframe import DataFrame
8686
from cudf.core.multiindex import MultiIndex
8787
from cudf.core.series import Series
88-
from cudf.core.tools.datetimes import DateOffset, MonthEnd, YearEnd
88+
from cudf.core.tools.datetimes import DateOffset
8989

9090

9191
def ensure_index(index_like: Any, nan_as_null=no_default) -> Index:
@@ -2025,9 +2025,7 @@ def __repr__(self) -> str:
20252025
and isinstance(self, DatetimeIndex)
20262026
and self._freq is not None
20272027
):
2028-
keywords.append(
2029-
f"freq={self._freq._maybe_as_fast_pandas_offset().freqstr!r}"
2030-
)
2028+
keywords.append(f"freq={self._freq.freqstr!r}")
20312029
joined_keywords = ", ".join(keywords)
20322030
lines.append(f"{prior_to_dtype} {joined_keywords})")
20332031
return "\n".join(lines)
@@ -3381,11 +3379,8 @@ def __init__(
33813379
if yearfirst is not False:
33823380
raise NotImplementedError("yearfirst == True is not yet supported")
33833381

3384-
if freq is None:
3385-
if isinstance(data, type(self)):
3386-
freq = data.freq
3387-
if was_pd_index and data.freq is not None:
3388-
freq = data.freq.freqstr
3382+
if freq is None and isinstance(data, (type(self), pd.DatetimeIndex)):
3383+
freq = data.freq
33893384

33903385
name = _getdefault_name(data, name=name)
33913386

@@ -3439,22 +3434,32 @@ def __init__(
34393434
@_performance_tracking
34403435
def serialize(self):
34413436
header, frames = super().serialize()
3442-
if self.freq is not None:
3437+
if self._freq is None:
3438+
header["freq"] = None
3439+
elif type(self._freq) is pd.DateOffset:
3440+
# generic offsets have no parseable freqstr; store the kwds
3441+
# plus n/normalize, which .kwds omits
34433442
header["freq"] = {
3444-
"kwds": self.freq.kwds,
3443+
"kwds": self._freq.kwds,
3444+
"n": self._freq.n,
3445+
"normalize": self._freq.normalize,
34453446
}
34463447
else:
3447-
header["freq"] = None
3448+
header["freq"] = self._freq.freqstr
34483449
return header, frames
34493450

34503451
@classmethod
34513452
@_performance_tracking
34523453
def deserialize(cls, header, frames):
34533454
obj = super().deserialize(header, frames)
3454-
if (header_payload := header.get("freq")) is not None:
3455-
freq = cudf.DateOffset(**header_payload["kwds"])
3455+
if isinstance(header_payload := header.get("freq"), dict):
3456+
freq = pd.DateOffset(
3457+
n=header_payload.get("n", 1),
3458+
normalize=header_payload.get("normalize", False),
3459+
**header_payload["kwds"],
3460+
)
34563461
else:
3457-
freq = None
3462+
freq = header_payload
34583463

34593464
obj._freq = _validate_freq(freq)
34603465
return obj
@@ -3522,7 +3527,7 @@ def find_label_range(self, loc: slice) -> slice:
35223527
@_performance_tracking
35233528
def copy(self, name=None, deep=False):
35243529
idx_copy = super().copy(name=name, deep=deep)
3525-
idx_copy._freq = _validate_freq(self._freq)
3530+
idx_copy._freq = self._freq
35263531
return idx_copy
35273532

35283533
def as_unit(self, unit: str, round_ok: bool = True) -> Self:
@@ -3578,8 +3583,8 @@ def asi8(self) -> cupy.ndarray:
35783583
return self._column.astype(np.dtype(np.int64)).values
35793584

35803585
@property
3581-
def inferred_freq(self) -> DateOffset | MonthEnd | YearEnd | None:
3582-
if self._freq:
3586+
def inferred_freq(self):
3587+
if self._freq is not None:
35833588
return self._freq
35843589

35853590
plc_col = self._column.plc_column
@@ -3635,7 +3640,12 @@ def inferred_freq(self) -> DateOffset | MonthEnd | YearEnd | None:
36353640
if (c := getattr(cmps, component)) != 0:
36363641
kwds[component] = c
36373642

3638-
return cudf.DateOffset(**kwds)
3643+
# not pd.DateOffset(**kwds): a generic pd.DateOffset never
3644+
# compares equal to the fast offsets pandas infers (e.g.
3645+
# pd.DateOffset(days=1) != pd.offsets.Day()) and its freqstr
3646+
# is not parseable, so single-unit offsets must be converted
3647+
# to their fast pandas equivalents
3648+
return cudf.DateOffset(**kwds)._maybe_as_fast_pandas_offset()
36393649

36403650
# maximum unique count supported is months with 4 unique lengths
36413651
# bail above that for now
@@ -3644,19 +3654,23 @@ def inferred_freq(self) -> DateOffset | MonthEnd | YearEnd | None:
36443654
if all(x in self.YEARLY_PERIODS for x in uniques_host):
36453655
# Could be year end or could be an anchored year end
36463656
if self.is_year_end.all():
3647-
return cudf.DateOffset._from_freqstr("YE-DEC")
3657+
return pd.tseries.frequencies.to_offset("YE-DEC")
36483658
else:
36493659
raise NotImplementedError()
36503660
elif all(x in self.MONTHLY_PERIODS for x in uniques_host):
36513661
if self.is_month_end.all():
3652-
return cudf.DateOffset._from_freqstr("ME")
3662+
return pd.tseries.frequencies.to_offset("ME")
36533663
else:
36543664
raise NotImplementedError
36553665
else:
36563666
return None
36573667
return None
36583668

36593669
def _get_slice_frequency(self, slc=None):
3670+
if self._freq is None:
3671+
# match pandas: slicing an index without a cached freq
3672+
# produces a result with freq=None
3673+
return None
36603674
if slc.step in (1, None):
36613675
# no change in freq
36623676
return self._freq
@@ -3665,12 +3679,7 @@ def _get_slice_frequency(self, slc=None):
36653679
else:
36663680
if slc:
36673681
# fastpath: dont introspect
3668-
# Multiply the pandas offset directly (pd.Timedelta(offset)
3669-
# fails for calendar-based offsets like Day in pandas 3).
3670-
new_freq = slc.step * self._freq._maybe_as_fast_pandas_offset()
3671-
return cudf.DateOffset._from_freqstr(
3672-
pd.tseries.frequencies.to_offset(new_freq).freqstr
3673-
)
3682+
return slc.step * self._freq
36743683
else:
36753684
return self.inferred_freq
36763685

@@ -3680,37 +3689,32 @@ def _validate_freq_against_data(self, freq) -> None:
36803689
unique_vals = cudf.Series._from_column(
36813690
self.to_series().diff()._column.unique()
36823691
)
3683-
if freq == cudf.DateOffset(months=1):
3692+
if freq == pd.DateOffset(months=1):
36843693
possible = pd.Series(list(self.MONTHLY_PERIODS | {pd.NaT}))
36853694
if unique_vals.isin(possible).sum() != len(unique_vals):
36863695
raise ValueError(
36873696
f"Inferred frequency from passed values does not "
3688-
f"conform to passed frequency "
3689-
f"{freq._maybe_as_fast_pandas_offset().freqstr}"
3697+
f"conform to passed frequency {freq.freqstr}"
36903698
)
3691-
elif freq == cudf.DateOffset(years=1):
3699+
elif freq == pd.DateOffset(years=1):
36923700
possible = pd.Series(list(self.YEARLY_PERIODS | {pd.NaT}))
36933701
if unique_vals.isin(possible).sum() != len(unique_vals):
36943702
raise ValueError(
36953703
f"Inferred frequency from passed values does not "
3696-
f"conform to passed frequency "
3697-
f"{freq._maybe_as_fast_pandas_offset().freqstr}"
3704+
f"conform to passed frequency {freq.freqstr}"
36983705
)
36993706
else:
37003707
if len(unique_vals) > 2 or (
3701-
len(unique_vals) == 2
3702-
and unique_vals[1].value
3703-
!= freq._maybe_as_fast_pandas_offset().nanos
3708+
len(unique_vals) == 2 and unique_vals[1].value != freq.nanos
37043709
):
37053710
raise ValueError(
37063711
f"Inferred frequency from passed values does not "
3707-
f"conform to passed frequency "
3708-
f"{freq._maybe_as_fast_pandas_offset().freqstr}"
3712+
f"conform to passed frequency {freq.freqstr}"
37093713
)
37103714

37113715
@property
3712-
def freq(self) -> DateOffset | None:
3713-
return self._freq # type: ignore[return-value] # (validated setter stores DateOffset-compatible value)
3716+
def freq(self) -> pd.tseries.offsets.BaseOffset | None:
3717+
return self._freq
37143718

37153719
@freq.setter
37163720
def freq(self, value) -> None:
@@ -4275,7 +4279,7 @@ def to_pandas(
42754279
inferred = result.inferred_freq
42764280
if inferred is None:
42774281
try:
4278-
result.freq = self._freq._maybe_as_fast_pandas_offset()
4282+
result.freq = self._freq
42794283
except ValueError:
42804284
pass
42814285
else:
@@ -5779,11 +5783,28 @@ def _get_nearest_indexer(
57795783
return indexer
57805784

57815785

5782-
def _validate_freq(freq: Any) -> DateOffset | MonthEnd | YearEnd | None:
5786+
def _validate_freq(freq: Any) -> pd.tseries.offsets.BaseOffset | None:
5787+
"""Normalize ``freq`` to a canonical pandas offset.
5788+
5789+
Accepts freq strings, pandas offsets and cudf's own offset classes,
5790+
rejecting frequencies that cudf cannot represent.
5791+
"""
5792+
from cudf.core.tools.datetimes import DateOffset, MonthEnd, YearEnd
5793+
5794+
if freq is None:
5795+
return None
5796+
if isinstance(freq, (DateOffset, MonthEnd, YearEnd)):
5797+
return freq._maybe_as_fast_pandas_offset()
57835798
if isinstance(freq, str):
5784-
return cudf.DateOffset._from_freqstr(freq)
5785-
elif freq is None:
5786-
return freq
5787-
elif freq is not None and not isinstance(freq, cudf.DateOffset):
5799+
# cudf supports a subset of pandas frequency strings; parse with
5800+
# cudf's DateOffset (which raises for unsupported ones) and store
5801+
# the equivalent pandas offset.
5802+
return DateOffset._from_freqstr(freq)._maybe_as_fast_pandas_offset()
5803+
if not isinstance(freq, pd.tseries.offsets.BaseOffset):
57885804
raise ValueError(f"Invalid frequency: {freq}")
5789-
return cast("cudf.DateOffset", freq)
5805+
# gate pandas offsets on the same supported subset
5806+
if type(freq) is pd.DateOffset:
5807+
DateOffset(**freq.kwds)
5808+
else:
5809+
DateOffset._from_freqstr(freq.freqstr)
5810+
return freq

python/cudf/cudf/pandas/_wrappers/pandas.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2789,6 +2789,10 @@ def _unpickle_offset_obj(pickled_args):
27892789
# same reducer/unpickler can be used for Timedelta:
27902790
copyreg.dispatch_table[Timedelta] = _reduce_proxied_td_obj
27912791
copyreg.dispatch_table[pd.Timedelta] = _reduce_obj
2792+
# Period has no fast (cudf) representation; pickle the wrapped/real pandas
2793+
# object directly (e.g. matplotlib figures store pandas.Period x-data).
2794+
copyreg.dispatch_table[Period] = _reduce_proxied_td_obj
2795+
copyreg.dispatch_table[pd.Period] = _reduce_obj
27922796

27932797
# TODO: Need to find a way to unpickle cross-version(old) pickled objects.
27942798
# Register custom reducer/unpickler functions for pandas objects
@@ -2820,6 +2824,18 @@ def _unpickle_offset_obj(pickled_args):
28202824
)
28212825

28222826
copyreg.dispatch_table[pd.DateOffset] = _reduce_offset_obj
2827+
# Concrete pandas offsets (Day, Week, Minute, ...) are stored e.g. as the
2828+
# ``freq`` of matplotlib's date converters. The module accelerator makes the
2829+
# offset module attributes resolve to proxies, so pickle's class-identity
2830+
# check fails; reduce each via its real ``__reduce__`` (with the accelerator
2831+
# disabled). ``DateOffset`` keeps its dedicated reducer above.
2832+
for _offset_cls in vars(pd.tseries.offsets).values():
2833+
if (
2834+
isinstance(_offset_cls, type)
2835+
and issubclass(_offset_cls, pd.tseries.offsets.BaseOffset)
2836+
and _offset_cls is not pd.DateOffset
2837+
):
2838+
copyreg.dispatch_table.setdefault(_offset_cls, _reduce_obj)
28232839

28242840

28252841
def _unpickle_NaT():

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

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2296,19 +2296,11 @@ def pytest_unconfigure(config):
22962296
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_across_dst[ns]": "AssertionError: Series.index are different",
22972297
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_across_dst[s]": "AssertionError: Series.index are different",
22982298
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_across_dst[us]": "AssertionError: Series.index are different",
2299-
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_int[ms]": "TypeError: unsupported operand type(s) for *: 'int' and 'DateOffset'",
2300-
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_int[ns]": "TypeError: unsupported operand type(s) for *: 'int' and 'DateOffset'",
2301-
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_int[s]": "TypeError: unsupported operand type(s) for *: 'int' and 'DateOffset'",
2302-
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_int[us]": "TypeError: unsupported operand type(s) for *: 'int' and 'DateOffset'",
2303-
"tests/indexes/datetimes/methods/test_to_period.py::TestToPeriod::test_to_period_tz[US/Eastern]": "TypeError: Argument 'freq' has incorrect type (expected str, got DateOffset)",
2304-
"tests/indexes/datetimes/methods/test_to_period.py::TestToPeriod::test_to_period_tz_utc_offset_consistency[Etc/GMT+1]": "TypeError: Argument 'freq' has incorrect type (expected str, got DateOffset)",
2305-
"tests/indexes/datetimes/methods/test_to_period.py::TestToPeriod::test_to_period_tz_utc_offset_consistency[Etc/GMT-1]": "TypeError: Argument 'freq' has incorrect type (expected str, got DateOffset)",
23062299
"tests/indexes/datetimes/methods/test_tz_convert.py::TestTZConvert::test_dti_tz_convert_dst": "ValueError: Inferred frequency None from passed values does not conform to passed frequency h",
23072300
"tests/indexes/datetimes/methods/test_tz_localize.py::TestTZLocalize::test_dti_tz_localize[]": "Failed: DID NOT RAISE <class 'ValueError'>",
23082301
"tests/indexes/datetimes/methods/test_tz_localize.py::TestTZLocalize::test_dti_tz_localize_nonexistent_raise_coerce": "Failed: DID NOT RAISE <class 'ValueError'>",
23092302
"tests/indexes/datetimes/methods/test_tz_localize.py::TestTZLocalize::test_dti_tz_localize_utc_conversion[tz2]": "Failed: DID NOT RAISE <class 'ValueError'>",
23102303
"tests/indexes/datetimes/methods/test_unique.py::test_index_unique2": "TODO: Add a reason for failure",
2311-
"tests/indexes/datetimes/test_arithmetic.py::TestDatetimeIndexArithmetic::test_add_dti_day": "TypeError: unsupported operand type(s) for +: 'DatetimeArray' and 'DateOffset'",
23122304
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_coverage": "TODO: Add a reason for failure",
23132305
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_datetime64_tzformat[W-SUN]": "TODO: Add a reason for failure",
23142306
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_dtype_tz_mismatch_raises": "Failed: DID NOT RAISE <class 'ValueError'>",
@@ -3220,20 +3212,6 @@ def pytest_unconfigure(config):
32203212
"tests/plotting/test_datetimelike.py::TestTSPlot::test_from_resampling_area_line_mixed_high_to_low[area-line]": "ValueError: You must pass a freq argument as current index has none.",
32213213
"tests/plotting/test_datetimelike.py::TestTSPlot::test_from_resampling_area_line_mixed_high_to_low[line-area]": "ValueError: You must pass a freq argument as current index has none.",
32223214
"tests/plotting/test_datetimelike.py::TestTSPlot::test_from_weekly_resampling": "ValueError: You must pass a freq argument as current index has none.",
3223-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_datetime_frame[D]": "TODO: Add a reason for failure",
3224-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_datetime_frame[W]": "TODO: Add a reason for failure",
3225-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_datetime_frame[h]": "TODO: Add a reason for failure",
3226-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_datetime_frame[min]": "TODO: Add a reason for failure",
3227-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_datetime_frame[s]": "TODO: Add a reason for failure",
3228-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_inferred_freq[D]": "TODO: Add a reason for failure",
3229-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_inferred_freq[ME]": "TODO: Add a reason for failure",
3230-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_inferred_freq[YE]": "TODO: Add a reason for failure",
3231-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_pickle_fig[DataFrame-idx0]": "_pickle.PicklingError: Can't pickle <class 'pandas.Period'>: it's not the same object as pandas.Period",
3232-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_pickle_fig[DataFrame-idx1]": "_pickle.PicklingError: Can't pickle <class 'pandas.Period'>: it's not the same object as pandas.Period",
3233-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_pickle_fig[DataFrame-idx3]": "_pickle.PicklingError: Can't pickle <class 'pandas.Period'>: it's not the same object as pandas.Period",
3234-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_pickle_fig[Series-idx0]": "_pickle.PicklingError: Can't pickle <class 'pandas.Period'>: it's not the same object as pandas.Period",
3235-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_pickle_fig[Series-idx1]": "_pickle.PicklingError: Can't pickle <class 'pandas.Period'>: it's not the same object as pandas.Period",
3236-
"tests/plotting/test_datetimelike.py::TestTSPlot::test_pickle_fig[Series-idx3]": "_pickle.PicklingError: Can't pickle <class 'pandas.Period'>: it's not the same object as pandas.Period",
32373215
"tests/plotting/test_datetimelike.py::TestTSPlot::test_to_weekly_resampling": "AssertionError: assert <Week: weekday=4> == <DateOffset: weeks=1>",
32383216
"tests/reductions/test_reductions.py::TestDatetime64SeriesReductions::test_minmax_nat_series[nat_ser0]": "TODO: Add a reason for failure",
32393217
"tests/reductions/test_reductions.py::TestDatetime64SeriesReductions::test_minmax_nat_series[nat_ser1]": "TODO: Add a reason for failure",

python/cudf_polars/cudf_polars/dsl/expressions/boolean.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def __init__(
9999
BooleanFunction.Name.All,
100100
BooleanFunction.Name.Any,
101101
BooleanFunction.Name.IsDuplicated,
102+
BooleanFunction.Name.IsEmpty,
102103
BooleanFunction.Name.IsFirstDistinct,
103104
BooleanFunction.Name.IsLastDistinct,
104105
BooleanFunction.Name.IsSorted,
@@ -107,7 +108,6 @@ def __init__(
107108
if self.name in {
108109
BooleanFunction.Name.HasNulls,
109110
BooleanFunction.Name.IsClose,
110-
BooleanFunction.Name.IsEmpty,
111111
}:
112112
raise NotImplementedError(
113113
f"Boolean function {self.name}"
@@ -192,6 +192,19 @@ def do_evaluate(
192192
self, df: DataFrame, *, context: ExecutionContext = ExecutionContext.FRAME
193193
) -> Column:
194194
"""Evaluate this expression given a dataframe for context."""
195+
if self.name is BooleanFunction.Name.IsEmpty:
196+
(child,) = self.children
197+
column = child.evaluate(df, context=context)
198+
return Column(
199+
plc.Column.from_scalar(
200+
plc.Scalar.from_py(
201+
column.size == 0, self.dtype.plc_type, stream=df.stream
202+
),
203+
1,
204+
stream=df.stream,
205+
),
206+
dtype=self.dtype,
207+
)
195208
if self.name in (
196209
BooleanFunction.Name.IsFinite,
197210
BooleanFunction.Name.IsInfinite,

0 commit comments

Comments
 (0)