Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 52 additions & 49 deletions python/cudf/cudf/core/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
from cudf.core.dataframe import DataFrame
from cudf.core.multiindex import MultiIndex
from cudf.core.series import Series
from cudf.core.tools.datetimes import DateOffset, MonthEnd, YearEnd
from cudf.core.tools.datetimes import DateOffset


def ensure_index(index_like: Any, nan_as_null=no_default) -> Index:
Expand Down Expand Up @@ -2025,9 +2025,7 @@ def __repr__(self) -> str:
and isinstance(self, DatetimeIndex)
and self._freq is not None
):
keywords.append(
f"freq={self._freq._maybe_as_fast_pandas_offset().freqstr!r}"
)
keywords.append(f"freq={self._freq.freqstr!r}")
joined_keywords = ", ".join(keywords)
lines.append(f"{prior_to_dtype} {joined_keywords})")
return "\n".join(lines)
Expand Down Expand Up @@ -3381,11 +3379,8 @@ def __init__(
if yearfirst is not False:
raise NotImplementedError("yearfirst == True is not yet supported")

if freq is None:
if isinstance(data, type(self)):
freq = data.freq
if was_pd_index and data.freq is not None:
freq = data.freq.freqstr
if freq is None and isinstance(data, (type(self), pd.DatetimeIndex)):
freq = data.freq

name = _getdefault_name(data, name=name)

Expand Down Expand Up @@ -3428,22 +3423,23 @@ def __init__(
@_performance_tracking
def serialize(self):
header, frames = super().serialize()
if self.freq is not None:
header["freq"] = {
"kwds": self.freq.kwds,
}
else:
if self._freq is None:
header["freq"] = None
elif type(self._freq) is pd.DateOffset:
# generic offsets have no parseable freqstr; store the kwds
header["freq"] = {"kwds": self._freq.kwds}
else:
header["freq"] = self._freq.freqstr
return header, frames

@classmethod
@_performance_tracking
def deserialize(cls, header, frames):
obj = super().deserialize(header, frames)
if (header_payload := header.get("freq")) is not None:
freq = cudf.DateOffset(**header_payload["kwds"])
if isinstance(header_payload := header.get("freq"), dict):
freq = pd.DateOffset(**header_payload["kwds"])
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
else:
freq = None
freq = header_payload

obj._freq = _validate_freq(freq)
return obj
Expand Down Expand Up @@ -3511,7 +3507,7 @@ def find_label_range(self, loc: slice) -> slice:
@_performance_tracking
def copy(self, name=None, deep=False):
idx_copy = super().copy(name=name, deep=deep)
idx_copy._freq = _validate_freq(self._freq)
idx_copy._freq = self._freq
return idx_copy

def as_unit(self, unit: str, round_ok: bool = True) -> Self:
Expand Down Expand Up @@ -3567,8 +3563,8 @@ def asi8(self) -> cupy.ndarray:
return self._column.astype(np.dtype(np.int64)).values

@property
def inferred_freq(self) -> DateOffset | MonthEnd | YearEnd | None:
if self._freq:
def inferred_freq(self):
if self._freq is not None:
return self._freq

plc_col = self._column.plc_column
Expand Down Expand Up @@ -3624,7 +3620,7 @@ def inferred_freq(self) -> DateOffset | MonthEnd | YearEnd | None:
if (c := getattr(cmps, component)) != 0:
kwds[component] = c

return cudf.DateOffset(**kwds)
return cudf.DateOffset(**kwds)._maybe_as_fast_pandas_offset()

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.

Are we at a stage where we could use pd.DateOffset(**kwds) directly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not quite — a generic pd.DateOffset(days=1) never compares equal to the fast offsets pandas infers (pd.DateOffset(days=1) != pd.offsets.Day()) and its freqstr is not parseable, which would break freq comparisons and the freq round-trip through _validate_freq/serialization. to_offset(Timedelta(...)) is not a substitute either, since it yields <24 * Hours> for a daily delta instead of <Day>. So single-unit offsets still need converting to their fast pandas equivalents via _maybe_as_fast_pandas_offset; I added a comment at the call site in d791fe0 explaining this.


# maximum unique count supported is months with 4 unique lengths
# bail above that for now
Expand All @@ -3633,12 +3629,12 @@ def inferred_freq(self) -> DateOffset | MonthEnd | YearEnd | None:
if all(x in self.YEARLY_PERIODS for x in uniques_host):
# Could be year end or could be an anchored year end
if self.is_year_end.all():
return cudf.DateOffset._from_freqstr("YE-DEC")
return pd.tseries.frequencies.to_offset("YE-DEC")
else:
raise NotImplementedError()
elif all(x in self.MONTHLY_PERIODS for x in uniques_host):
if self.is_month_end.all():
return cudf.DateOffset._from_freqstr("ME")
return pd.tseries.frequencies.to_offset("ME")
else:
raise NotImplementedError
else:
Expand All @@ -3654,12 +3650,7 @@ def _get_slice_frequency(self, slc=None):
else:
if slc:
# fastpath: dont introspect
# Multiply the pandas offset directly (pd.Timedelta(offset)
# fails for calendar-based offsets like Day in pandas 3).
new_freq = slc.step * self._freq._maybe_as_fast_pandas_offset()
return cudf.DateOffset._from_freqstr(
pd.tseries.frequencies.to_offset(new_freq).freqstr
)
return slc.step * self._freq
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
else:
return self.inferred_freq

Expand All @@ -3669,37 +3660,32 @@ def _validate_freq_against_data(self, freq) -> None:
unique_vals = cudf.Series._from_column(
self.to_series().diff()._column.unique()
)
if freq == cudf.DateOffset(months=1):
if freq == pd.DateOffset(months=1):
possible = pd.Series(list(self.MONTHLY_PERIODS | {pd.NaT}))
if unique_vals.isin(possible).sum() != len(unique_vals):
raise ValueError(
f"Inferred frequency from passed values does not "
f"conform to passed frequency "
f"{freq._maybe_as_fast_pandas_offset().freqstr}"
f"conform to passed frequency {freq.freqstr}"
)
elif freq == cudf.DateOffset(years=1):
elif freq == pd.DateOffset(years=1):
possible = pd.Series(list(self.YEARLY_PERIODS | {pd.NaT}))
if unique_vals.isin(possible).sum() != len(unique_vals):
raise ValueError(
f"Inferred frequency from passed values does not "
f"conform to passed frequency "
f"{freq._maybe_as_fast_pandas_offset().freqstr}"
f"conform to passed frequency {freq.freqstr}"
)
else:
if len(unique_vals) > 2 or (
len(unique_vals) == 2
and unique_vals[1].value
!= freq._maybe_as_fast_pandas_offset().nanos
len(unique_vals) == 2 and unique_vals[1].value != freq.nanos
):
raise ValueError(
f"Inferred frequency from passed values does not "
f"conform to passed frequency "
f"{freq._maybe_as_fast_pandas_offset().freqstr}"
f"conform to passed frequency {freq.freqstr}"
)

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

@freq.setter
def freq(self, value) -> None:
Expand Down Expand Up @@ -4264,7 +4250,7 @@ def to_pandas(
inferred = result.inferred_freq
if inferred is None:
try:
result.freq = self._freq._maybe_as_fast_pandas_offset()
result.freq = self._freq
except ValueError:
pass
else:
Expand Down Expand Up @@ -5768,11 +5754,28 @@ def _get_nearest_indexer(
return indexer


def _validate_freq(freq: Any) -> DateOffset | MonthEnd | YearEnd | None:
def _validate_freq(freq: Any) -> pd.tseries.offsets.BaseOffset | None:
"""Normalize ``freq`` to a canonical pandas offset.

Accepts freq strings, pandas offsets and cudf's own offset classes,
rejecting frequencies that cudf cannot represent.
"""
from cudf.core.tools.datetimes import DateOffset, MonthEnd, YearEnd

if freq is None:
return None
if isinstance(freq, (DateOffset, MonthEnd, YearEnd)):
return freq._maybe_as_fast_pandas_offset()
if isinstance(freq, str):
return cudf.DateOffset._from_freqstr(freq)
elif freq is None:
return freq
elif freq is not None and not isinstance(freq, cudf.DateOffset):
# cudf supports a subset of pandas frequency strings; parse with
# cudf's DateOffset (which raises for unsupported ones) and store
# the equivalent pandas offset.
return DateOffset._from_freqstr(freq)._maybe_as_fast_pandas_offset()
if not isinstance(freq, pd.tseries.offsets.BaseOffset):
raise ValueError(f"Invalid frequency: {freq}")
return cast("cudf.DateOffset", freq)
# gate pandas offsets on the same supported subset
if type(freq) is pd.DateOffset:
DateOffset(**freq.kwds)
else:
DateOffset._from_freqstr(freq.freqstr)
return freq
16 changes: 16 additions & 0 deletions python/cudf/cudf/pandas/_wrappers/pandas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2789,6 +2789,10 @@ def _unpickle_offset_obj(pickled_args):
# same reducer/unpickler can be used for Timedelta:
copyreg.dispatch_table[Timedelta] = _reduce_proxied_td_obj
copyreg.dispatch_table[pd.Timedelta] = _reduce_obj
# Period has no fast (cudf) representation; pickle the wrapped/real pandas
# object directly (e.g. matplotlib figures store pandas.Period x-data).
copyreg.dispatch_table[Period] = _reduce_proxied_td_obj
copyreg.dispatch_table[pd.Period] = _reduce_obj

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

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


def _unpickle_NaT():
Expand Down
22 changes: 0 additions & 22 deletions python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2344,19 +2344,11 @@ def pytest_unconfigure(config):
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_across_dst[ns]": "AssertionError: Series.index are different",
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_across_dst[s]": "AssertionError: Series.index are different",
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_across_dst[us]": "AssertionError: Series.index are different",
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_int[ms]": "TypeError: unsupported operand type(s) for *: 'int' and 'DateOffset'",
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_int[ns]": "TypeError: unsupported operand type(s) for *: 'int' and 'DateOffset'",
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_int[s]": "TypeError: unsupported operand type(s) for *: 'int' and 'DateOffset'",
"tests/indexes/datetimes/methods/test_shift.py::TestDatetimeIndexShift::test_dti_shift_int[us]": "TypeError: unsupported operand type(s) for *: 'int' and 'DateOffset'",
"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)",
"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)",
"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)",
"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",
"tests/indexes/datetimes/methods/test_tz_localize.py::TestTZLocalize::test_dti_tz_localize[]": "Failed: DID NOT RAISE <class 'ValueError'>",
"tests/indexes/datetimes/methods/test_tz_localize.py::TestTZLocalize::test_dti_tz_localize_nonexistent_raise_coerce": "Failed: DID NOT RAISE <class 'ValueError'>",
"tests/indexes/datetimes/methods/test_tz_localize.py::TestTZLocalize::test_dti_tz_localize_utc_conversion[tz2]": "Failed: DID NOT RAISE <class 'ValueError'>",
"tests/indexes/datetimes/methods/test_unique.py::test_index_unique2": "TODO: Add a reason for failure",
"tests/indexes/datetimes/test_arithmetic.py::TestDatetimeIndexArithmetic::test_add_dti_day": "TypeError: unsupported operand type(s) for +: 'DatetimeArray' and 'DateOffset'",
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_coverage": "TODO: Add a reason for failure",
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_datetime64_tzformat[W-SUN]": "TODO: Add a reason for failure",
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_dtype": "AssertionError: Index are different",
Expand Down Expand Up @@ -3280,20 +3272,6 @@ def pytest_unconfigure(config):
"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.",
"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.",
"tests/plotting/test_datetimelike.py::TestTSPlot::test_from_weekly_resampling": "ValueError: You must pass a freq argument as current index has none.",
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_datetime_frame[D]": "TODO: Add a reason for failure",
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_datetime_frame[W]": "TODO: Add a reason for failure",
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_datetime_frame[h]": "TODO: Add a reason for failure",
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_datetime_frame[min]": "TODO: Add a reason for failure",
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_datetime_frame[s]": "TODO: Add a reason for failure",
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_inferred_freq[D]": "TODO: Add a reason for failure",
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_inferred_freq[ME]": "TODO: Add a reason for failure",
"tests/plotting/test_datetimelike.py::TestTSPlot::test_line_plot_inferred_freq[YE]": "TODO: Add a reason for failure",
"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",
"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",
"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",
"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",
"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",
"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",
"tests/plotting/test_datetimelike.py::TestTSPlot::test_to_weekly_resampling": "AssertionError: assert <Week: weekday=4> == <DateOffset: weeks=1>",
"tests/reductions/test_reductions.py::TestDatetime64SeriesReductions::test_minmax_nat_series[nat_ser0]": "TODO: Add a reason for failure",
"tests/reductions/test_reductions.py::TestDatetime64SeriesReductions::test_minmax_nat_series[nat_ser1]": "TODO: Add a reason for failure",
Expand Down
Loading