Skip to content

Commit 4e034ec

Browse files
DEPR: DatetimeIndex.intersection with mixed timezones cast to UTC, not object (#45357)
* DEPR: DatetimeIndex.intersection with mixed timezones cast to UTC instead of object * GH ref * mypy fixup Co-authored-by: Jeff Reback <[email protected]>
1 parent e6a20bd commit 4e034ec

File tree

3 files changed

+45
-20
lines changed

3 files changed

+45
-20
lines changed

doc/source/whatsnew/v1.5.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,11 @@ Slicing on a :class:`DataFrame` will not be affected.
140140
Other Deprecations
141141
^^^^^^^^^^^^^^^^^^
142142
- Deprecated the keyword ``line_terminator`` in :meth:`DataFrame.to_csv` and :meth:`Series.to_csv`, use ``lineterminator`` instead; this is for consistency with :func:`read_csv` and the standard library 'csv' module (:issue:`9568`)
143+
- Deprecated behavior of :meth:`DatetimeIndex.intersection` and :meth:`DatetimeIndex.symmetric_difference` (``union`` behavior was already deprecated in version 1.3.0) with mixed timezones; in a future version both will be cast to UTC instead of object dtype (:issue:`39328`, :issue:`45357`)
143144
- Deprecated :meth:`DataFrame.iteritems`, :meth:`Series.iteritems`, :meth:`HDFStore.iteritems` in favor of :meth:`DataFrame.items`, :meth:`Series.items`, :meth:`HDFStore.items` (:issue:`45321`)
144145
-
145146

147+
146148
.. ---------------------------------------------------------------------------
147149
.. _whatsnew_150.performance:
148150

pandas/core/indexes/base.py

+35-15
Original file line numberDiff line numberDiff line change
@@ -3046,6 +3046,30 @@ def _validate_sort_keyword(self, sort):
30463046
f"None or False; {sort} was passed."
30473047
)
30483048

3049+
@final
3050+
def _deprecate_dti_setop(self, other: Index, setop: str_t):
3051+
"""
3052+
Deprecate setop behavior between timezone-aware DatetimeIndexes with
3053+
mismatched timezones.
3054+
"""
3055+
# Caller is responsibelf or checking
3056+
# `not is_dtype_equal(self.dtype, other.dtype)`
3057+
if (
3058+
isinstance(self, ABCDatetimeIndex)
3059+
and isinstance(other, ABCDatetimeIndex)
3060+
and self.tz is not None
3061+
and other.tz is not None
3062+
):
3063+
# GH#39328, GH#45357
3064+
warnings.warn(
3065+
f"In a future version, the {setop} of DatetimeIndex objects "
3066+
"with mismatched timezones will cast both to UTC instead of "
3067+
"object dtype. To retain the old behavior, "
3068+
f"use `index.astype(object).{setop}(other)`",
3069+
FutureWarning,
3070+
stacklevel=find_stack_level(),
3071+
)
3072+
30493073
@final
30503074
def union(self, other, sort=None):
30513075
"""
@@ -3144,21 +3168,7 @@ def union(self, other, sort=None):
31443168
"Can only union MultiIndex with MultiIndex or Index of tuples, "
31453169
"try mi.to_flat_index().union(other) instead."
31463170
)
3147-
if (
3148-
isinstance(self, ABCDatetimeIndex)
3149-
and isinstance(other, ABCDatetimeIndex)
3150-
and self.tz is not None
3151-
and other.tz is not None
3152-
):
3153-
# GH#39328
3154-
warnings.warn(
3155-
"In a future version, the union of DatetimeIndex objects "
3156-
"with mismatched timezones will cast both to UTC instead of "
3157-
"object dtype. To retain the old behavior, "
3158-
"use `index.astype(object).union(other)`",
3159-
FutureWarning,
3160-
stacklevel=find_stack_level(),
3161-
)
3171+
self._deprecate_dti_setop(other, "union")
31623172

31633173
dtype = self._find_common_type_compat(other)
31643174
left = self.astype(dtype, copy=False)
@@ -3294,6 +3304,9 @@ def intersection(self, other, sort=False):
32943304
self._assert_can_do_setop(other)
32953305
other, result_name = self._convert_can_do_setop(other)
32963306

3307+
if not is_dtype_equal(self.dtype, other.dtype):
3308+
self._deprecate_dti_setop(other, "intersection")
3309+
32973310
if self.equals(other):
32983311
if self.has_duplicates:
32993312
return self.unique()._get_reconciled_name_object(other)
@@ -3424,6 +3437,10 @@ def difference(self, other, sort=None):
34243437
self._assert_can_do_setop(other)
34253438
other, result_name = self._convert_can_do_setop(other)
34263439

3440+
# Note: we do NOT call _deprecate_dti_setop here, as there
3441+
# is no requirement that .difference be commutative, so it does
3442+
# not cast to object.
3443+
34273444
if self.equals(other):
34283445
# Note: we do not (yet) sort even if sort=None GH#24959
34293446
return self[:0].rename(result_name)
@@ -3498,6 +3515,9 @@ def symmetric_difference(self, other, result_name=None, sort=None):
34983515
if result_name is None:
34993516
result_name = result_name_update
35003517

3518+
if not is_dtype_equal(self.dtype, other.dtype):
3519+
self._deprecate_dti_setop(other, "symmetric_difference")
3520+
35013521
if not self._should_compare(other):
35023522
return self.union(other, sort=sort).rename(result_name)
35033523

pandas/tests/indexes/datetimes/test_timezones.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -1140,20 +1140,23 @@ def test_dti_convert_tz_aware_datetime_datetime(self, tz):
11401140
tm.assert_numpy_array_equal(converted.asi8, ex_vals)
11411141
assert converted.tz is pytz.utc
11421142

1143-
def test_dti_union_aware(self):
1143+
# Note: not difference, as there is no symmetry requirement there
1144+
@pytest.mark.parametrize("setop", ["union", "intersection", "symmetric_difference"])
1145+
def test_dti_setop_aware(self, setop):
11441146
# non-overlapping
11451147
rng = date_range("2012-11-15 00:00:00", periods=6, freq="H", tz="US/Central")
11461148

11471149
rng2 = date_range("2012-11-15 12:00:00", periods=6, freq="H", tz="US/Eastern")
11481150

11491151
with tm.assert_produces_warning(FutureWarning):
11501152
# # GH#39328 will cast both to UTC
1151-
result = rng.union(rng2)
1153+
result = getattr(rng, setop)(rng2)
11521154

1153-
expected = rng.astype("O").union(rng2.astype("O"))
1155+
expected = getattr(rng.astype("O"), setop)(rng2.astype("O"))
11541156
tm.assert_index_equal(result, expected)
1155-
assert result[0].tz.zone == "US/Central"
1156-
assert result[-1].tz.zone == "US/Eastern"
1157+
if len(result):
1158+
assert result[0].tz.zone == "US/Central"
1159+
assert result[-1].tz.zone == "US/Eastern"
11571160

11581161
def test_dti_union_mixed(self):
11591162
# GH 21671

0 commit comments

Comments
 (0)