Repository navigation
Fix cudf.pandas datetimelike plotting failures (canonical freq offsets + Period/offset pickling) #23054
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix cudf.pandas datetimelike plotting failures (canonical freq offsets + Period/offset pickling) #23054
Changes from 6 commits
418afac
42ca645
6e7f4ce
8410d2a
1cf26a3
c9cd350
d791fe0
fdb05e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: | ||
|
|
@@ -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) | ||
|
|
@@ -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) | ||
|
|
||
|
|
@@ -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"]) | ||
| else: | ||
| freq = None | ||
| freq = header_payload | ||
|
|
||
| obj._freq = _validate_freq(freq) | ||
| return obj | ||
|
|
@@ -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: | ||
|
|
@@ -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 | ||
|
|
@@ -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() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we at a stage where we could use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not quite — a generic |
||
|
|
||
| # maximum unique count supported is months with 4 unique lengths | ||
| # bail above that for now | ||
|
|
@@ -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: | ||
|
|
@@ -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 | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| else: | ||
| return self.inferred_freq | ||
|
|
||
|
|
@@ -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: | ||
|
|
@@ -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: | ||
|
|
@@ -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 | ||
Uh oh!
There was an error while loading. Please reload this page.