From 003727340422d1dda775c10893da76bdf66bef28 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 4 Jun 2018 07:39:09 -0700 Subject: [PATCH 1/7] update tests to reflect changed Period subtraction return type --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/_libs/tslibs/period.pyx | 7 +++++-- pandas/core/indexes/datetimelike.py | 7 ++++++- pandas/core/indexes/period.py | 10 ++++++---- pandas/tests/indexes/period/test_arithmetic.py | 15 +++++++++------ pandas/tests/scalar/period/test_period.py | 7 ++++--- pandas/tests/series/test_arithmetic.py | 5 +++-- 7 files changed, 34 insertions(+), 18 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 6cbc19cca99e1..c369e7b92dc93 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -29,6 +29,7 @@ Datetimelike API Changes ^^^^^^^^^^^^^^^^^^^^^^^^ - For :class:`DatetimeIndex` and :class:`TimedeltaIndex` with non-``None`` ``freq`` attribute, addition or subtraction of integer-dtyped array or ``Index`` will return an object of the same class (:issue:`19959`) +- Subtraction of :class:`Period` from :class:`Period` will return a :class:`DateOffset` object instead of an integer (:issue:`????`) .. _whatsnew_0240.api.other: diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 008747c0a9e78..b532d7277a386 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1120,9 +1120,12 @@ cdef class _Period(object): if other.freq != self.freq: msg = _DIFFERENT_FREQ.format(self.freqstr, other.freqstr) raise IncompatibleFrequency(msg) - return self.ordinal - other.ordinal + return (self.ordinal - other.ordinal) * self.freq elif getattr(other, '_typ', None) == 'periodindex': - return -other.__sub__(self) + # GH#??? PeriodIndex - Period returns an object-index + # of DateOffset objects, for which we cannot use __neg__ + # directly, so we have to apply it pointwise + return other.__sub__(self).map(lambda x: -x) else: # pragma: no cover return NotImplemented elif is_period_object(other): diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index c7cb245263df8..a47dfe03445f5 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -899,7 +899,9 @@ def __add__(self, other): raise TypeError("cannot add {dtype}-dtype to {cls}" .format(dtype=other.dtype, cls=type(self).__name__)) - + elif is_categorical_dtype(other): + # Categorical op will raise; defer explicitly + return NotImplemented else: # pragma: no cover return NotImplemented @@ -964,6 +966,9 @@ def __sub__(self, other): raise TypeError("cannot subtract {dtype}-dtype from {cls}" .format(dtype=other.dtype, cls=type(self).__name__)) + elif is_categorical_dtype(other): + # Categorical op will raise; defer explicitly + return NotImplemented else: # pragma: no cover return NotImplemented diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index b9e8f9028dbf7..c8752651b1cbe 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -557,7 +557,7 @@ def is_full(self): return True if not self.is_monotonic: raise ValueError('Index is not monotonic') - values = self.values + values = self.asi8 return ((values[1:] - values[:-1]) < 2).all() @property @@ -761,17 +761,19 @@ def _sub_datelike(self, other): return NotImplemented def _sub_period(self, other): + # If the operation is well-defined, we return an object-Index + # of DateOffsets. Null entries are filled with pd.NaT if self.freq != other.freq: msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) raise IncompatibleFrequency(msg) asi8 = self.asi8 new_data = asi8 - other.ordinal + new_data = np.array([self.freq * x for x in new_data]) if self.hasnans: - new_data = new_data.astype(np.float64) - new_data[self._isnan] = np.nan - # result must be Int64Index or Float64Index + new_data[self._isnan] = tslib.NaT + return Index(new_data) def shift(self, n): diff --git a/pandas/tests/indexes/period/test_arithmetic.py b/pandas/tests/indexes/period/test_arithmetic.py index aea019d910fe0..3a6ca14400dff 100644 --- a/pandas/tests/indexes/period/test_arithmetic.py +++ b/pandas/tests/indexes/period/test_arithmetic.py @@ -730,11 +730,12 @@ def test_pi_ops(self): self._check(idx + 2, lambda x: x - 2, idx) result = idx - Period('2011-01', freq='M') - exp = pd.Index([0, 1, 2, 3], name='idx') + off = idx.freq + exp = pd.Index([0 * off, 1 * off, 2 * off, 3 * off], name='idx') tm.assert_index_equal(result, exp) result = Period('2011-01', freq='M') - idx - exp = pd.Index([0, -1, -2, -3], name='idx') + exp = pd.Index([0 * off, -1 * off, -2 * off, -3 * off], name='idx') tm.assert_index_equal(result, exp) @pytest.mark.parametrize('ng', ["str", 1.5]) @@ -864,14 +865,15 @@ def test_pi_sub_period(self): freq='M', name='idx') result = idx - pd.Period('2012-01', freq='M') - exp = pd.Index([-12, -11, -10, -9], name='idx') + off = idx.freq + exp = pd.Index([-12 * off, -11 * off, -10 * off, -9 * off], name='idx') tm.assert_index_equal(result, exp) result = np.subtract(idx, pd.Period('2012-01', freq='M')) tm.assert_index_equal(result, exp) result = pd.Period('2012-01', freq='M') - idx - exp = pd.Index([12, 11, 10, 9], name='idx') + exp = pd.Index([12 * off, 11 * off, 10 * off, 9 * off], name='idx') tm.assert_index_equal(result, exp) result = np.subtract(pd.Period('2012-01', freq='M'), idx) @@ -898,11 +900,12 @@ def test_pi_sub_period_nat(self): freq='M', name='idx') result = idx - pd.Period('2012-01', freq='M') - exp = pd.Index([-12, np.nan, -10, -9], name='idx') + off = idx.freq + exp = pd.Index([-12 * off, pd.NaT, -10 * off, -9 * off], 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') + exp = pd.Index([12 * off, pd.NaT, 10 * off, 9 * off], name='idx') tm.assert_index_equal(result, exp) exp = pd.TimedeltaIndex([np.nan, np.nan, np.nan, np.nan], name='idx') diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index f43ab0704f0f4..ffc375ba12e34 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -572,7 +572,7 @@ def test_strftime(self): def test_sub_delta(self): left, right = Period('2011', freq='A'), Period('2007', freq='A') result = left - right - assert result == 4 + assert result == 4 * right.freq with pytest.raises(period.IncompatibleFrequency): left - Period('2007-01', freq='M') @@ -1064,8 +1064,9 @@ def test_sub(self): dt1 = Period('2011-01-01', freq='D') dt2 = Period('2011-01-15', freq='D') - assert dt1 - dt2 == -14 - assert dt2 - dt1 == 14 + off = dt1.freq + assert dt1 - dt2 == -14 * off + assert dt2 - dt1 == 14 * off msg = r"Input has different freq=M from Period\(freq=D\)" with tm.assert_raises_regex(period.IncompatibleFrequency, msg): diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index ec0d7296e540e..9b853e06e77ca 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -477,8 +477,9 @@ def test_ops_series_period(self): assert ser.dtype == object per = pd.Period('2015-01-10', freq='D') + off = per.freq # dtype will be object because of original dtype - expected = pd.Series([9, 8], name='xxx', dtype=object) + expected = pd.Series([9 * off, 8 * off], name='xxx', dtype=object) tm.assert_series_equal(per - ser, expected) tm.assert_series_equal(ser - per, -1 * expected) @@ -486,7 +487,7 @@ def test_ops_series_period(self): pd.Period('2015-01-04', freq='D')], name='xxx') assert s2.dtype == object - expected = pd.Series([4, 2], name='xxx', dtype=object) + expected = pd.Series([4 * off, 2 * off], name='xxx', dtype=object) tm.assert_series_equal(s2 - ser, expected) tm.assert_series_equal(ser - s2, -1 * expected) From b981d3214791b90eb54b682edbc57e062644f6cd Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 4 Jun 2018 07:48:15 -0700 Subject: [PATCH 2/7] update GH reference in comments and whatsnew --- doc/source/whatsnew/v0.24.0.txt | 3 ++- pandas/_libs/tslibs/period.pyx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index c369e7b92dc93..73b746f0d59e0 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -29,7 +29,8 @@ Datetimelike API Changes ^^^^^^^^^^^^^^^^^^^^^^^^ - For :class:`DatetimeIndex` and :class:`TimedeltaIndex` with non-``None`` ``freq`` attribute, addition or subtraction of integer-dtyped array or ``Index`` will return an object of the same class (:issue:`19959`) -- Subtraction of :class:`Period` from :class:`Period` will return a :class:`DateOffset` object instead of an integer (:issue:`????`) +- Subtraction of :class:`Period` from :class:`Period` will return a :class:`DateOffset` object instead of an integer (:issue:`21314`) +- Subtraction of :class:`Period` from :class:`PeriodIndex` (or vice-versa) will return an object-dtype :class:`Index` instead of :class:`Int64Index` (:issue:`21314`) .. _whatsnew_0240.api.other: diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index b532d7277a386..af21ad6ca1f58 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1122,7 +1122,7 @@ cdef class _Period(object): raise IncompatibleFrequency(msg) return (self.ordinal - other.ordinal) * self.freq elif getattr(other, '_typ', None) == 'periodindex': - # GH#??? PeriodIndex - Period returns an object-index + # GH#21314 PeriodIndex - Period returns an object-index # of DateOffset objects, for which we cannot use __neg__ # directly, so we have to apply it pointwise return other.__sub__(self).map(lambda x: -x) From 5be05ee0f338394288b9b3c0030bd4bb77b43e8e Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 4 Jun 2018 08:20:02 -0700 Subject: [PATCH 3/7] update frame Period __sub__ test --- pandas/tests/frame/test_arithmetic.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index 65afe85628f8e..fb381a5640519 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -258,9 +258,10 @@ def test_ops_frame_period(self): assert df['B'].dtype == object p = pd.Period('2015-03', freq='M') + off = p.freq # dtype will be object because of original dtype - exp = pd.DataFrame({'A': np.array([2, 1], dtype=object), - 'B': np.array([14, 13], dtype=object)}) + exp = pd.DataFrame({'A': np.array([2 * off, 1 * off], dtype=object), + 'B': np.array([14 * off, 13 * off], dtype=object)}) tm.assert_frame_equal(p - df, exp) tm.assert_frame_equal(df - p, -1 * exp) @@ -271,7 +272,7 @@ def test_ops_frame_period(self): assert df2['A'].dtype == object assert df2['B'].dtype == object - exp = pd.DataFrame({'A': np.array([4, 4], dtype=object), - 'B': np.array([16, 16], dtype=object)}) + exp = pd.DataFrame({'A': np.array([4 * off, 4 * off], dtype=object), + 'B': np.array([16 * off, 16 * off], dtype=object)}) tm.assert_frame_equal(df2 - df, exp) tm.assert_frame_equal(df - df2, -1 * exp) From 6c1f5f99d94386a6eb2d75d02d228aefa094b213 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 5 Jun 2018 18:20:30 -0700 Subject: [PATCH 4/7] update docstring --- pandas/core/indexes/period.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index c8752651b1cbe..c689dfe49169f 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -551,7 +551,8 @@ def is_all_dates(self): @property def is_full(self): """ - Returns True if there are any missing periods from start to end + Returns True if this PeriodIndex is range-like in that all Periods + between start and end are present, in order. """ if len(self) == 0: return True From 3f1b65c35341c9449bf1157b782e80c8128c3732 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 5 Jun 2018 18:35:09 -0700 Subject: [PATCH 5/7] period subtraction docs section --- doc/source/whatsnew/v0.24.0.txt | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 73b746f0d59e0..d50f41e3e4cef 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -25,6 +25,46 @@ Backwards incompatible API changes .. _whatsnew_0240.api.datetimelike: +Period Subtraction +^^^^^^^^^^^^^^^^^^ + +Subtraction of a ``Period`` from another ``Period`` will give a ``DateOffset``. +instead of an integer (:issue:`21314`) + +.. ipython:: python + + june = pd.Period('June 2018') + april = pd.Period('April 2018') + june - april + +Previous Behavior: + +.. code-block:: ipython + + In [2]: june = pd.Period('June 2018') + + In [3]: april = pd.Period('April 2018') + + In [4]: june - april + Out [4]: 2 + +Similarly, subtraction of a ``Period`` from a ``PeriodIndex`` will not return +an ``Index`` of ``DateOffset`` objects instead of an ``Int64Index`` + +.. ipython:: python + + pi = pd.period_range('June 2018', freq='M', periods=3) + pi - pi[0] + +Previous Behavior: + +.. code-block:: ipython + + In [2]: pi = pd.period_range('June 2018', freq='M', periods=3) + + In [3]: pi - pi[0] + Out[3]: Int64Index([0, 1, 2], dtype='int64') + Datetimelike API Changes ^^^^^^^^^^^^^^^^^^^^^^^^ From d205afce2aeea8f9d45ff3c6109acfc563a0232b Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 18 Jun 2018 17:54:43 -0700 Subject: [PATCH 6/7] remove redundant whatsnew entry, typo fixup, add period_subtraction section --- doc/source/whatsnew/v0.24.0.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index c9d02b6f8beaa..120c2f2ff243c 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -61,6 +61,9 @@ Current Behavior: .. _whatsnew_0240.api.datetimelike: + +.. _whatsnew_0240.api.period_subtraction: + Period Subtraction ^^^^^^^^^^^^^^^^^^ @@ -84,7 +87,7 @@ Previous Behavior: In [4]: june - april Out [4]: 2 -Similarly, subtraction of a ``Period`` from a ``PeriodIndex`` will not return +Similarly, subtraction of a ``Period`` from a ``PeriodIndex`` will now return an ``Index`` of ``DateOffset`` objects instead of an ``Int64Index`` .. ipython:: python @@ -105,7 +108,6 @@ Datetimelike API Changes ^^^^^^^^^^^^^^^^^^^^^^^^ - For :class:`DatetimeIndex` and :class:`TimedeltaIndex` with non-``None`` ``freq`` attribute, addition or subtraction of integer-dtyped array or ``Index`` will return an object of the same class (:issue:`19959`) -- Subtraction of :class:`Period` from :class:`Period` will return a :class:`DateOffset` object instead of an integer (:issue:`21314`) - Subtraction of :class:`Period` from :class:`PeriodIndex` (or vice-versa) will return an object-dtype :class:`Index` instead of :class:`Int64Index` (:issue:`21314`) .. _whatsnew_0240.api.other: From efd7c752dff40c4bd49eca576c0ccf259778ea2b Mon Sep 17 00:00:00 2001 From: Jeff Reback Date: Tue, 26 Jun 2018 18:56:27 -0400 Subject: [PATCH 7/7] minor doc --- doc/source/whatsnew/v0.24.0.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 2f9da9284cc24..41be4cb0053ff 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -117,7 +117,6 @@ Datetimelike API Changes - For :class:`DatetimeIndex` and :class:`TimedeltaIndex` with non-``None`` ``freq`` attribute, addition or subtraction of integer-dtyped array or ``Index`` will return an object of the same class (:issue:`19959`) - :class:`DateOffset` objects are now immutable. Attempting to alter one of these will now raise ``AttributeError`` (:issue:`21341`) -- Subtraction of :class:`Period` from :class:`PeriodIndex` (or vice-versa) will return an object-dtype :class:`Index` instead of :class:`Int64Index` (:issue:`21314`) .. _whatsnew_0240.api.other: