diff --git a/doc/source/whatsnew/v0.18.2.txt b/doc/source/whatsnew/v0.18.2.txt index 80ed23b04738a..ac9c1cf109dc0 100644 --- a/doc/source/whatsnew/v0.18.2.txt +++ b/doc/source/whatsnew/v0.18.2.txt @@ -75,3 +75,30 @@ Performance Improvements Bug Fixes ~~~~~~~~~ + + + + + + + + + + + +- Bug in ``PeriodIndex`` and ``Period`` subtraction raises ``AttributeError`` (:issue:`13071`) + + + + + + + + + + + + + + +- Bug in ``NaT`` - ``Period`` raises ``AttributeError`` (:issue:`13071`) diff --git a/pandas/src/period.pyx b/pandas/src/period.pyx index e5802ccef7495..457ca2b8ec842 100644 --- a/pandas/src/period.pyx +++ b/pandas/src/period.pyx @@ -826,26 +826,36 @@ cdef class Period(object): return NotImplemented def __sub__(self, other): - if isinstance(other, (timedelta, np.timedelta64, - offsets.Tick, offsets.DateOffset, Timedelta)): - neg_other = -other - return self + neg_other - elif lib.is_integer(other): - if self.ordinal == tslib.iNaT: - ordinal = self.ordinal - else: - ordinal = self.ordinal - other * self.freq.n - return Period(ordinal=ordinal, freq=self.freq) + if isinstance(self, Period): + if isinstance(other, (timedelta, np.timedelta64, + offsets.Tick, offsets.DateOffset, Timedelta)): + neg_other = -other + return self + neg_other + elif lib.is_integer(other): + if self.ordinal == tslib.iNaT: + ordinal = self.ordinal + else: + ordinal = self.ordinal - other * self.freq.n + return Period(ordinal=ordinal, freq=self.freq) + elif isinstance(other, Period): + if other.freq != self.freq: + raise ValueError("Cannot do arithmetic with " + "non-conforming periods") + if self.ordinal == tslib.iNaT or other.ordinal == tslib.iNaT: + return Period(ordinal=tslib.iNaT, freq=self.freq) + return self.ordinal - other.ordinal + elif getattr(other, '_typ', None) == 'periodindex': + return -other.__sub__(self) + else: # pragma: no cover + return NotImplemented elif isinstance(other, Period): - if other.freq != self.freq: - raise ValueError("Cannot do arithmetic with " - "non-conforming periods") - if self.ordinal == tslib.iNaT or other.ordinal == tslib.iNaT: - return Period(ordinal=tslib.iNaT, freq=self.freq) - return self.ordinal - other.ordinal - else: # pragma: no cover + if self is tslib.NaT: + return tslib.NaT + return NotImplemented + else: return NotImplemented + def asfreq(self, freq, how='E'): """ Convert Period to desired frequency, either at the start or end of the diff --git a/pandas/tseries/base.py b/pandas/tseries/base.py index 185d806a64fe8..f1e061bb1b9bc 100644 --- a/pandas/tseries/base.py +++ b/pandas/tseries/base.py @@ -14,6 +14,7 @@ AbstractMethodError) import pandas.formats.printing as printing import pandas.tslib as tslib +import pandas._period as prlib import pandas.lib as lib from pandas.core.index import Index from pandas.indexes.base import _index_shared_docs @@ -533,6 +534,9 @@ def _add_datelike(self, other): def _sub_datelike(self, other): raise AbstractMethodError(self) + def _sub_period(self, other): + return NotImplemented + @classmethod def _add_datetimelike_methods(cls): """ @@ -591,6 +595,8 @@ def __sub__(self, other): return self.shift(-other) elif isinstance(other, (tslib.Timestamp, datetime)): return self._sub_datelike(other) + elif isinstance(other, prlib.Period): + return self._sub_period(other) else: # pragma: no cover return NotImplemented cls.__sub__ = __sub__ diff --git a/pandas/tseries/period.py b/pandas/tseries/period.py index 478b25568d471..fb91185746181 100644 --- a/pandas/tseries/period.py +++ b/pandas/tseries/period.py @@ -4,6 +4,7 @@ import pandas.tseries.frequencies as frequencies from pandas.tseries.frequencies import get_freq_code as _gfc from pandas.tseries.index import DatetimeIndex, Int64Index, Index +from pandas.tseries.tdi import TimedeltaIndex from pandas.tseries.base import DatelikeOps, DatetimeIndexOpsMixin from pandas.tseries.tools import parse_time_string import pandas.tseries.offsets as offsets @@ -595,6 +596,32 @@ def _add_delta(self, other): ordinal_delta = self._maybe_convert_timedelta(other) return self.shift(ordinal_delta) + def _sub_datelike(self, other): + if other is tslib.NaT: + new_data = np.empty(len(self), dtype=np.int64) + new_data.fill(tslib.iNaT) + return TimedeltaIndex(new_data, name=self.name) + return NotImplemented + + def _sub_period(self, other): + if self.freq != other.freq: + msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) + raise IncompatibleFrequency(msg) + + if other.ordinal == tslib.iNaT: + new_data = np.empty(len(self)) + new_data.fill(np.nan) + else: + asi8 = self.asi8 + new_data = asi8 - other.ordinal + + if self.hasnans: + mask = asi8 == tslib.iNaT + new_data = new_data.astype(np.float64) + new_data[mask] = np.nan + # result must be Int64Index or Float64Index + return Index(new_data, name=self.name) + def shift(self, n): """ Specialized shift which produces an PeriodIndex diff --git a/pandas/tseries/tests/test_period.py b/pandas/tseries/tests/test_period.py index 12ba0b1b1bd9b..debb3e7956488 100644 --- a/pandas/tseries/tests/test_period.py +++ b/pandas/tseries/tests/test_period.py @@ -3418,6 +3418,16 @@ def test_add_offset_nat(self): with tm.assertRaises(ValueError): p + o + def test_sub_pdnat(self): + # GH 13071 + p = pd.Period('2011-01', freq='M') + self.assertIs(p - pd.NaT, pd.NaT) + self.assertIs(pd.NaT - p, pd.NaT) + + p = pd.Period('NaT', freq='M') + self.assertIs(p - pd.NaT, pd.NaT) + self.assertIs(pd.NaT - p, pd.NaT) + def test_sub_offset(self): # freq is DateOffset for freq in ['A', '2A', '3A']: @@ -3614,6 +3624,48 @@ def test_pi_ops_array(self): '2011-01-01 12:15:00'], freq='S', name='idx') self.assert_index_equal(result, exp) + def test_pi_sub_period(self): + # GH 13071 + idx = PeriodIndex(['2011-01', '2011-02', '2011-03', + '2011-04'], freq='M', name='idx') + + result = idx - pd.Period('2012-01', freq='M') + exp = pd.Index([-12, -11, -10, -9], name='idx') + tm.assert_index_equal(result, exp) + + result = pd.Period('2012-01', freq='M') - idx + exp = pd.Index([12, 11, 10, 9], name='idx') + tm.assert_index_equal(result, exp) + + exp = pd.Index([np.nan, np.nan, np.nan, np.nan], name='idx') + tm.assert_index_equal(idx - pd.Period('NaT', freq='M'), exp) + tm.assert_index_equal(pd.Period('NaT', freq='M') - idx, exp) + + def test_pi_sub_pdnat(self): + # GH 13071 + idx = PeriodIndex(['2011-01', '2011-02', 'NaT', + '2011-04'], freq='M', name='idx') + exp = pd.TimedeltaIndex([pd.NaT] * 4, name='idx') + tm.assert_index_equal(pd.NaT - idx, exp) + tm.assert_index_equal(idx - pd.NaT, exp) + + def test_pi_sub_period_nat(self): + # GH 13071 + idx = PeriodIndex(['2011-01', 'NaT', '2011-03', + '2011-04'], freq='M', name='idx') + + result = idx - pd.Period('2012-01', freq='M') + exp = pd.Index([-12, np.nan, -10, -9], name='idx') + tm.assert_index_equal(result, exp) + + result = pd.Period('2012-01', freq='M') - idx + exp = pd.Index([12, np.nan, 10, 9], name='idx') + tm.assert_index_equal(result, exp) + + exp = pd.Index([np.nan, np.nan, np.nan, np.nan], name='idx') + tm.assert_index_equal(idx - pd.Period('NaT', freq='M'), exp) + tm.assert_index_equal(pd.Period('NaT', freq='M') - idx, exp) + class TestPeriodRepresentation(tm.TestCase): """