Skip to content

Commit 8dc22d8

Browse files
ms7463jreback
authored andcommitted
BUG - remove scaling multiplier from Period diff result (#23915)
1 parent 6983905 commit 8dc22d8

File tree

8 files changed

+89
-12
lines changed

8 files changed

+89
-12
lines changed

doc/source/whatsnew/v0.24.0.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -1116,7 +1116,9 @@ from :class:`Period`, :class:`PeriodIndex`, and in some cases
11161116
:class:`Timestamp`, :class:`DatetimeIndex` and :class:`TimedeltaIndex`.
11171117

11181118
This usage is now deprecated. Instead add or subtract integer multiples of
1119-
the object's ``freq`` attribute (:issue:`21939`)
1119+
the object's ``freq`` attribute. The result of subtraction of :class:`Period`
1120+
objects will be agnostic of the multiplier of the objects' ``freq`` attribute
1121+
(:issue:`21939`, :issue:`23878`).
11201122

11211123
Previous Behavior:
11221124

pandas/_libs/tslibs/offsets.pyx

+8
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,14 @@ class _BaseOffset(object):
352352
if name not in ['n', 'normalize']}
353353
return {name: kwds[name] for name in kwds if kwds[name] is not None}
354354

355+
@property
356+
def base(self):
357+
"""
358+
Returns a copy of the calling offset object with n=1 and all other
359+
attributes equal.
360+
"""
361+
return type(self)(n=1, normalize=self.normalize, **self.kwds)
362+
355363
def __add__(self, other):
356364
if getattr(other, "_typ", None) in ["datetimeindex", "periodindex",
357365
"datetimearray", "periodarray",

pandas/_libs/tslibs/period.pyx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1686,7 +1686,8 @@ cdef class _Period(object):
16861686
if other.freq != self.freq:
16871687
msg = _DIFFERENT_FREQ.format(self.freqstr, other.freqstr)
16881688
raise IncompatibleFrequency(msg)
1689-
return (self.ordinal - other.ordinal) * self.freq
1689+
# GH 23915 - mul by base freq since __add__ is agnostic of n
1690+
return (self.ordinal - other.ordinal) * self.freq.base
16901691
elif getattr(other, '_typ', None) == 'periodindex':
16911692
# GH#21314 PeriodIndex - Period returns an object-index
16921693
# of DateOffset objects, for which we cannot use __neg__

pandas/conftest.py

+8
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,14 @@ def mock():
636636
return pytest.importorskip("mock")
637637

638638

639+
@pytest.fixture(params=[getattr(pd.offsets, o) for o in pd.offsets.__all__ if
640+
issubclass(getattr(pd.offsets, o), pd.offsets.Tick)])
641+
def tick_classes(request):
642+
"""
643+
Fixture for Tick based datetime offsets available for a time series.
644+
"""
645+
return request.param
646+
639647
# ----------------------------------------------------------------
640648
# Global setup for tests using Hypothesis
641649

pandas/core/arrays/datetimelike.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ def _sub_period_array(self, other):
710710
arr_mask=self._isnan,
711711
b_mask=other._isnan)
712712

713-
new_values = np.array([self.freq * x for x in new_values])
713+
new_values = np.array([self.freq.base * x for x in new_values])
714714
if self.hasnans or other.hasnans:
715715
mask = (self._isnan) | (other._isnan)
716716
new_values[mask] = NaT

pandas/tests/arithmetic/test_period.py

+35
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,41 @@ def test_parr_sub_pi_mismatched_freq(self, box_with_array):
370370
with pytest.raises(IncompatibleFrequency):
371371
rng - other
372372

373+
@pytest.mark.parametrize('n', [1, 2, 3, 4])
374+
def test_sub_n_gt_1_ticks(self, tick_classes, n):
375+
# GH 23878
376+
p1_d = '19910905'
377+
p2_d = '19920406'
378+
p1 = pd.PeriodIndex([p1_d], freq=tick_classes(n))
379+
p2 = pd.PeriodIndex([p2_d], freq=tick_classes(n))
380+
381+
expected = (pd.PeriodIndex([p2_d], freq=p2.freq.base)
382+
- pd.PeriodIndex([p1_d], freq=p1.freq.base))
383+
384+
tm.assert_index_equal((p2 - p1), expected)
385+
386+
@pytest.mark.parametrize('n', [1, 2, 3, 4])
387+
@pytest.mark.parametrize('offset, kwd_name', [
388+
(pd.offsets.YearEnd, 'month'),
389+
(pd.offsets.QuarterEnd, 'startingMonth'),
390+
(pd.offsets.MonthEnd, None),
391+
(pd.offsets.Week, 'weekday')
392+
])
393+
def test_sub_n_gt_1_offsets(self, offset, kwd_name, n):
394+
# GH 23878
395+
kwds = {kwd_name: 3} if kwd_name is not None else {}
396+
p1_d = '19910905'
397+
p2_d = '19920406'
398+
freq = offset(n, normalize=False, **kwds)
399+
p1 = pd.PeriodIndex([p1_d], freq=freq)
400+
p2 = pd.PeriodIndex([p2_d], freq=freq)
401+
402+
result = p2 - p1
403+
expected = (pd.PeriodIndex([p2_d], freq=freq.base)
404+
- pd.PeriodIndex([p1_d], freq=freq.base))
405+
406+
tm.assert_index_equal(result, expected)
407+
373408
# -------------------------------------------------------------
374409
# Invalid Operations
375410

pandas/tests/scalar/period/test_period.py

+32
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,38 @@ def test_sub(self):
11061106
with pytest.raises(period.IncompatibleFrequency, match=msg):
11071107
per1 - Period('2011-02', freq='M')
11081108

1109+
@pytest.mark.parametrize('n', [1, 2, 3, 4])
1110+
def test_sub_n_gt_1_ticks(self, tick_classes, n):
1111+
# GH 23878
1112+
p1 = pd.Period('19910905', freq=tick_classes(n))
1113+
p2 = pd.Period('19920406', freq=tick_classes(n))
1114+
1115+
expected = (pd.Period(str(p2), freq=p2.freq.base)
1116+
- pd.Period(str(p1), freq=p1.freq.base))
1117+
1118+
assert (p2 - p1) == expected
1119+
1120+
@pytest.mark.parametrize('normalize', [True, False])
1121+
@pytest.mark.parametrize('n', [1, 2, 3, 4])
1122+
@pytest.mark.parametrize('offset, kwd_name', [
1123+
(pd.offsets.YearEnd, 'month'),
1124+
(pd.offsets.QuarterEnd, 'startingMonth'),
1125+
(pd.offsets.MonthEnd, None),
1126+
(pd.offsets.Week, 'weekday')
1127+
])
1128+
def test_sub_n_gt_1_offsets(self, offset, kwd_name, n, normalize):
1129+
# GH 23878
1130+
kwds = {kwd_name: 3} if kwd_name is not None else {}
1131+
p1_d = '19910905'
1132+
p2_d = '19920406'
1133+
p1 = pd.Period(p1_d, freq=offset(n, normalize, **kwds))
1134+
p2 = pd.Period(p2_d, freq=offset(n, normalize, **kwds))
1135+
1136+
expected = (pd.Period(p2_d, freq=p2.freq.base)
1137+
- pd.Period(p1_d, freq=p1.freq.base))
1138+
1139+
assert (p2 - p1) == expected
1140+
11091141
def test_add_offset(self):
11101142
# freq is DateOffset
11111143
for freq in ['A', '2A', '3A']:

pandas/tests/tseries/offsets/conftest.py

-9
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,3 @@ def month_classes(request):
1919
Fixture for month based datetime offsets available for a time series.
2020
"""
2121
return request.param
22-
23-
24-
@pytest.fixture(params=[getattr(offsets, o) for o in offsets.__all__ if
25-
issubclass(getattr(offsets, o), offsets.Tick)])
26-
def tick_classes(request):
27-
"""
28-
Fixture for Tick based datetime offsets available for a time series.
29-
"""
30-
return request.param

0 commit comments

Comments
 (0)