Skip to content

Commit 6490f20

Browse files
jbrockmendelclaude
andauthored
REF: move more freq management from DatetimeArray/TimedeltaArray to Index (GH#24566) (#65266)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 123045b commit 6490f20

6 files changed

Lines changed: 76 additions & 77 deletions

File tree

pandas/core/arrays/datetimelike.py

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
round_nsint64,
5353
)
5454
from pandas._libs.tslibs.np_datetime import compare_mismatched_resolutions
55-
from pandas._libs.tslibs.offsets import FY5253Mixin
5655
from pandas._libs.tslibs.timedeltas import get_unit_for_round
5756
from pandas._libs.tslibs.timestamps import integer_op_not_supported
5857
from pandas._typing import (
@@ -495,10 +494,6 @@ def view(self, dtype: Dtype | None = None) -> ArrayLike:
495494
# are present in this file.
496495
return super().view(dtype)
497496

498-
def _putmask(self, mask: npt.NDArray[np.bool_], value) -> None:
499-
super()._putmask(mask, value)
500-
self._freq = None # GH#24555
501-
502497
# ------------------------------------------------------------------
503498
# Validation Methods
504499
# TODO: try to de-duplicate these, ensure identical behavior
@@ -1861,64 +1856,6 @@ def freq(self, value) -> None:
18611856

18621857
self._freq = value
18631858

1864-
@final
1865-
def _maybe_pin_freq(self, freq, validate_kwds: dict) -> None:
1866-
"""
1867-
Constructor helper to pin the appropriate `freq` attribute. Assumes
1868-
that self._freq is currently set to any freq inferred from input data.
1869-
"""
1870-
if freq is None:
1871-
# user explicitly passed None -> override any inferred_freq
1872-
self._freq = None
1873-
elif freq == "infer":
1874-
# if self._freq is *not* None then we already inferred a freq
1875-
# and there is nothing left to do
1876-
if self._freq is None:
1877-
# Set _freq directly to bypass duplicative _validate_frequency
1878-
# check.
1879-
self._freq = to_offset(self.inferred_freq) # type: ignore[assignment]
1880-
elif freq is lib.no_default:
1881-
# user did not specify anything, keep inferred freq if the original
1882-
# data had one, otherwise do nothing
1883-
pass
1884-
elif self._freq is None:
1885-
# We cannot inherit a freq from the data, so we need to validate
1886-
# the user-passed freq
1887-
freq = to_offset(freq)
1888-
type(self)._validate_frequency(self, freq, **validate_kwds)
1889-
self._freq = freq
1890-
else:
1891-
# Otherwise we just need to check that the user-passed freq
1892-
# doesn't conflict with the one we already have.
1893-
freq = to_offset(freq)
1894-
if freq != self._freq:
1895-
# GH#61086 freq may be equivalent but not equal (e.g.
1896-
# QS-FEB vs QS-MAY), so validate against the actual data.
1897-
if len(self) == 0:
1898-
pass
1899-
elif len(self) == 1:
1900-
if not freq.is_on_offset(self[0]):
1901-
raise ValueError(
1902-
f"Inferred frequency {self._freq} from passed "
1903-
"values does not conform to passed frequency "
1904-
f"{freq.freqstr}"
1905-
)
1906-
elif self[0] + freq == self[1]:
1907-
# For standard offsets, the step is a deterministic
1908-
# function of the date, so agreement on one step proves
1909-
# equivalence. For Custom/FY5253 offsets, external
1910-
# state (holidays, 52/53-week patterns) could cause
1911-
# later steps to diverge, so we validate fully.
1912-
if hasattr(freq, "_holidays") or isinstance(freq, FY5253Mixin):
1913-
type(self)._validate_frequency(self, freq, **validate_kwds)
1914-
else:
1915-
raise ValueError(
1916-
f"Inferred frequency {self._freq} from passed "
1917-
"values does not conform to passed frequency "
1918-
f"{freq.freqstr}"
1919-
)
1920-
self._freq = freq
1921-
19221859
@final
19231860
@classmethod
19241861
def _validate_frequency(cls, index, freq: BaseOffset, **kwargs) -> None:

pandas/core/arrays/datetimes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1264,7 +1264,7 @@ def to_period(self, freq=None) -> PeriodArray:
12641264
)
12651265

12661266
if freq is None:
1267-
freq = self.freqstr or self.inferred_freq
1267+
freq = self.inferred_freq
12681268

12691269
if freq is None:
12701270
raise ValueError(

pandas/core/indexes/datetimelike.py

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
to_offset,
3939
)
4040
from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit
41+
from pandas._libs.tslibs.offsets import FY5253Mixin
4142
from pandas.compat.numpy import function as nv
4243
from pandas.errors import (
4344
InvalidIndexError,
@@ -682,6 +683,72 @@ def astype(self, dtype, copy: bool = True):
682683
result._data._freq = self.freq
683684
return result
684685

686+
def putmask(self, mask, value) -> Index:
687+
# GH#24555 putmask may modify values out-of-sequence; drop freq
688+
result = super().putmask(mask, value)
689+
if isinstance(result, type(self)):
690+
result._data._freq = None
691+
return result
692+
693+
def _pin_freq(self, freq, validate_kwds: dict) -> None:
694+
"""
695+
Constructor helper to pin the appropriate ``freq`` attribute on
696+
``self._data``. Assumes ``self._data._freq`` is currently set to any
697+
freq inferred from input data.
698+
"""
699+
arr = self._data
700+
if freq is None:
701+
# user explicitly passed None -> override any inferred_freq
702+
arr._freq = None
703+
elif freq == "infer":
704+
# if arr._freq is *not* None then we already inferred a freq
705+
# and there is nothing left to do
706+
if arr._freq is None:
707+
# Set _freq directly to bypass duplicative _validate_frequency
708+
# check.
709+
arr._freq = to_offset(self.inferred_freq)
710+
elif freq is lib.no_default:
711+
# user did not specify anything, keep inferred freq if the original
712+
# data had one, otherwise do nothing
713+
pass
714+
elif arr._freq is None:
715+
# We cannot inherit a freq from the data, so we need to validate
716+
# the user-passed freq
717+
freq = to_offset(freq)
718+
type(arr)._validate_frequency(self, freq, **validate_kwds)
719+
arr._freq = freq
720+
else:
721+
# Otherwise we just need to check that the user-passed freq
722+
# doesn't conflict with the one we already have.
723+
freq = to_offset(freq)
724+
if freq != arr._freq:
725+
# GH#61086 freq may be equivalent but not equal (e.g.
726+
# QS-FEB vs QS-MAY), so validate against the actual data.
727+
if len(self) == 0:
728+
pass
729+
elif len(self) == 1:
730+
if not freq.is_on_offset(self[0]):
731+
raise ValueError(
732+
f"Inferred frequency {arr._freq} from passed "
733+
"values does not conform to passed frequency "
734+
f"{freq.freqstr}"
735+
)
736+
elif self[0] + freq == self[1]:
737+
# For standard offsets, the step is a deterministic
738+
# function of the date, so agreement on one step proves
739+
# equivalence. For Custom/FY5253 offsets, external
740+
# state (holidays, 52/53-week patterns) could cause
741+
# later steps to diverge, so we validate fully.
742+
if hasattr(freq, "_holidays") or isinstance(freq, FY5253Mixin):
743+
type(arr)._validate_frequency(self, freq, **validate_kwds)
744+
else:
745+
raise ValueError(
746+
f"Inferred frequency {arr._freq} from passed "
747+
"values does not conform to passed frequency "
748+
f"{freq.freqstr}"
749+
)
750+
arr._freq = freq
751+
685752
def _get_arithmetic_result_freq(self, other) -> BaseOffset | None:
686753
"""
687754
Check if we can preserve self.freq in addition or subtraction.
@@ -850,13 +917,10 @@ def as_unit(self, unit: TimeUnit) -> Self:
850917

851918
def _with_freq(self, freq):
852919
# GH#29843
853-
if freq is None:
854-
# Always valid
920+
if freq is None or (len(self) == 0 and isinstance(freq, BaseOffset)):
921+
# None is always valid. For offsets on empty index the array's
922+
# _with_freq below performs the m-dtype Tick validation.
855923
pass
856-
elif len(self) == 0 and isinstance(freq, BaseOffset):
857-
# Always valid. In the TimedeltaArray case, we require a Tick offset
858-
if self.dtype.kind == "m" and not isinstance(freq, (Tick, Day)):
859-
raise TypeError("TimedeltaArray/Index freq must be a Tick")
860924
else:
861925
# As an internal method, we can ensure this assertion always holds
862926
assert freq == "infer"
@@ -1231,9 +1295,6 @@ def _get_getitem_freq(self, key) -> BaseOffset | None:
12311295
"""
12321296
Find the `freq` attribute to assign to the result of a __getitem__ lookup.
12331297
"""
1234-
if self.ndim != 1:
1235-
return None
1236-
12371298
key = check_array_indexer(self._data, key) # maybe ndarray[bool] -> slice
12381299
freq = None
12391300
if isinstance(key, slice):

pandas/core/indexes/datetimes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -922,13 +922,13 @@ def __new__(
922922

923923
if inferred_freq is not None:
924924
dtarr._freq = inferred_freq
925-
dtarr._maybe_pin_freq(freq, {"ambiguous": ambiguous})
926925

927926
refs = None
928927
if not copy and isinstance(data, (Index, ABCSeries)):
929928
refs = data._references
930929

931930
subarr = cls._simple_new(dtarr, name=name, refs=refs)
931+
subarr._pin_freq(freq, {"ambiguous": ambiguous})
932932
return subarr
933933

934934
# --------------------------------------------------------------------

pandas/core/indexes/timedeltas.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,13 @@ def __new__(
208208
# - Cases checked above all return/raise before reaching here - #
209209

210210
tdarr = TimedeltaArray._from_sequence(data, dtype=dtype, copy=copy)
211-
tdarr._maybe_pin_freq(freq, {})
212211
refs = None
213212
if not copy and isinstance(data, (ABCSeries, Index)):
214213
refs = data._references
215214

216-
return cls._simple_new(tdarr, name=name, refs=refs)
215+
result = cls._simple_new(tdarr, name=name, refs=refs)
216+
result._pin_freq(freq, {})
217+
return result
217218

218219
# -------------------------------------------------------------------
219220

pandas/tests/arrays/test_datetimelike.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1109,7 +1109,7 @@ def test_to_timestamp(self, how, arr1d):
11091109
def test_to_timestamp_roundtrip_bday(self):
11101110
# Case where infer_freq inside would choose "D" instead of "B"
11111111
dta = pd.date_range("2021-10-18", periods=3, freq="B", unit="ns")._data
1112-
parr = dta.to_period()
1112+
parr = dta.to_period("B")
11131113
result = parr.to_timestamp()
11141114
assert result.freq == "B"
11151115
tm.assert_extension_array_equal(result, dta.as_unit("us"))

0 commit comments

Comments
 (0)