Skip to content

Commit e88ad28

Browse files
authoredSep 7, 2016
API/DEPR: Remove +/- as setops for DatetimeIndex/PeriodIndex (GH9630) (#14164)
API/DEPR: Remove +/- as setops for DatetimeIndex/PeriodIndex (GH9630) xref #13777, deprecations put in place in #9630
1 parent 844d5fb commit e88ad28

File tree

4 files changed

+165
-95
lines changed

4 files changed

+165
-95
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

+10-10
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
Base and utility classes for tseries type pandas objects.
33
"""
44

5-
import warnings
65
from datetime import datetime, timedelta
76

87
from pandas import compat
@@ -628,10 +627,9 @@ def __add__(self, other):
628627
raise TypeError("cannot add TimedeltaIndex and {typ}"
629628
.format(typ=type(other)))
630629
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)
630+
raise TypeError("cannot add {typ1} and {typ2}"
631+
.format(typ1=type(self).__name__,
632+
typ2=type(other).__name__))
635633
elif isinstance(other, (DateOffset, timedelta, np.timedelta64,
636634
tslib.Timedelta)):
637635
return self._add_delta(other)
@@ -646,20 +644,22 @@ def __add__(self, other):
646644

647645
def __sub__(self, other):
648646
from pandas.core.index import Index
647+
from pandas.tseries.index import DatetimeIndex
649648
from pandas.tseries.tdi import TimedeltaIndex
650649
from pandas.tseries.offsets import DateOffset
651650
if isinstance(other, TimedeltaIndex):
652651
return self._add_delta(-other)
653652
elif isinstance(self, TimedeltaIndex) and isinstance(other, Index):
654653
if not isinstance(other, TimedeltaIndex):
655654
raise TypeError("cannot subtract TimedeltaIndex and {typ}"
656-
.format(typ=type(other)))
655+
.format(typ=type(other).__name__))
657656
return self._add_delta(-other)
657+
elif isinstance(other, DatetimeIndex):
658+
return self._sub_datelike(other)
658659
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)
660+
raise TypeError("cannot subtract {typ1} and {typ2}"
661+
.format(typ1=type(self).__name__,
662+
typ2=type(other).__name__))
663663
elif isinstance(other, (DateOffset, timedelta, np.timedelta64,
664664
tslib.Timedelta)):
665665
return self._add_delta(-other)

‎pandas/tseries/index.py

+34-10
Original file line numberDiff line numberDiff line change
@@ -731,19 +731,43 @@ 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,
752+
fill_value=tslib.iNaT)
741753
else:
742-
i8 = self.asi8
743-
result = i8 - other.value
744-
result = self._maybe_mask_results(result, fill_value=tslib.iNaT)
754+
raise TypeError("cannot subtract DatetimeIndex and {typ}"
755+
.format(typ=type(other).__name__))
745756
return TimedeltaIndex(result, name=self.name, copy=False)
746757

758+
def _sub_datelike_dti(self, other):
759+
"""subtraction of two DatetimeIndexes"""
760+
if not len(self) == len(other):
761+
raise ValueError("cannot add indices of unequal length")
762+
763+
self_i8 = self.asi8
764+
other_i8 = other.asi8
765+
new_values = self_i8 - other_i8
766+
if self.hasnans or other.hasnans:
767+
mask = (self._isnan) | (other._isnan)
768+
new_values[mask] = tslib.iNaT
769+
return new_values.view('i8')
770+
747771
def _maybe_update_attributes(self, attrs):
748772
""" Update Index attributes (e.g. freq) depending on op """
749773
freq = attrs.get('freq', None)

‎pandas/tseries/tests/test_base.py

+99-72
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,12 @@ 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):
390-
rng += other
391-
tm.assert_index_equal(rng, expected)
384+
385+
def test_add_iadd(self):
386+
for tz in self.tz:
392387

393388
# offset
394389
offsets = [pd.offsets.Hour(2), timedelta(hours=2),
@@ -421,7 +416,26 @@ def test_add_iadd(self):
421416
with tm.assertRaisesRegexp(TypeError, msg):
422417
Timestamp('2011-01-01') + idx
423418

424-
def test_sub_isub(self):
419+
def test_add_dti_dti(self):
420+
# previously performed setop (deprecated in 0.16.0), now raises
421+
# TypeError (GH14164)
422+
423+
dti = date_range('20130101', periods=3)
424+
dti_tz = date_range('20130101', periods=3).tz_localize('US/Eastern')
425+
426+
with tm.assertRaises(TypeError):
427+
dti + dti
428+
429+
with tm.assertRaises(TypeError):
430+
dti_tz + dti_tz
431+
432+
with tm.assertRaises(TypeError):
433+
dti_tz + dti
434+
435+
with tm.assertRaises(TypeError):
436+
dti + dti_tz
437+
438+
def test_difference(self):
425439
for tz in self.tz:
426440
# diff
427441
rng1 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
@@ -439,19 +453,22 @@ def test_sub_isub(self):
439453
for rng, other, expected in [(rng1, other1, expected1),
440454
(rng2, other2, expected2),
441455
(rng3, other3, expected3)]:
442-
result_union = rng.difference(other)
456+
result_diff = rng.difference(other)
457+
tm.assert_index_equal(result_diff, expected)
443458

444-
tm.assert_index_equal(result_union, expected)
459+
def test_sub_isub(self):
460+
for tz in self.tz:
445461

446462
# offset
447463
offsets = [pd.offsets.Hour(2), timedelta(hours=2),
448464
np.timedelta64(2, 'h'), Timedelta(hours=2)]
449465

450466
for delta in offsets:
451467
rng = pd.date_range('2000-01-01', '2000-02-01', tz=tz)
452-
result = rng - delta
453468
expected = pd.date_range('1999-12-31 22:00',
454469
'2000-01-31 22:00', tz=tz)
470+
471+
result = rng - delta
455472
tm.assert_index_equal(result, expected)
456473
rng -= delta
457474
tm.assert_index_equal(rng, expected)
@@ -466,6 +483,47 @@ def test_sub_isub(self):
466483
rng -= 1
467484
tm.assert_index_equal(rng, expected)
468485

486+
def test_sub_dti_dti(self):
487+
# previously performed setop (deprecated in 0.16.0), now changed to
488+
# return subtraction -> TimeDeltaIndex (GH ...)
489+
490+
dti = date_range('20130101', periods=3)
491+
dti_tz = date_range('20130101', periods=3).tz_localize('US/Eastern')
492+
dti_tz2 = date_range('20130101', periods=3).tz_localize('UTC')
493+
expected = TimedeltaIndex([0, 0, 0])
494+
495+
result = dti - dti
496+
tm.assert_index_equal(result, expected)
497+
498+
result = dti_tz - dti_tz
499+
tm.assert_index_equal(result, expected)
500+
501+
with tm.assertRaises(TypeError):
502+
dti_tz - dti
503+
504+
with tm.assertRaises(TypeError):
505+
dti - dti_tz
506+
507+
with tm.assertRaises(TypeError):
508+
dti_tz - dti_tz2
509+
510+
# isub
511+
dti -= dti
512+
tm.assert_index_equal(dti, expected)
513+
514+
# different length raises ValueError
515+
dti1 = date_range('20130101', periods=3)
516+
dti2 = date_range('20130101', periods=4)
517+
with tm.assertRaises(ValueError):
518+
dti1 - dti2
519+
520+
# NaN propagation
521+
dti1 = DatetimeIndex(['2012-01-01', np.nan, '2012-01-03'])
522+
dti2 = DatetimeIndex(['2012-01-02', '2012-01-03', np.nan])
523+
expected = TimedeltaIndex(['1 days', np.nan, np.nan])
524+
result = dti2 - dti1
525+
tm.assert_index_equal(result, expected)
526+
469527
def test_sub_period(self):
470528
# GH 13078
471529
# not supported, check TypeError
@@ -1239,50 +1297,6 @@ def _check(result, expected):
12391297
['20121231', '20130101', '20130102'], tz='US/Eastern')
12401298
tm.assert_index_equal(result, expected)
12411299

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-
12861300
def test_dti_tdi_numeric_ops(self):
12871301

12881302
# These are normally union/diff set-like ops
@@ -2005,7 +2019,7 @@ def test_resolution(self):
20052019
idx = pd.period_range(start='2013-04-01', periods=30, freq=freq)
20062020
self.assertEqual(idx.resolution, expected)
20072021

2008-
def test_add_iadd(self):
2022+
def test_union(self):
20092023
# union
20102024
rng1 = pd.period_range('1/1/2000', freq='D', periods=5)
20112025
other1 = pd.period_range('1/6/2000', freq='D', periods=5)
@@ -2031,7 +2045,8 @@ def test_add_iadd(self):
20312045
rng5 = pd.PeriodIndex(['2000-01-01 09:01', '2000-01-01 09:03',
20322046
'2000-01-01 09:05'], freq='T')
20332047
other5 = pd.PeriodIndex(['2000-01-01 09:01', '2000-01-01 09:05'
2034-
'2000-01-01 09:08'], freq='T')
2048+
'2000-01-01 09:08'],
2049+
freq='T')
20352050
expected5 = pd.PeriodIndex(['2000-01-01 09:01', '2000-01-01 09:03',
20362051
'2000-01-01 09:05', '2000-01-01 09:08'],
20372052
freq='T')
@@ -2052,20 +2067,19 @@ def test_add_iadd(self):
20522067
expected6),
20532068
(rng7, other7, expected7)]:
20542069

2055-
# GH9094
2056-
with tm.assert_produces_warning(FutureWarning):
2057-
result_add = rng + other
2058-
20592070
result_union = rng.union(other)
2060-
2061-
tm.assert_index_equal(result_add, expected)
20622071
tm.assert_index_equal(result_union, expected)
20632072

2064-
# GH 6527
2065-
# GH9094
2066-
with tm.assert_produces_warning(FutureWarning):
2067-
rng += other
2068-
tm.assert_index_equal(rng, expected)
2073+
def test_add_iadd(self):
2074+
rng = pd.period_range('1/1/2000', freq='D', periods=5)
2075+
other = pd.period_range('1/6/2000', freq='D', periods=5)
2076+
2077+
# previously performed setop union, now raises TypeError (GH14164)
2078+
with tm.assertRaises(TypeError):
2079+
rng + other
2080+
2081+
with tm.assertRaises(TypeError):
2082+
rng += other
20692083

20702084
# offset
20712085
# DateOffset
@@ -2152,7 +2166,7 @@ def test_add_iadd(self):
21522166
rng += 1
21532167
tm.assert_index_equal(rng, expected)
21542168

2155-
def test_sub_isub(self):
2169+
def test_difference(self):
21562170
# diff
21572171
rng1 = pd.period_range('1/1/2000', freq='D', periods=5)
21582172
other1 = pd.period_range('1/6/2000', freq='D', periods=5)
@@ -2194,6 +2208,19 @@ def test_sub_isub(self):
21942208
result_union = rng.difference(other)
21952209
tm.assert_index_equal(result_union, expected)
21962210

2211+
def test_sub_isub(self):
2212+
2213+
# previously performed setop, now raises TypeError (GH14164)
2214+
# TODO needs to wait on #13077 for decision on result type
2215+
rng = pd.period_range('1/1/2000', freq='D', periods=5)
2216+
other = pd.period_range('1/6/2000', freq='D', periods=5)
2217+
2218+
with tm.assertRaises(TypeError):
2219+
rng - other
2220+
2221+
with tm.assertRaises(TypeError):
2222+
rng -= other
2223+
21972224
# offset
21982225
# DateOffset
21992226
rng = pd.period_range('2014', '2024', freq='A')

0 commit comments

Comments
 (0)
Please sign in to comment.