Skip to content

Commit 5afb953

Browse files
jbrockmendeljreback
authored andcommitted
Make Period - Period return DateOffset instead of int (#21314)
1 parent 66b517c commit 5afb953

File tree

8 files changed

+83
-23
lines changed

8 files changed

+83
-23
lines changed

doc/source/whatsnew/v0.24.0.txt

+43
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,49 @@ Current Behavior:
7070

7171
.. _whatsnew_0240.api.datetimelike:
7272

73+
74+
.. _whatsnew_0240.api.period_subtraction:
75+
76+
Period Subtraction
77+
^^^^^^^^^^^^^^^^^^
78+
79+
Subtraction of a ``Period`` from another ``Period`` will give a ``DateOffset``.
80+
instead of an integer (:issue:`21314`)
81+
82+
.. ipython:: python
83+
84+
june = pd.Period('June 2018')
85+
april = pd.Period('April 2018')
86+
june - april
87+
88+
Previous Behavior:
89+
90+
.. code-block:: ipython
91+
92+
In [2]: june = pd.Period('June 2018')
93+
94+
In [3]: april = pd.Period('April 2018')
95+
96+
In [4]: june - april
97+
Out [4]: 2
98+
99+
Similarly, subtraction of a ``Period`` from a ``PeriodIndex`` will now return
100+
an ``Index`` of ``DateOffset`` objects instead of an ``Int64Index``
101+
102+
.. ipython:: python
103+
104+
pi = pd.period_range('June 2018', freq='M', periods=3)
105+
pi - pi[0]
106+
107+
Previous Behavior:
108+
109+
.. code-block:: ipython
110+
111+
In [2]: pi = pd.period_range('June 2018', freq='M', periods=3)
112+
113+
In [3]: pi - pi[0]
114+
Out[3]: Int64Index([0, 1, 2], dtype='int64')
115+
73116
Datetimelike API Changes
74117
^^^^^^^^^^^^^^^^^^^^^^^^
75118

pandas/_libs/tslibs/period.pyx

+5-2
Original file line numberDiff line numberDiff line change
@@ -1123,9 +1123,12 @@ cdef class _Period(object):
11231123
if other.freq != self.freq:
11241124
msg = _DIFFERENT_FREQ.format(self.freqstr, other.freqstr)
11251125
raise IncompatibleFrequency(msg)
1126-
return self.ordinal - other.ordinal
1126+
return (self.ordinal - other.ordinal) * self.freq
11271127
elif getattr(other, '_typ', None) == 'periodindex':
1128-
return -other.__sub__(self)
1128+
# GH#21314 PeriodIndex - Period returns an object-index
1129+
# of DateOffset objects, for which we cannot use __neg__
1130+
# directly, so we have to apply it pointwise
1131+
return other.__sub__(self).map(lambda x: -x)
11291132
else: # pragma: no cover
11301133
return NotImplemented
11311134
elif is_period_object(other):

pandas/core/indexes/datetimelike.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,9 @@ def __add__(self, other):
899899
raise TypeError("cannot add {dtype}-dtype to {cls}"
900900
.format(dtype=other.dtype,
901901
cls=type(self).__name__))
902-
902+
elif is_categorical_dtype(other):
903+
# Categorical op will raise; defer explicitly
904+
return NotImplemented
903905
else: # pragma: no cover
904906
return NotImplemented
905907

@@ -964,6 +966,9 @@ def __sub__(self, other):
964966
raise TypeError("cannot subtract {dtype}-dtype from {cls}"
965967
.format(dtype=other.dtype,
966968
cls=type(self).__name__))
969+
elif is_categorical_dtype(other):
970+
# Categorical op will raise; defer explicitly
971+
return NotImplemented
967972
else: # pragma: no cover
968973
return NotImplemented
969974

pandas/core/indexes/period.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -551,13 +551,14 @@ def is_all_dates(self):
551551
@property
552552
def is_full(self):
553553
"""
554-
Returns True if there are any missing periods from start to end
554+
Returns True if this PeriodIndex is range-like in that all Periods
555+
between start and end are present, in order.
555556
"""
556557
if len(self) == 0:
557558
return True
558559
if not self.is_monotonic:
559560
raise ValueError('Index is not monotonic')
560-
values = self.values
561+
values = self.asi8
561562
return ((values[1:] - values[:-1]) < 2).all()
562563

563564
@property
@@ -761,17 +762,19 @@ def _sub_datelike(self, other):
761762
return NotImplemented
762763

763764
def _sub_period(self, other):
765+
# If the operation is well-defined, we return an object-Index
766+
# of DateOffsets. Null entries are filled with pd.NaT
764767
if self.freq != other.freq:
765768
msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr)
766769
raise IncompatibleFrequency(msg)
767770

768771
asi8 = self.asi8
769772
new_data = asi8 - other.ordinal
773+
new_data = np.array([self.freq * x for x in new_data])
770774

771775
if self.hasnans:
772-
new_data = new_data.astype(np.float64)
773-
new_data[self._isnan] = np.nan
774-
# result must be Int64Index or Float64Index
776+
new_data[self._isnan] = tslib.NaT
777+
775778
return Index(new_data)
776779

777780
def shift(self, n):

pandas/tests/frame/test_arithmetic.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,10 @@ def test_ops_frame_period(self):
258258
assert df['B'].dtype == object
259259

260260
p = pd.Period('2015-03', freq='M')
261+
off = p.freq
261262
# dtype will be object because of original dtype
262-
exp = pd.DataFrame({'A': np.array([2, 1], dtype=object),
263-
'B': np.array([14, 13], dtype=object)})
263+
exp = pd.DataFrame({'A': np.array([2 * off, 1 * off], dtype=object),
264+
'B': np.array([14 * off, 13 * off], dtype=object)})
264265
tm.assert_frame_equal(p - df, exp)
265266
tm.assert_frame_equal(df - p, -1 * exp)
266267

@@ -271,7 +272,7 @@ def test_ops_frame_period(self):
271272
assert df2['A'].dtype == object
272273
assert df2['B'].dtype == object
273274

274-
exp = pd.DataFrame({'A': np.array([4, 4], dtype=object),
275-
'B': np.array([16, 16], dtype=object)})
275+
exp = pd.DataFrame({'A': np.array([4 * off, 4 * off], dtype=object),
276+
'B': np.array([16 * off, 16 * off], dtype=object)})
276277
tm.assert_frame_equal(df2 - df, exp)
277278
tm.assert_frame_equal(df - df2, -1 * exp)

pandas/tests/indexes/period/test_arithmetic.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -730,11 +730,12 @@ def test_pi_ops(self):
730730

731731
self._check(idx + 2, lambda x: x - 2, idx)
732732
result = idx - Period('2011-01', freq='M')
733-
exp = pd.Index([0, 1, 2, 3], name='idx')
733+
off = idx.freq
734+
exp = pd.Index([0 * off, 1 * off, 2 * off, 3 * off], name='idx')
734735
tm.assert_index_equal(result, exp)
735736

736737
result = Period('2011-01', freq='M') - idx
737-
exp = pd.Index([0, -1, -2, -3], name='idx')
738+
exp = pd.Index([0 * off, -1 * off, -2 * off, -3 * off], name='idx')
738739
tm.assert_index_equal(result, exp)
739740

740741
@pytest.mark.parametrize('ng', ["str", 1.5])
@@ -864,14 +865,15 @@ def test_pi_sub_period(self):
864865
freq='M', name='idx')
865866

866867
result = idx - pd.Period('2012-01', freq='M')
867-
exp = pd.Index([-12, -11, -10, -9], name='idx')
868+
off = idx.freq
869+
exp = pd.Index([-12 * off, -11 * off, -10 * off, -9 * off], name='idx')
868870
tm.assert_index_equal(result, exp)
869871

870872
result = np.subtract(idx, pd.Period('2012-01', freq='M'))
871873
tm.assert_index_equal(result, exp)
872874

873875
result = pd.Period('2012-01', freq='M') - idx
874-
exp = pd.Index([12, 11, 10, 9], name='idx')
876+
exp = pd.Index([12 * off, 11 * off, 10 * off, 9 * off], name='idx')
875877
tm.assert_index_equal(result, exp)
876878

877879
result = np.subtract(pd.Period('2012-01', freq='M'), idx)
@@ -898,11 +900,12 @@ def test_pi_sub_period_nat(self):
898900
freq='M', name='idx')
899901

900902
result = idx - pd.Period('2012-01', freq='M')
901-
exp = pd.Index([-12, np.nan, -10, -9], name='idx')
903+
off = idx.freq
904+
exp = pd.Index([-12 * off, pd.NaT, -10 * off, -9 * off], name='idx')
902905
tm.assert_index_equal(result, exp)
903906

904907
result = pd.Period('2012-01', freq='M') - idx
905-
exp = pd.Index([12, np.nan, 10, 9], name='idx')
908+
exp = pd.Index([12 * off, pd.NaT, 10 * off, 9 * off], name='idx')
906909
tm.assert_index_equal(result, exp)
907910

908911
exp = pd.TimedeltaIndex([np.nan, np.nan, np.nan, np.nan], name='idx')

pandas/tests/scalar/period/test_period.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ def test_strftime(self):
572572
def test_sub_delta(self):
573573
left, right = Period('2011', freq='A'), Period('2007', freq='A')
574574
result = left - right
575-
assert result == 4
575+
assert result == 4 * right.freq
576576

577577
with pytest.raises(period.IncompatibleFrequency):
578578
left - Period('2007-01', freq='M')
@@ -1064,8 +1064,9 @@ def test_sub(self):
10641064
dt1 = Period('2011-01-01', freq='D')
10651065
dt2 = Period('2011-01-15', freq='D')
10661066

1067-
assert dt1 - dt2 == -14
1068-
assert dt2 - dt1 == 14
1067+
off = dt1.freq
1068+
assert dt1 - dt2 == -14 * off
1069+
assert dt2 - dt1 == 14 * off
10691070

10701071
msg = r"Input has different freq=M from Period\(freq=D\)"
10711072
with tm.assert_raises_regex(period.IncompatibleFrequency, msg):

pandas/tests/series/test_arithmetic.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -517,16 +517,17 @@ def test_ops_series_period(self):
517517
assert ser.dtype == object
518518

519519
per = pd.Period('2015-01-10', freq='D')
520+
off = per.freq
520521
# dtype will be object because of original dtype
521-
expected = pd.Series([9, 8], name='xxx', dtype=object)
522+
expected = pd.Series([9 * off, 8 * off], name='xxx', dtype=object)
522523
tm.assert_series_equal(per - ser, expected)
523524
tm.assert_series_equal(ser - per, -1 * expected)
524525

525526
s2 = pd.Series([pd.Period('2015-01-05', freq='D'),
526527
pd.Period('2015-01-04', freq='D')], name='xxx')
527528
assert s2.dtype == object
528529

529-
expected = pd.Series([4, 2], name='xxx', dtype=object)
530+
expected = pd.Series([4 * off, 2 * off], name='xxx', dtype=object)
530531
tm.assert_series_equal(s2 - ser, expected)
531532
tm.assert_series_equal(ser - s2, -1 * expected)
532533

0 commit comments

Comments
 (0)