Skip to content

Commit a01e58f

Browse files
sinhrksjreback
authored andcommitted
BUG: ufunc with PeriodIndex may raise IncompatibleFrequency
Author: sinhrks <[email protected]> Closes #13980 from sinhrks/period_numpy_ufunc and squashes the following commits: f94d027 [sinhrks] BUG: ufunc with PeriodIndex may raise IncompatibleFrequency
1 parent 6cc7135 commit a01e58f

File tree

4 files changed

+48
-8
lines changed

4 files changed

+48
-8
lines changed

doc/source/whatsnew/v0.19.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,7 @@ Bug Fixes
10651065
- Bug in ``concat`` and ``groupby`` for hierarchical frames with ``RangeIndex`` levels (:issue:`13542`).
10661066

10671067
- Bug in ``agg()`` function on groupby dataframe changes dtype of ``datetime64[ns]`` column to ``float64`` (:issue:`12821`)
1068+
- Bug in using NumPy ufunc with ``PeriodIndex`` to add or subtract integer raise ``IncompatibleFrequency``. Note that using standard operator like ``+`` or ``-`` is recommended, because standard operators use more efficient path (:issue:`13980`)
10681069

10691070
- Bug in operations on ``NaT`` returning ``float`` instead of ``datetime64[ns]`` (:issue:`12941`)
10701071

pandas/tseries/period.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -359,9 +359,15 @@ def __array_wrap__(self, result, context=None):
359359
if isinstance(context, tuple) and len(context) > 0:
360360
func = context[0]
361361
if (func is np.add):
362-
return self._add_delta(context[1][1])
362+
try:
363+
return self._add_delta(context[1][1])
364+
except IncompatibleFrequency:
365+
raise TypeError
363366
elif (func is np.subtract):
364-
return self._add_delta(-context[1][1])
367+
try:
368+
return self._add_delta(-context[1][1])
369+
except IncompatibleFrequency:
370+
raise TypeError
365371
elif isinstance(func, np.ufunc):
366372
if 'M->M' not in func.types:
367373
msg = "ufunc '{0}' not supported for the PeriodIndex"
@@ -371,7 +377,7 @@ def __array_wrap__(self, result, context=None):
371377

372378
if is_bool_dtype(result):
373379
return result
374-
return PeriodIndex(result, freq=self.freq, name=self.name)
380+
return self._shallow_copy(result)
375381

376382
@property
377383
def _box_func(self):
@@ -628,6 +634,11 @@ def _maybe_convert_timedelta(self, other):
628634
offset_nanos = tslib._delta_to_nanoseconds(offset)
629635
if (nanos % offset_nanos).all() == 0:
630636
return nanos // offset_nanos
637+
elif is_integer(other):
638+
# integer is passed to .shift via
639+
# _add_datetimelike_methods basically
640+
# but ufunc may pass integer to _add_delta
641+
return other
631642
# raise when input doesn't have freq
632643
msg = "Input has different freq from PeriodIndex(freq={0})"
633644
raise IncompatibleFrequency(msg.format(self.freqstr))

pandas/tseries/tests/test_base.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -1758,12 +1758,11 @@ def test_representation(self):
17581758
idx1 = PeriodIndex([], freq='D')
17591759
idx2 = PeriodIndex(['2011-01-01'], freq='D')
17601760
idx3 = PeriodIndex(['2011-01-01', '2011-01-02'], freq='D')
1761-
idx4 = PeriodIndex(
1762-
['2011-01-01', '2011-01-02', '2011-01-03'], freq='D')
1761+
idx4 = PeriodIndex(['2011-01-01', '2011-01-02', '2011-01-03'],
1762+
freq='D')
17631763
idx5 = PeriodIndex(['2011', '2012', '2013'], freq='A')
1764-
idx6 = PeriodIndex(
1765-
['2011-01-01 09:00', '2012-02-01 10:00', 'NaT'], freq='H')
1766-
1764+
idx6 = PeriodIndex(['2011-01-01 09:00', '2012-02-01 10:00',
1765+
'NaT'], freq='H')
17671766
idx7 = pd.period_range('2013Q1', periods=1, freq="Q")
17681767
idx8 = pd.period_range('2013Q1', periods=2, freq="Q")
17691768
idx9 = pd.period_range('2013Q1', periods=3, freq="Q")

pandas/tseries/tests/test_period.py

+29
Original file line numberDiff line numberDiff line change
@@ -4140,6 +4140,7 @@ def test_pi_ops_errors(self):
41404140
s = pd.Series(idx)
41414141

41424142
msg = "unsupported operand type\(s\)"
4143+
41434144
for obj in [idx, s]:
41444145
for ng in ["str", 1.5]:
41454146
with tm.assertRaisesRegexp(TypeError, msg):
@@ -4152,15 +4153,43 @@ def test_pi_ops_errors(self):
41524153
with tm.assertRaisesRegexp(TypeError, msg):
41534154
obj - ng
41544155

4156+
# ToDo: currently, it accepts float because PeriodIndex.values
4157+
# is internally int. Should be fixed after GH13988
4158+
# msg is different depending on NumPy version
4159+
if not _np_version_under1p9:
4160+
for ng in ["str"]:
4161+
with tm.assertRaises(TypeError):
4162+
np.add(obj, ng)
4163+
4164+
with tm.assertRaises(TypeError):
4165+
np.add(ng, obj)
4166+
4167+
with tm.assertRaises(TypeError):
4168+
np.subtract(ng, obj)
4169+
41554170
def test_pi_ops_nat(self):
41564171
idx = PeriodIndex(['2011-01', '2011-02', 'NaT',
41574172
'2011-04'], freq='M', name='idx')
41584173
expected = PeriodIndex(['2011-03', '2011-04',
41594174
'NaT', '2011-06'], freq='M', name='idx')
41604175
self._check(idx, lambda x: x + 2, expected)
41614176
self._check(idx, lambda x: 2 + x, expected)
4177+
self._check(idx, lambda x: np.add(x, 2), expected)
41624178

41634179
self._check(idx + 2, lambda x: x - 2, idx)
4180+
self._check(idx + 2, lambda x: np.subtract(x, 2), idx)
4181+
4182+
# freq with mult
4183+
idx = PeriodIndex(['2011-01', '2011-02', 'NaT',
4184+
'2011-04'], freq='2M', name='idx')
4185+
expected = PeriodIndex(['2011-07', '2011-08',
4186+
'NaT', '2011-10'], freq='2M', name='idx')
4187+
self._check(idx, lambda x: x + 3, expected)
4188+
self._check(idx, lambda x: 3 + x, expected)
4189+
self._check(idx, lambda x: np.add(x, 3), expected)
4190+
4191+
self._check(idx + 3, lambda x: x - 3, idx)
4192+
self._check(idx + 3, lambda x: np.subtract(x, 3), idx)
41644193

41654194
def test_pi_ops_array_int(self):
41664195
idx = PeriodIndex(['2011-01', '2011-02', 'NaT',

0 commit comments

Comments
 (0)