diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 40adeb28d47b6..2c8ad1782af4d 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -547,6 +547,7 @@ Datetimelike - Bug in :meth:`DatetimeIndex.intersection` losing ``freq`` and timezone in some cases (:issue:`33604`) - Bug in :class:`DatetimeIndex` addition and subtraction with some types of :class:`DateOffset` objects incorrectly retaining an invalid ``freq`` attribute (:issue:`33779`) - Bug in :class:`DatetimeIndex` where setting the ``freq`` attribute on an index could silently change the ``freq`` attribute on another index viewing the same data (:issue:`33552`) +- Bug in :meth:`DatetimeIndex.intersection` and :meth:`TimedeltaIndex.intersection` with results not having the correct ``name`` attribute (:issue:`33904`) Timedelta ^^^^^^^^^ diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index fe48f05820a4a..3f18f69149658 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -6,7 +6,7 @@ import numpy as np -from pandas._libs import NaT, iNaT, join as libjoin, lib +from pandas._libs import NaT, Timedelta, iNaT, join as libjoin, lib from pandas._libs.tslibs import timezones from pandas._typing import Label from pandas.compat.numpy import function as nv @@ -41,7 +41,7 @@ from pandas.core.sorting import ensure_key_mapped from pandas.core.tools.timedeltas import to_timedelta -from pandas.tseries.frequencies import DateOffset +from pandas.tseries.offsets import DateOffset, Tick _index_doc_kwargs = dict(ibase._index_doc_kwargs) @@ -641,6 +641,7 @@ def intersection(self, other, sort=False): """ self._validate_sort_keyword(sort) self._assert_can_do_setop(other) + res_name = get_op_result_name(self, other) if self.equals(other): return self._get_reconciled_name_object(other) @@ -654,12 +655,18 @@ def intersection(self, other, sort=False): result = Index.intersection(self, other, sort=sort) if isinstance(result, type(self)): if result.freq is None: + # TODO: no tests rely on this; needed? result = result._with_freq("infer") + assert result.name == res_name return result elif not self._can_fast_intersect(other): result = Index.intersection(self, other, sort=sort) - result = result._with_freq("infer") + assert result.name == res_name + # We need to invalidate the freq because Index.intersection + # uses _shallow_copy on a view of self._data, which will preserve + # self.freq if we're not careful. + result = result._with_freq(None)._with_freq("infer") return result # to make our life easier, "sort" the two ranges @@ -674,27 +681,34 @@ def intersection(self, other, sort=False): start = right[0] if end < start: - return type(self)(data=[], dtype=self.dtype, freq=self.freq) + return type(self)(data=[], dtype=self.dtype, freq=self.freq, name=res_name) else: lslice = slice(*left.slice_locs(start, end)) left_chunk = left._values[lslice] - return self._shallow_copy(left_chunk) + return type(self)._simple_new(left_chunk, name=res_name) def _can_fast_intersect(self: _T, other: _T) -> bool: if self.freq is None: return False - if other.freq != self.freq: + elif other.freq != self.freq: return False - if not self.is_monotonic_increasing: + elif not self.is_monotonic_increasing: # Because freq is not None, we must then be monotonic decreasing return False - if not self.freq.is_anchored(): - # If freq is not anchored, then despite having matching freqs, - # we might not "line up" - return False + elif self.freq.is_anchored(): + # this along with matching freqs ensure that we "line up", + # so intersection will preserve freq + return True + + elif isinstance(self.freq, Tick): + # We "line up" if and only if the difference between two of our points + # is a multiple of our freq + diff = self[0] - other[0] + remainder = diff % self.freq.delta + return remainder == Timedelta(0) return True @@ -749,6 +763,7 @@ def _fast_union(self, other, sort=None): right_chunk = right._values[:loc] dates = concat_compat((left._values, right_chunk)) # With sort being False, we can't infer that result.freq == self.freq + # TODO: no tests rely on the _with_freq("infer"); needed? result = self._shallow_copy(dates)._with_freq("infer") return result else: @@ -781,9 +796,12 @@ def _union(self, other, sort): if this._can_fast_union(other): result = this._fast_union(other, sort=sort) - if result.freq is None: + if sort is None: # In the case where sort is None, _can_fast_union # implies that result.freq should match self.freq + assert result.freq == self.freq, (result.freq, self.freq) + elif result.freq is None: + # TODO: no tests rely on this; needed? result = result._with_freq("infer") return result else: diff --git a/pandas/tests/indexes/datetimes/test_setops.py b/pandas/tests/indexes/datetimes/test_setops.py index 0473ecf9de24d..6670b079ddd29 100644 --- a/pandas/tests/indexes/datetimes/test_setops.py +++ b/pandas/tests/indexes/datetimes/test_setops.py @@ -222,7 +222,7 @@ def test_intersection(self, tz, sort): expected3 = date_range("6/1/2000", "6/20/2000", freq="D", name=None) rng4 = date_range("7/1/2000", "7/31/2000", freq="D", name="idx") - expected4 = DatetimeIndex([], name="idx") + expected4 = DatetimeIndex([], freq="D", name="idx") for (rng, expected) in [ (rng2, expected2), @@ -264,7 +264,7 @@ def test_intersection(self, tz, sort): if sort is None: expected = expected.sort_values() tm.assert_index_equal(result, expected) - assert result.freq is None + assert result.freq == expected.freq # parametrize over both anchored and non-anchored freqs, as they # have different code paths diff --git a/pandas/tests/indexes/timedeltas/test_setops.py b/pandas/tests/indexes/timedeltas/test_setops.py index d7576697435a0..6a2f66cade733 100644 --- a/pandas/tests/indexes/timedeltas/test_setops.py +++ b/pandas/tests/indexes/timedeltas/test_setops.py @@ -156,7 +156,7 @@ def test_zero_length_input_index(self, sort): # if no overlap exists return empty index ( timedelta_range("1 day", periods=10, freq="h", name="idx")[5:], - TimedeltaIndex([], name="idx"), + TimedeltaIndex([], freq="h", name="idx"), ), ], ) diff --git a/pandas/tests/indexing/test_partial.py b/pandas/tests/indexing/test_partial.py index ee87a3f9f5ecd..a1bd6fed32cad 100644 --- a/pandas/tests/indexing/test_partial.py +++ b/pandas/tests/indexing/test_partial.py @@ -128,7 +128,7 @@ def test_partial_setting(self): df.at[dates[-1] + dates.freq, "A"] = 7 tm.assert_frame_equal(df, expected) - exp_other = DataFrame({0: 7}, index=[dates[-1] + dates.freq]) + exp_other = DataFrame({0: 7}, index=dates[-1:] + dates.freq) expected = pd.concat([df_orig, exp_other], axis=1) df = df_orig.copy()