Skip to content

Commit 2cd1480

Browse files
sinhrksjreback
authored andcommitted
BUG: PeriodIndex and Period subtraction error
Similar to #5202. Author: sinhrks <[email protected]> Closes #13071 from sinhrks/period_period_sub and squashes the following commits: 0f0951f [sinhrks] BUG: PeriodIndex and Period subtraction error
1 parent 437654c commit 2cd1480

File tree

5 files changed

+139
-17
lines changed

5 files changed

+139
-17
lines changed

doc/source/whatsnew/v0.18.2.txt

+27
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,30 @@ Performance Improvements
7575

7676
Bug Fixes
7777
~~~~~~~~~
78+
79+
80+
81+
82+
83+
84+
85+
86+
87+
88+
89+
- Bug in ``PeriodIndex`` and ``Period`` subtraction raises ``AttributeError`` (:issue:`13071`)
90+
91+
92+
93+
94+
95+
96+
97+
98+
99+
100+
101+
102+
103+
104+
- Bug in ``NaT`` - ``Period`` raises ``AttributeError`` (:issue:`13071`)

pandas/src/period.pyx

+27-17
Original file line numberDiff line numberDiff line change
@@ -826,26 +826,36 @@ cdef class Period(object):
826826
return NotImplemented
827827

828828
def __sub__(self, other):
829-
if isinstance(other, (timedelta, np.timedelta64,
830-
offsets.Tick, offsets.DateOffset, Timedelta)):
831-
neg_other = -other
832-
return self + neg_other
833-
elif lib.is_integer(other):
834-
if self.ordinal == tslib.iNaT:
835-
ordinal = self.ordinal
836-
else:
837-
ordinal = self.ordinal - other * self.freq.n
838-
return Period(ordinal=ordinal, freq=self.freq)
829+
if isinstance(self, Period):
830+
if isinstance(other, (timedelta, np.timedelta64,
831+
offsets.Tick, offsets.DateOffset, Timedelta)):
832+
neg_other = -other
833+
return self + neg_other
834+
elif lib.is_integer(other):
835+
if self.ordinal == tslib.iNaT:
836+
ordinal = self.ordinal
837+
else:
838+
ordinal = self.ordinal - other * self.freq.n
839+
return Period(ordinal=ordinal, freq=self.freq)
840+
elif isinstance(other, Period):
841+
if other.freq != self.freq:
842+
raise ValueError("Cannot do arithmetic with "
843+
"non-conforming periods")
844+
if self.ordinal == tslib.iNaT or other.ordinal == tslib.iNaT:
845+
return Period(ordinal=tslib.iNaT, freq=self.freq)
846+
return self.ordinal - other.ordinal
847+
elif getattr(other, '_typ', None) == 'periodindex':
848+
return -other.__sub__(self)
849+
else: # pragma: no cover
850+
return NotImplemented
839851
elif isinstance(other, Period):
840-
if other.freq != self.freq:
841-
raise ValueError("Cannot do arithmetic with "
842-
"non-conforming periods")
843-
if self.ordinal == tslib.iNaT or other.ordinal == tslib.iNaT:
844-
return Period(ordinal=tslib.iNaT, freq=self.freq)
845-
return self.ordinal - other.ordinal
846-
else: # pragma: no cover
852+
if self is tslib.NaT:
853+
return tslib.NaT
854+
return NotImplemented
855+
else:
847856
return NotImplemented
848857

858+
849859
def asfreq(self, freq, how='E'):
850860
"""
851861
Convert Period to desired frequency, either at the start or end of the

pandas/tseries/base.py

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
AbstractMethodError)
1515
import pandas.formats.printing as printing
1616
import pandas.tslib as tslib
17+
import pandas._period as prlib
1718
import pandas.lib as lib
1819
from pandas.core.index import Index
1920
from pandas.indexes.base import _index_shared_docs
@@ -533,6 +534,9 @@ def _add_datelike(self, other):
533534
def _sub_datelike(self, other):
534535
raise AbstractMethodError(self)
535536

537+
def _sub_period(self, other):
538+
return NotImplemented
539+
536540
@classmethod
537541
def _add_datetimelike_methods(cls):
538542
"""
@@ -591,6 +595,8 @@ def __sub__(self, other):
591595
return self.shift(-other)
592596
elif isinstance(other, (tslib.Timestamp, datetime)):
593597
return self._sub_datelike(other)
598+
elif isinstance(other, prlib.Period):
599+
return self._sub_period(other)
594600
else: # pragma: no cover
595601
return NotImplemented
596602
cls.__sub__ = __sub__

pandas/tseries/period.py

+27
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pandas.tseries.frequencies as frequencies
55
from pandas.tseries.frequencies import get_freq_code as _gfc
66
from pandas.tseries.index import DatetimeIndex, Int64Index, Index
7+
from pandas.tseries.tdi import TimedeltaIndex
78
from pandas.tseries.base import DatelikeOps, DatetimeIndexOpsMixin
89
from pandas.tseries.tools import parse_time_string
910
import pandas.tseries.offsets as offsets
@@ -595,6 +596,32 @@ def _add_delta(self, other):
595596
ordinal_delta = self._maybe_convert_timedelta(other)
596597
return self.shift(ordinal_delta)
597598

599+
def _sub_datelike(self, other):
600+
if other is tslib.NaT:
601+
new_data = np.empty(len(self), dtype=np.int64)
602+
new_data.fill(tslib.iNaT)
603+
return TimedeltaIndex(new_data, name=self.name)
604+
return NotImplemented
605+
606+
def _sub_period(self, other):
607+
if self.freq != other.freq:
608+
msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr)
609+
raise IncompatibleFrequency(msg)
610+
611+
if other.ordinal == tslib.iNaT:
612+
new_data = np.empty(len(self))
613+
new_data.fill(np.nan)
614+
else:
615+
asi8 = self.asi8
616+
new_data = asi8 - other.ordinal
617+
618+
if self.hasnans:
619+
mask = asi8 == tslib.iNaT
620+
new_data = new_data.astype(np.float64)
621+
new_data[mask] = np.nan
622+
# result must be Int64Index or Float64Index
623+
return Index(new_data, name=self.name)
624+
598625
def shift(self, n):
599626
"""
600627
Specialized shift which produces an PeriodIndex

pandas/tseries/tests/test_period.py

+52
Original file line numberDiff line numberDiff line change
@@ -3418,6 +3418,16 @@ def test_add_offset_nat(self):
34183418
with tm.assertRaises(ValueError):
34193419
p + o
34203420

3421+
def test_sub_pdnat(self):
3422+
# GH 13071
3423+
p = pd.Period('2011-01', freq='M')
3424+
self.assertIs(p - pd.NaT, pd.NaT)
3425+
self.assertIs(pd.NaT - p, pd.NaT)
3426+
3427+
p = pd.Period('NaT', freq='M')
3428+
self.assertIs(p - pd.NaT, pd.NaT)
3429+
self.assertIs(pd.NaT - p, pd.NaT)
3430+
34213431
def test_sub_offset(self):
34223432
# freq is DateOffset
34233433
for freq in ['A', '2A', '3A']:
@@ -3614,6 +3624,48 @@ def test_pi_ops_array(self):
36143624
'2011-01-01 12:15:00'], freq='S', name='idx')
36153625
self.assert_index_equal(result, exp)
36163626

3627+
def test_pi_sub_period(self):
3628+
# GH 13071
3629+
idx = PeriodIndex(['2011-01', '2011-02', '2011-03',
3630+
'2011-04'], freq='M', name='idx')
3631+
3632+
result = idx - pd.Period('2012-01', freq='M')
3633+
exp = pd.Index([-12, -11, -10, -9], name='idx')
3634+
tm.assert_index_equal(result, exp)
3635+
3636+
result = pd.Period('2012-01', freq='M') - idx
3637+
exp = pd.Index([12, 11, 10, 9], name='idx')
3638+
tm.assert_index_equal(result, exp)
3639+
3640+
exp = pd.Index([np.nan, np.nan, np.nan, np.nan], name='idx')
3641+
tm.assert_index_equal(idx - pd.Period('NaT', freq='M'), exp)
3642+
tm.assert_index_equal(pd.Period('NaT', freq='M') - idx, exp)
3643+
3644+
def test_pi_sub_pdnat(self):
3645+
# GH 13071
3646+
idx = PeriodIndex(['2011-01', '2011-02', 'NaT',
3647+
'2011-04'], freq='M', name='idx')
3648+
exp = pd.TimedeltaIndex([pd.NaT] * 4, name='idx')
3649+
tm.assert_index_equal(pd.NaT - idx, exp)
3650+
tm.assert_index_equal(idx - pd.NaT, exp)
3651+
3652+
def test_pi_sub_period_nat(self):
3653+
# GH 13071
3654+
idx = PeriodIndex(['2011-01', 'NaT', '2011-03',
3655+
'2011-04'], freq='M', name='idx')
3656+
3657+
result = idx - pd.Period('2012-01', freq='M')
3658+
exp = pd.Index([-12, np.nan, -10, -9], name='idx')
3659+
tm.assert_index_equal(result, exp)
3660+
3661+
result = pd.Period('2012-01', freq='M') - idx
3662+
exp = pd.Index([12, np.nan, 10, 9], name='idx')
3663+
tm.assert_index_equal(result, exp)
3664+
3665+
exp = pd.Index([np.nan, np.nan, np.nan, np.nan], name='idx')
3666+
tm.assert_index_equal(idx - pd.Period('NaT', freq='M'), exp)
3667+
tm.assert_index_equal(pd.Period('NaT', freq='M') - idx, exp)
3668+
36173669

36183670
class TestPeriodRepresentation(tm.TestCase):
36193671
"""

0 commit comments

Comments
 (0)