Skip to content

Commit 4c41a76

Browse files
API/DEPR: Remove +/- as setops for DatetimeIndex/PeriodIndex (GH9630)
1 parent 8023029 commit 4c41a76

File tree

4 files changed

+144
-82
lines changed

4 files changed

+144
-82
lines changed

doc/source/whatsnew/v0.19.0.txt

+22-3
Original file line numberDiff line numberDiff line change
@@ -932,14 +932,16 @@ New Behavior:
932932
Index ``+`` / ``-`` no longer used for set operations
933933
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
934934

935-
Addition and subtraction of the base Index type (not the numeric subclasses)
935+
Addition and subtraction of the base Index type and of DatetimeIndex
936+
(not the numeric index types)
936937
previously performed set operations (set union and difference). This
937938
behaviour was already deprecated since 0.15.0 (in favor using the specific
938939
``.union()`` and ``.difference()`` methods), and is now disabled. When
939940
possible, ``+`` and ``-`` are now used for element-wise operations, for
940-
example for concatenating strings (:issue:`8227`, :issue:`14127`).
941+
example for concatenating strings or subtracting datetimes
942+
(:issue:`8227`, :issue:`14127`).
941943

942-
Previous Behavior:
944+
Previous behavior:
943945

944946
.. code-block:: ipython
945947

@@ -962,6 +964,23 @@ For example, the behaviour of adding two integer Indexes:
962964

963965
is unchanged. The base ``Index`` is now made consistent with this behaviour.
964966

967+
Further, because of this change, it is now possible to subtract two
968+
DatetimeIndex objects resulting in a TimedeltaIndex:
969+
970+
Previous behavior:
971+
972+
.. code-block:: ipython
973+
974+
In [1]: pd.DatetimeIndex(['2016-01-01', '2016-01-02']) - pd.DatetimeIndex(['2016-01-02', '2016-01-03'])
975+
FutureWarning: using '-' to provide set differences with datetimelike Indexes is deprecated, use .difference()
976+
Out[1]: DatetimeIndex(['2016-01-01'], dtype='datetime64[ns]', freq=None)
977+
978+
New behavior:
979+
980+
.. ipython:: python
981+
982+
pd.DatetimeIndex(['2016-01-01', '2016-01-02']) - pd.DatetimeIndex(['2016-01-02', '2016-01-03'])
983+
965984

966985
.. _whatsnew_0190.api.difference:
967986

pandas/tseries/base.py

+4-8
Original file line numberDiff line numberDiff line change
@@ -628,10 +628,9 @@ def __add__(self, other):
628628
raise TypeError("cannot add TimedeltaIndex and {typ}"
629629
.format(typ=type(other)))
630630
elif isinstance(other, Index):
631-
warnings.warn("using '+' to provide set union with "
632-
"datetimelike Indexes is deprecated, "
633-
"use .union()", FutureWarning, stacklevel=2)
634-
return self.union(other)
631+
raise TypeError("cannot add {typ1} and {typ2}"
632+
.format(typ1=type(self).__name__,
633+
typ2=type(other).__name__))
635634
elif isinstance(other, (DateOffset, timedelta, np.timedelta64,
636635
tslib.Timedelta)):
637636
return self._add_delta(other)
@@ -656,10 +655,7 @@ def __sub__(self, other):
656655
.format(typ=type(other)))
657656
return self._add_delta(-other)
658657
elif isinstance(other, Index):
659-
warnings.warn("using '-' to provide set differences with "
660-
"datetimelike Indexes is deprecated, "
661-
"use .difference()", FutureWarning, stacklevel=2)
662-
return self.difference(other)
658+
return self._sub_datelike(other)
663659
elif isinstance(other, (DateOffset, timedelta, np.timedelta64,
664660
tslib.Timedelta)):
665661
return self._add_delta(-other)

pandas/tseries/index.py

+30-10
Original file line numberDiff line numberDiff line change
@@ -731,19 +731,39 @@ def _add_datelike(self, other):
731731
def _sub_datelike(self, other):
732732
# subtract a datetime from myself, yielding a TimedeltaIndex
733733
from pandas import TimedeltaIndex
734-
other = Timestamp(other)
735-
if other is tslib.NaT:
736-
result = self._nat_new(box=False)
737-
# require tz compat
738-
elif not self._has_same_tz(other):
739-
raise TypeError("Timestamp subtraction must have the same "
740-
"timezones or no timezones")
734+
if isinstance(other, DatetimeIndex):
735+
# require tz compat
736+
if not self._has_same_tz(other):
737+
raise TypeError("DatetimeIndex subtraction must have the same "
738+
"timezones or no timezones")
739+
result = self._sub_datelike_dti(other)
740+
elif isinstance(other, (tslib.Timestamp, datetime)):
741+
other = Timestamp(other)
742+
if other is tslib.NaT:
743+
result = self._nat_new(box=False)
744+
# require tz compat
745+
elif not self._has_same_tz(other):
746+
raise TypeError("Timestamp subtraction must have the same "
747+
"timezones or no timezones")
748+
else:
749+
i8 = self.asi8
750+
result = i8 - other.value
751+
result = self._maybe_mask_results(result, fill_value=tslib.iNaT)
741752
else:
742-
i8 = self.asi8
743-
result = i8 - other.value
744-
result = self._maybe_mask_results(result, fill_value=tslib.iNaT)
753+
raise TypeError("cannot subtract DatetimeIndex and {typ}"
754+
.format(typ=type(other).__name__))
745755
return TimedeltaIndex(result, name=self.name, copy=False)
746756

757+
def _sub_datelike_dti(self, other):
758+
"""subtraction of two DatetimeIndexes"""
759+
self_i8 = self.asi8
760+
other_i8 = other.asi8
761+
new_values = self_i8 - other_i8
762+
if self.hasnans or other.hasnans:
763+
mask = (self._isnan) | (other._isnan)
764+
new_values[mask] = tslib.iNaT
765+
return new_values.view('i8')
766+
747767
def _maybe_update_attributes(self, attrs):
748768
""" Update Index attributes (e.g. freq) depending on op """
749769
freq = attrs.get('freq', None)

pandas/tseries/tests/test_base.py

+88-61
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ def test_resolution(self):
360360
tz=tz)
361361
self.assertEqual(idx.resolution, expected)
362362

363-
def test_add_iadd(self):
363+
def test_union(self):
364364
for tz in self.tz:
365365
# union
366366
rng1 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
@@ -378,17 +378,34 @@ def test_add_iadd(self):
378378
for rng, other, expected in [(rng1, other1, expected1),
379379
(rng2, other2, expected2),
380380
(rng3, other3, expected3)]:
381-
# GH9094
382-
with tm.assert_produces_warning(FutureWarning):
383-
result_add = rng + other
384-
result_union = rng.union(other)
385381

386-
tm.assert_index_equal(result_add, expected)
382+
result_union = rng.union(other)
387383
tm.assert_index_equal(result_union, expected)
388-
# GH9094
389-
with tm.assert_produces_warning(FutureWarning):
384+
385+
def test_add_iadd(self):
386+
for tz in self.tz:
387+
rng1 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
388+
other1 = pd.date_range('1/6/2000', freq='D', periods=5, tz=tz)
389+
expected1 = pd.date_range('1/1/2000', freq='D', periods=10, tz=tz)
390+
391+
rng2 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
392+
other2 = pd.date_range('1/4/2000', freq='D', periods=5, tz=tz)
393+
expected2 = pd.date_range('1/1/2000', freq='D', periods=8, tz=tz)
394+
395+
rng3 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
396+
other3 = pd.DatetimeIndex([], tz=tz)
397+
expected3 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
398+
399+
for rng, other, expected in [(rng1, other1, expected1),
400+
(rng2, other2, expected2),
401+
(rng3, other3, expected3)]:
402+
# previously performed setop (deprecated in 0.16.0), now
403+
# raises TypeError (GH..)
404+
with tm.assertRaises(TypeError):
405+
rng + other
406+
407+
with tm.assertRaises(TypeError):
390408
rng += other
391-
tm.assert_index_equal(rng, expected)
392409

393410
# offset
394411
offsets = [pd.offsets.Hour(2), timedelta(hours=2),
@@ -421,7 +438,26 @@ def test_add_iadd(self):
421438
with tm.assertRaisesRegexp(TypeError, msg):
422439
Timestamp('2011-01-01') + idx
423440

424-
def test_sub_isub(self):
441+
def test_add_dti_dti(self):
442+
# previously performed setop (deprecated in 0.16.0), now raises
443+
# TypeError (GH..)
444+
445+
dti = date_range('20130101', periods=3)
446+
dti_tz = date_range('20130101', periods=3).tz_localize('US/Eastern')
447+
448+
with tm.assertRaises(TypeError):
449+
dti + dti
450+
451+
with tm.assertRaises(TypeError):
452+
dti_tz + dti_tz
453+
454+
with tm.assertRaises(TypeError):
455+
dti_tz + dti
456+
457+
with tm.assertRaises(TypeError):
458+
dti + dti_tz
459+
460+
def test_diff(self):
425461
for tz in self.tz:
426462
# diff
427463
rng1 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
@@ -439,9 +475,22 @@ def test_sub_isub(self):
439475
for rng, other, expected in [(rng1, other1, expected1),
440476
(rng2, other2, expected2),
441477
(rng3, other3, expected3)]:
442-
result_union = rng.difference(other)
478+
result_diff = rng.difference(other)
479+
tm.assert_index_equal(result_diff, expected)
443480

444-
tm.assert_index_equal(result_union, expected)
481+
def test_sub_isub(self):
482+
for tz in self.tz:
483+
rng1 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
484+
other1 = pd.date_range('1/6/2000', freq='D', periods=5, tz=tz)
485+
expected1 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
486+
487+
rng2 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
488+
other2 = pd.date_range('1/4/2000', freq='D', periods=5, tz=tz)
489+
expected2 = pd.date_range('1/1/2000', freq='D', periods=3, tz=tz)
490+
491+
rng3 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
492+
other3 = pd.DatetimeIndex([], tz=tz)
493+
expected3 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
445494

446495
# offset
447496
offsets = [pd.offsets.Hour(2), timedelta(hours=2),
@@ -466,6 +515,30 @@ def test_sub_isub(self):
466515
rng -= 1
467516
tm.assert_index_equal(rng, expected)
468517

518+
def test_sub_dti_dti(self):
519+
# previously performed setop (deprecated in 0.16.0), now changed to
520+
# return subtraction -> TimeDeltaIndex (GH ...)
521+
522+
dti = date_range('20130101', periods=3)
523+
dti_tz = date_range('20130101', periods=3).tz_localize('US/Eastern')
524+
dti_tz2 = date_range('20130101', periods=3).tz_localize('UTC')
525+
expected = TimedeltaIndex([0, 0, 0])
526+
527+
result = dti - dti
528+
tm.assert_index_equal(result, expected)
529+
530+
result = dti_tz - dti_tz
531+
tm.assert_index_equal(result, expected)
532+
533+
with tm.assertRaises(TypeError):
534+
dti_tz - dti
535+
536+
with tm.assertRaises(TypeError):
537+
dti - dti_tz
538+
539+
with tm.assertRaises(TypeError):
540+
dti_tz - dti_tz2
541+
469542
def test_sub_period(self):
470543
# GH 13078
471544
# not supported, check TypeError
@@ -1239,50 +1312,6 @@ def _check(result, expected):
12391312
['20121231', '20130101', '20130102'], tz='US/Eastern')
12401313
tm.assert_index_equal(result, expected)
12411314

1242-
def test_dti_dti_deprecated_ops(self):
1243-
1244-
# deprecated in 0.16.0 (GH9094)
1245-
# change to return subtraction -> TimeDeltaIndex in 0.17.0
1246-
# shoudl move to the appropriate sections above
1247-
1248-
dti = date_range('20130101', periods=3)
1249-
dti_tz = date_range('20130101', periods=3).tz_localize('US/Eastern')
1250-
1251-
with tm.assert_produces_warning(FutureWarning):
1252-
result = dti - dti
1253-
expected = Index([])
1254-
tm.assert_index_equal(result, expected)
1255-
1256-
with tm.assert_produces_warning(FutureWarning):
1257-
result = dti + dti
1258-
expected = dti
1259-
tm.assert_index_equal(result, expected)
1260-
1261-
with tm.assert_produces_warning(FutureWarning):
1262-
result = dti_tz - dti_tz
1263-
expected = Index([])
1264-
tm.assert_index_equal(result, expected)
1265-
1266-
with tm.assert_produces_warning(FutureWarning):
1267-
result = dti_tz + dti_tz
1268-
expected = dti_tz
1269-
tm.assert_index_equal(result, expected)
1270-
1271-
with tm.assert_produces_warning(FutureWarning):
1272-
result = dti_tz - dti
1273-
expected = dti_tz
1274-
tm.assert_index_equal(result, expected)
1275-
1276-
with tm.assert_produces_warning(FutureWarning):
1277-
result = dti - dti_tz
1278-
expected = dti
1279-
tm.assert_index_equal(result, expected)
1280-
1281-
with tm.assert_produces_warning(FutureWarning):
1282-
self.assertRaises(TypeError, lambda: dti_tz + dti)
1283-
with tm.assert_produces_warning(FutureWarning):
1284-
self.assertRaises(TypeError, lambda: dti + dti_tz)
1285-
12861315
def test_dti_tdi_numeric_ops(self):
12871316

12881317
# These are normally union/diff set-like ops
@@ -2053,19 +2082,17 @@ def test_add_iadd(self):
20532082
(rng7, other7, expected7)]:
20542083

20552084
# GH9094
2056-
with tm.assert_produces_warning(FutureWarning):
2057-
result_add = rng + other
2085+
with tm.assertRaises(TypeError):
2086+
rng + other
20582087

20592088
result_union = rng.union(other)
20602089

2061-
tm.assert_index_equal(result_add, expected)
20622090
tm.assert_index_equal(result_union, expected)
20632091

20642092
# GH 6527
20652093
# GH9094
2066-
with tm.assert_produces_warning(FutureWarning):
2094+
with tm.assertRaises(TypeError):
20672095
rng += other
2068-
tm.assert_index_equal(rng, expected)
20692096

20702097
# offset
20712098
# DateOffset

0 commit comments

Comments
 (0)