From 0697606110f6c345787f565dd0000f9d5e667551 Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 25 Oct 2025 14:56:51 -0700 Subject: [PATCH 1/2] BUG: freq invalidation in DatetimeIndex.where --- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/core/arrays/datetimelike.py | 4 ++++ pandas/tests/indexes/datetimes/test_indexing.py | 11 +++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 75b4c5c0fe14d..0cde8114f4962 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -980,6 +980,7 @@ Datetimelike - Bug in :meth:`DatetimeIndex.is_year_start` and :meth:`DatetimeIndex.is_quarter_start` does not raise on Custom business days frequencies bigger then "1C" (:issue:`58664`) - Bug in :meth:`DatetimeIndex.is_year_start` and :meth:`DatetimeIndex.is_quarter_start` returning ``False`` on double-digit frequencies (:issue:`58523`) - Bug in :meth:`DatetimeIndex.union` and :meth:`DatetimeIndex.intersection` when ``unit`` was non-nanosecond (:issue:`59036`) +- Bug in :meth:`DatetimeIndex.where` and :meth:`TimedeltaIndex.where` failing to set ``freq=None`` in some cases (:issue:`24555`) - Bug in :meth:`Index.union` with a ``pyarrow`` timestamp dtype incorrectly returning ``object`` dtype (:issue:`58421`) - Bug in :meth:`Series.dt.microsecond` producing incorrect results for pyarrow backed :class:`Series`. (:issue:`59154`) - Bug in :meth:`Timestamp.normalize` and :meth:`DatetimeArray.normalize` returning incorrect results instead of raising on integer overflow for very small (distant past) values (:issue:`60583`) @@ -996,7 +997,6 @@ Datetimelike - Bug in constructing arrays with a timezone-aware :class:`ArrowDtype` from timezone-naive datetime objects incorrectly treating those as UTC times instead of wall times like :class:`DatetimeTZDtype` (:issue:`61775`) - Bug in setting scalar values with mismatched resolution into arrays with non-nanosecond ``datetime64``, ``timedelta64`` or :class:`DatetimeTZDtype` incorrectly truncating those scalars (:issue:`56410`) - Timedelta ^^^^^^^^^ - Accuracy improvement in :meth:`Timedelta.to_pytimedelta` to round microseconds consistently for large nanosecond based Timedelta (:issue:`57841`) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 97113d98a2aab..913034bf849de 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -527,6 +527,10 @@ def view(self, dtype: Dtype | None = None) -> ArrayLike: # are present in this file. return super().view(dtype) + def _putmask(self, mask: npt.NDArray[np.bool_], value) -> None: + super()._putmask(mask, value) + self._freq = None # GH#24555 + # ------------------------------------------------------------------ # Validation Methods # TODO: try to de-duplicate these, ensure identical behavior diff --git a/pandas/tests/indexes/datetimes/test_indexing.py b/pandas/tests/indexes/datetimes/test_indexing.py index 742e3439db723..11877024e7be0 100644 --- a/pandas/tests/indexes/datetimes/test_indexing.py +++ b/pandas/tests/indexes/datetimes/test_indexing.py @@ -119,6 +119,17 @@ def test_getitem_int_list(self): class TestWhere: + @pytest.mark.parametrize("is_td", [True, False]) + def test_where_freq_invalidation(self, is_td): + # GH#24555 + index = date_range("20130101", periods=3, tz="US/Eastern") + if is_td: + index = index - index[0] + other = Index([pd.NaT, pd.NaT] + index[2:].tolist()) + + result = index.where(notna(other), other) + assert result.freq is None + def test_where_doesnt_retain_freq(self): dti = date_range("20130101", periods=3, freq="D", name="idx") cond = [True, True, False] From 5035f14ffc7777efff4ed564a53bbc5c39904d42 Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 25 Oct 2025 17:02:51 -0700 Subject: [PATCH 2/2] mypy fixup --- pandas/core/arrays/datetimelike.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 913034bf849de..1fbcd0665c467 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -393,7 +393,9 @@ def __getitem__(self, key: PositionalIndexer2D) -> Self | DTScalarOrNaT: else: # At this point we know the result is an array. result = cast(Self, result) - result._freq = self._get_getitem_freq(key) + # error: Incompatible types in assignment (expression has type + # "BaseOffset | None", variable has type "None") + result._freq = self._get_getitem_freq(key) # type: ignore[assignment] return result def _get_getitem_freq(self, key) -> BaseOffset | None: @@ -2046,7 +2048,7 @@ def _maybe_pin_freq(self, freq, validate_kwds: dict) -> None: if self._freq is None: # Set _freq directly to bypass duplicative _validate_frequency # check. - self._freq = to_offset(self.inferred_freq) + self._freq = to_offset(self.inferred_freq) # type: ignore[assignment] elif freq is lib.no_default: # user did not specify anything, keep inferred freq if the original # data had one, otherwise do nothing @@ -2446,7 +2448,7 @@ def take( if isinstance(maybe_slice, slice): freq = self._get_getitem_freq(maybe_slice) - result._freq = freq + result._freq = freq # type: ignore[assignment] return result