Skip to content

Commit 492605e

Browse files
committed
Merge pull request #7741 from sinhrks/period_ops
ENH/BUG: DatetimeIndex and PeriodIndex in-place ops behaves incorrectly
2 parents 3657a51 + e6d6e10 commit 492605e

File tree

7 files changed

+274
-53
lines changed

7 files changed

+274
-53
lines changed

doc/source/v0.15.0.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ Bug Fixes
240240
- Bug in ``DataFrame.as_matrix()`` with mixed ``datetime64[ns]`` and ``timedelta64[ns]`` dtypes (:issue:`7778`)
241241
- Bug in ``HDFStore.select_column()`` not preserving UTC timezone info when selecting a DatetimeIndex (:issue:`7777`)
242242

243+
- Bug in ``DatetimeIndex`` and ``PeriodIndex`` in-place addition and subtraction cause different result from normal one (:issue:`6527`)
244+
- Bug in adding and subtracting ``PeriodIndex`` with ``PeriodIndex`` raise ``TypeError`` (:issue:`7741`)
245+
- Bug in ``combine_first`` with ``PeriodIndex`` data raises ``TypeError`` (:issue:`3367`)
246+
243247

244248
- Bug in pickles contains ``DateOffset`` may raise ``AttributeError`` when ``normalize`` attribute is reffered internally (:issue:`7748`)
245249

pandas/core/base.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
Base and utility classes for pandas objects.
33
"""
4+
import datetime
5+
46
from pandas import compat
57
import numpy as np
68
from pandas.core import common as com
@@ -511,4 +513,34 @@ def resolution(self):
511513
from pandas.tseries.frequencies import get_reso_string
512514
return get_reso_string(self._resolution)
513515

516+
def __add__(self, other):
517+
from pandas.core.index import Index
518+
from pandas.tseries.offsets import DateOffset
519+
if isinstance(other, Index):
520+
return self.union(other)
521+
elif isinstance(other, (DateOffset, datetime.timedelta, np.timedelta64)):
522+
return self._add_delta(other)
523+
elif com.is_integer(other):
524+
return self.shift(other)
525+
else: # pragma: no cover
526+
return NotImplemented
527+
528+
def __sub__(self, other):
529+
from pandas.core.index import Index
530+
from pandas.tseries.offsets import DateOffset
531+
if isinstance(other, Index):
532+
return self.diff(other)
533+
elif isinstance(other, (DateOffset, datetime.timedelta, np.timedelta64)):
534+
return self._add_delta(-other)
535+
elif com.is_integer(other):
536+
return self.shift(-other)
537+
else: # pragma: no cover
538+
return NotImplemented
539+
540+
__iadd__ = __add__
541+
__isub__ = __sub__
542+
543+
def _add_delta(self, other):
544+
return NotImplemented
545+
514546

pandas/tests/test_base.py

Lines changed: 224 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,8 @@ def test_factorize(self):
481481

482482
class TestDatetimeIndexOps(Ops):
483483
_allowed = '_allow_datetime_index_ops'
484+
tz = [None, 'UTC', 'Asia/Tokyo', 'US/Eastern',
485+
'dateutil/Asia/Singapore', 'dateutil/US/Pacific']
484486

485487
def setUp(self):
486488
super(TestDatetimeIndexOps, self).setUp()
@@ -545,7 +547,7 @@ def test_asobject_tolist(self):
545547
self.assertEqual(idx.tolist(), expected_list)
546548

547549
def test_minmax(self):
548-
for tz in [None, 'Asia/Tokyo', 'US/Eastern']:
550+
for tz in self.tz:
549551
# monotonic
550552
idx1 = pd.DatetimeIndex([pd.NaT, '2011-01-01', '2011-01-02',
551553
'2011-01-03'], tz=tz)
@@ -613,6 +615,100 @@ def test_resolution(self):
613615
idx = pd.date_range(start='2013-04-01', periods=30, freq=freq, tz=tz)
614616
self.assertEqual(idx.resolution, expected)
615617

618+
def test_add_iadd(self):
619+
for tz in self.tz:
620+
# union
621+
rng1 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
622+
other1 = pd.date_range('1/6/2000', freq='D', periods=5, tz=tz)
623+
expected1 = pd.date_range('1/1/2000', freq='D', periods=10, tz=tz)
624+
625+
rng2 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
626+
other2 = pd.date_range('1/4/2000', freq='D', periods=5, tz=tz)
627+
expected2 = pd.date_range('1/1/2000', freq='D', periods=8, tz=tz)
628+
629+
rng3 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
630+
other3 = pd.DatetimeIndex([], tz=tz)
631+
expected3 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
632+
633+
for rng, other, expected in [(rng1, other1, expected1), (rng2, other2, expected2),
634+
(rng3, other3, expected3)]:
635+
result_add = rng + other
636+
result_union = rng.union(other)
637+
638+
tm.assert_index_equal(result_add, expected)
639+
tm.assert_index_equal(result_union, expected)
640+
rng += other
641+
tm.assert_index_equal(rng, expected)
642+
643+
# offset
644+
if _np_version_under1p7:
645+
offsets = [pd.offsets.Hour(2), timedelta(hours=2)]
646+
else:
647+
offsets = [pd.offsets.Hour(2), timedelta(hours=2), np.timedelta64(2, 'h')]
648+
649+
for delta in offsets:
650+
rng = pd.date_range('2000-01-01', '2000-02-01', tz=tz)
651+
result = rng + delta
652+
expected = pd.date_range('2000-01-01 02:00', '2000-02-01 02:00', tz=tz)
653+
tm.assert_index_equal(result, expected)
654+
rng += delta
655+
tm.assert_index_equal(rng, expected)
656+
657+
# int
658+
rng = pd.date_range('2000-01-01 09:00', freq='H', periods=10, tz=tz)
659+
result = rng + 1
660+
expected = pd.date_range('2000-01-01 10:00', freq='H', periods=10, tz=tz)
661+
tm.assert_index_equal(result, expected)
662+
rng += 1
663+
tm.assert_index_equal(rng, expected)
664+
665+
def test_sub_isub(self):
666+
for tz in self.tz:
667+
# diff
668+
rng1 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
669+
other1 = pd.date_range('1/6/2000', freq='D', periods=5, tz=tz)
670+
expected1 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
671+
672+
rng2 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
673+
other2 = pd.date_range('1/4/2000', freq='D', periods=5, tz=tz)
674+
expected2 = pd.date_range('1/1/2000', freq='D', periods=3, tz=tz)
675+
676+
rng3 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
677+
other3 = pd.DatetimeIndex([], tz=tz)
678+
expected3 = pd.date_range('1/1/2000', freq='D', periods=5, tz=tz)
679+
680+
for rng, other, expected in [(rng1, other1, expected1), (rng2, other2, expected2),
681+
(rng3, other3, expected3)]:
682+
result_add = rng - other
683+
result_union = rng.diff(other)
684+
685+
tm.assert_index_equal(result_add, expected)
686+
tm.assert_index_equal(result_union, expected)
687+
rng -= other
688+
tm.assert_index_equal(rng, expected)
689+
690+
# offset
691+
if _np_version_under1p7:
692+
offsets = [pd.offsets.Hour(2), timedelta(hours=2)]
693+
else:
694+
offsets = [pd.offsets.Hour(2), timedelta(hours=2), np.timedelta64(2, 'h')]
695+
696+
for delta in offsets:
697+
rng = pd.date_range('2000-01-01', '2000-02-01', tz=tz)
698+
result = rng - delta
699+
expected = pd.date_range('1999-12-31 22:00', '2000-01-31 22:00', tz=tz)
700+
tm.assert_index_equal(result, expected)
701+
rng -= delta
702+
tm.assert_index_equal(rng, expected)
703+
704+
# int
705+
rng = pd.date_range('2000-01-01 09:00', freq='H', periods=10, tz=tz)
706+
result = rng - 1
707+
expected = pd.date_range('2000-01-01 08:00', freq='H', periods=10, tz=tz)
708+
tm.assert_index_equal(result, expected)
709+
rng -= 1
710+
tm.assert_index_equal(rng, expected)
711+
616712

617713
class TestPeriodIndexOps(Ops):
618714
_allowed = '_allow_period_index_ops'
@@ -745,6 +841,133 @@ def test_resolution(self):
745841
idx = pd.period_range(start='2013-04-01', periods=30, freq=freq)
746842
self.assertEqual(idx.resolution, expected)
747843

844+
def test_add_iadd(self):
845+
# union
846+
rng1 = pd.period_range('1/1/2000', freq='D', periods=5)
847+
other1 = pd.period_range('1/6/2000', freq='D', periods=5)
848+
expected1 = pd.period_range('1/1/2000', freq='D', periods=10)
849+
850+
rng2 = pd.period_range('1/1/2000', freq='D', periods=5)
851+
other2 = pd.period_range('1/4/2000', freq='D', periods=5)
852+
expected2 = pd.period_range('1/1/2000', freq='D', periods=8)
853+
854+
rng3 = pd.period_range('1/1/2000', freq='D', periods=5)
855+
other3 = pd.PeriodIndex([], freq='D')
856+
expected3 = pd.period_range('1/1/2000', freq='D', periods=5)
857+
858+
rng4 = pd.period_range('2000-01-01 09:00', freq='H', periods=5)
859+
other4 = pd.period_range('2000-01-02 09:00', freq='H', periods=5)
860+
expected4 = pd.PeriodIndex(['2000-01-01 09:00', '2000-01-01 10:00',
861+
'2000-01-01 11:00', '2000-01-01 12:00',
862+
'2000-01-01 13:00', '2000-01-02 09:00',
863+
'2000-01-02 10:00', '2000-01-02 11:00',
864+
'2000-01-02 12:00', '2000-01-02 13:00'],
865+
freq='H')
866+
867+
rng5 = pd.PeriodIndex(['2000-01-01 09:01', '2000-01-01 09:03',
868+
'2000-01-01 09:05'], freq='T')
869+
other5 = pd.PeriodIndex(['2000-01-01 09:01', '2000-01-01 09:05'
870+
'2000-01-01 09:08'], freq='T')
871+
expected5 = pd.PeriodIndex(['2000-01-01 09:01', '2000-01-01 09:03',
872+
'2000-01-01 09:05', '2000-01-01 09:08'],
873+
freq='T')
874+
875+
rng6 = pd.period_range('2000-01-01', freq='M', periods=7)
876+
other6 = pd.period_range('2000-04-01', freq='M', periods=7)
877+
expected6 = pd.period_range('2000-01-01', freq='M', periods=10)
878+
879+
rng7 = pd.period_range('2003-01-01', freq='A', periods=5)
880+
other7 = pd.period_range('1998-01-01', freq='A', periods=8)
881+
expected7 = pd.period_range('1998-01-01', freq='A', periods=10)
882+
883+
for rng, other, expected in [(rng1, other1, expected1), (rng2, other2, expected2),
884+
(rng3, other3, expected3), (rng4, other4, expected4),
885+
(rng5, other5, expected5), (rng6, other6, expected6),
886+
(rng7, other7, expected7)]:
887+
888+
result_add = rng + other
889+
result_union = rng.union(other)
890+
891+
tm.assert_index_equal(result_add, expected)
892+
tm.assert_index_equal(result_union, expected)
893+
# GH 6527
894+
rng += other
895+
tm.assert_index_equal(rng, expected)
896+
897+
# offset
898+
for delta in [pd.offsets.Hour(2), timedelta(hours=2)]:
899+
rng = pd.period_range('2000-01-01', '2000-02-01')
900+
with tm.assertRaisesRegexp(TypeError, 'unsupported operand type\(s\)'):
901+
result = rng + delta
902+
with tm.assertRaisesRegexp(TypeError, 'unsupported operand type\(s\)'):
903+
rng += delta
904+
905+
# int
906+
rng = pd.period_range('2000-01-01 09:00', freq='H', periods=10)
907+
result = rng + 1
908+
expected = pd.period_range('2000-01-01 10:00', freq='H', periods=10)
909+
tm.assert_index_equal(result, expected)
910+
rng += 1
911+
tm.assert_index_equal(rng, expected)
912+
913+
def test_sub_isub(self):
914+
# diff
915+
rng1 = pd.period_range('1/1/2000', freq='D', periods=5)
916+
other1 = pd.period_range('1/6/2000', freq='D', periods=5)
917+
expected1 = pd.period_range('1/1/2000', freq='D', periods=5)
918+
919+
rng2 = pd.period_range('1/1/2000', freq='D', periods=5)
920+
other2 = pd.period_range('1/4/2000', freq='D', periods=5)
921+
expected2 = pd.period_range('1/1/2000', freq='D', periods=3)
922+
923+
rng3 = pd.period_range('1/1/2000', freq='D', periods=5)
924+
other3 = pd.PeriodIndex([], freq='D')
925+
expected3 = pd.period_range('1/1/2000', freq='D', periods=5)
926+
927+
rng4 = pd.period_range('2000-01-01 09:00', freq='H', periods=5)
928+
other4 = pd.period_range('2000-01-02 09:00', freq='H', periods=5)
929+
expected4 = rng4
930+
931+
rng5 = pd.PeriodIndex(['2000-01-01 09:01', '2000-01-01 09:03',
932+
'2000-01-01 09:05'], freq='T')
933+
other5 = pd.PeriodIndex(['2000-01-01 09:01', '2000-01-01 09:05'], freq='T')
934+
expected5 = pd.PeriodIndex(['2000-01-01 09:03'], freq='T')
935+
936+
rng6 = pd.period_range('2000-01-01', freq='M', periods=7)
937+
other6 = pd.period_range('2000-04-01', freq='M', periods=7)
938+
expected6 = pd.period_range('2000-01-01', freq='M', periods=3)
939+
940+
rng7 = pd.period_range('2003-01-01', freq='A', periods=5)
941+
other7 = pd.period_range('1998-01-01', freq='A', periods=8)
942+
expected7 = pd.period_range('2006-01-01', freq='A', periods=2)
943+
944+
for rng, other, expected in [(rng1, other1, expected1), (rng2, other2, expected2),
945+
(rng3, other3, expected3), (rng4, other4, expected4),
946+
(rng5, other5, expected5), (rng6, other6, expected6),
947+
(rng7, other7, expected7),]:
948+
result_add = rng - other
949+
result_union = rng.diff(other)
950+
951+
tm.assert_index_equal(result_add, expected)
952+
tm.assert_index_equal(result_union, expected)
953+
rng -= other
954+
tm.assert_index_equal(rng, expected)
955+
956+
# offset
957+
for delta in [pd.offsets.Hour(2), timedelta(hours=2)]:
958+
with tm.assertRaisesRegexp(TypeError, 'unsupported operand type\(s\)'):
959+
result = rng + delta
960+
with tm.assertRaisesRegexp(TypeError, 'unsupported operand type\(s\)'):
961+
rng += delta
962+
963+
# int
964+
rng = pd.period_range('2000-01-01 09:00', freq='H', periods=10)
965+
result = rng - 1
966+
expected = pd.period_range('2000-01-01 08:00', freq='H', periods=10)
967+
tm.assert_index_equal(result, expected)
968+
rng -= 1
969+
tm.assert_index_equal(rng, expected)
970+
748971

749972
if __name__ == '__main__':
750973
import nose

pandas/tseries/index.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -595,30 +595,6 @@ def __setstate__(self, state):
595595
else: # pragma: no cover
596596
np.ndarray.__setstate__(self, state)
597597

598-
def __add__(self, other):
599-
if isinstance(other, Index):
600-
return self.union(other)
601-
elif isinstance(other, (DateOffset, timedelta)):
602-
return self._add_delta(other)
603-
elif isinstance(other, np.timedelta64):
604-
return self._add_delta(other)
605-
elif com.is_integer(other):
606-
return self.shift(other)
607-
else: # pragma: no cover
608-
raise TypeError(other)
609-
610-
def __sub__(self, other):
611-
if isinstance(other, Index):
612-
return self.diff(other)
613-
elif isinstance(other, (DateOffset, timedelta)):
614-
return self._add_delta(-other)
615-
elif isinstance(other, np.timedelta64):
616-
return self._add_delta(-other)
617-
elif com.is_integer(other):
618-
return self.shift(-other)
619-
else: # pragma: no cover
620-
raise TypeError(other)
621-
622598
def _add_delta(self, delta):
623599
if isinstance(delta, (Tick, timedelta)):
624600
inc = offsets._delta_to_nanoseconds(delta)

pandas/tseries/period.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -872,19 +872,6 @@ def shift(self, n):
872872
values[mask] = tslib.iNaT
873873
return PeriodIndex(data=values, name=self.name, freq=self.freq)
874874

875-
def __add__(self, other):
876-
try:
877-
return self.shift(other)
878-
except TypeError:
879-
# self.values + other raises TypeError for invalid input
880-
return NotImplemented
881-
882-
def __sub__(self, other):
883-
try:
884-
return self.shift(-other)
885-
except TypeError:
886-
return NotImplemented
887-
888875
@property
889876
def inferred_type(self):
890877
# b/c data is represented as ints make sure we can't have ambiguous

pandas/tseries/tests/test_period.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2450,6 +2450,20 @@ def test_recreate_from_data(self):
24502450
idx = PeriodIndex(org.values, freq=o)
24512451
self.assertTrue(idx.equals(org))
24522452

2453+
def test_combine_first(self):
2454+
# GH 3367
2455+
didx = pd.DatetimeIndex(start='1950-01-31', end='1950-07-31', freq='M')
2456+
pidx = pd.PeriodIndex(start=pd.Period('1950-1'), end=pd.Period('1950-7'), freq='M')
2457+
# check to be consistent with DatetimeIndex
2458+
for idx in [didx, pidx]:
2459+
a = pd.Series([1, np.nan, np.nan, 4, 5, np.nan, 7], index=idx)
2460+
b = pd.Series([9, 9, 9, 9, 9, 9, 9], index=idx)
2461+
2462+
result = a.combine_first(b)
2463+
expected = pd.Series([1, 9, 9, 4, 5, 9, 7], index=idx, dtype=np.float64)
2464+
tm.assert_series_equal(result, expected)
2465+
2466+
24532467
def _permute(obj):
24542468
return obj.take(np.random.permutation(len(obj)))
24552469

0 commit comments

Comments
 (0)