Skip to content

Commit 09fc282

Browse files
jbrockmendelclaude
andauthored
REF: relocate _freq storage from DTA/TDA arrays to Index (GH#24566) (#65322)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 95417df commit 09fc282

19 files changed

Lines changed: 260 additions & 149 deletions

pandas/core/arrays/datetimes.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -547,9 +547,7 @@ def _generate_range(
547547

548548
dt64_values = i8values.view(f"datetime64[{unit}]")
549549
dtype = tz_to_dtype(tz, unit=unit)
550-
result = cls._simple_new(dt64_values, dtype=dtype)
551-
result._freq = freq
552-
return result
550+
return cls._simple_new(dt64_values, dtype=dtype)
553551

554552
# -----------------------------------------------------------------
555553
# DatetimeLike Interface

pandas/core/arrays/period.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,23 @@ def to_timestamp(self, freq=None, how: str = "start") -> DatetimeArray:
977977
new_data = libperiod.periodarr_to_dt64arr(new_parr.asi8, base)
978978
dta = DatetimeArray._from_sequence(new_data, dtype=new_data.dtype)
979979
assert dta.unit == unit
980+
return dta
981+
982+
def _to_timestamp_freq(
983+
self, dta: DatetimeArray, target_freq, how: str
984+
) -> BaseOffset | None:
985+
"""
986+
Determine the freq to stamp on the DatetimeArray returned by
987+
``self.to_timestamp(target_freq, how)``.
988+
989+
Called by ``PeriodIndex.to_timestamp`` so the array-level
990+
``to_timestamp`` can return a bare DatetimeArray.
991+
"""
992+
how = libperiod.validate_end_alias(how)
993+
if how == "E":
994+
# For the "end" case, to_timestamp routes through arithmetic that
995+
# does not preserve freq; match that behavior here.
996+
return None
980997

981998
if self.freq.rule_code == "B":
982999
# See if we can retain BDay instead of Day in cases where
@@ -985,22 +1002,23 @@ def to_timestamp(self, freq=None, how: str = "start") -> DatetimeArray:
9851002
if len(diffs) == 1:
9861003
diff = diffs[0]
9871004
if diff == self.dtype._n:
988-
dta._freq = self.freq
1005+
return self.freq
9891006
elif diff == 1:
990-
dta._freq = self.freq.base
1007+
return self.freq.base
9911008
# TODO: other cases?
992-
return dta
993-
else:
994-
dta._freq = to_offset(dta.inferred_freq)
995-
if freq is not None:
996-
freq = to_offset(freq)
997-
if (
998-
isinstance(dta.freq, Day)
999-
and not isinstance(freq, Day)
1000-
and Timedelta(freq) == Timedelta(days=dta.freq.n)
1001-
):
1002-
dta._freq = freq
1003-
return dta
1009+
return None
1010+
1011+
result_freq = to_offset(dta.inferred_freq)
1012+
if target_freq is not None:
1013+
# match the normalization applied in to_timestamp
1014+
target_freq = Period._maybe_convert_freq(target_freq)
1015+
if (
1016+
isinstance(result_freq, Day)
1017+
and not isinstance(target_freq, Day)
1018+
and Timedelta(target_freq) == Timedelta(days=result_freq.n)
1019+
):
1020+
result_freq = target_freq
1021+
return result_freq
10041022

10051023
# --------------------------------------------------------------------
10061024

pandas/core/arrays/timedeltas.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,7 @@ def _generate_range(
307307
index = index[:-1]
308308

309309
td64values = index.view(f"m8[{unit}]")
310-
result = cls._simple_new(td64values, dtype=td64values.dtype)
311-
result._freq = freq
312-
return result
310+
return cls._simple_new(td64values, dtype=td64values.dtype)
313311

314312
# ----------------------------------------------------------------
315313
# DatetimeLike Interface

pandas/core/groupby/grouper.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,9 @@ def _codes_and_uniques(self) -> tuple[npt.NDArray[np.signedinteger], ArrayLike]:
681681
elif isinstance(self.grouping_vector, ops.BaseGrouper):
682682
# we have a list of groupers
683683
codes = self.grouping_vector.codes_info
684-
uniques = self.grouping_vector.result_index._values
684+
# Pass the full Index (not ._values) so DatetimeIndex/TimedeltaIndex
685+
# freq survives the trip through _with_infer.
686+
uniques = self.grouping_vector.result_index # type: ignore[assignment]
685687
elif self._uniques is not None:
686688
# GH#50486 Code grouping_vector using _uniques; allows
687689
# including uniques that are not present in grouping_vector.

pandas/core/indexes/base.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,14 @@ def __new__(
586586
klass = cls._dtype_to_subclass(arr.dtype)
587587

588588
arr = klass._ensure_array(arr, arr.dtype, copy=False)
589-
return klass._simple_new(arr, name, refs=refs) # type: ignore[return-value] # pyright: ignore[reportReturnType]
589+
out = klass._simple_new(arr, name, refs=refs)
590+
# Preserve freq when wrapping a DatetimeIndex/TimedeltaIndex input,
591+
# since _simple_new doesn't copy Index-level attributes like _freq.
592+
if isinstance(data, (ABCDatetimeIndex, ABCTimedeltaIndex)) and isinstance(
593+
out, (ABCDatetimeIndex, ABCTimedeltaIndex)
594+
):
595+
out._freq = data._freq
596+
return out # type: ignore[return-value] # pyright: ignore[reportReturnType]
590597

591598
@classmethod
592599
def _ensure_array(cls, data: ArrayLike, dtype: DtypeObj, copy: bool) -> ArrayLike:

0 commit comments

Comments
 (0)