From f94d0271dea2d4808743493421da989f91231a68 Mon Sep 17 00:00:00 2001 From: sinhrks Date: Sat, 13 Aug 2016 09:54:21 +0900 Subject: [PATCH] BUG: ufunc with PeriodIndex may raise IncompatibleFrequency --- doc/source/whatsnew/v0.19.0.txt | 1 + pandas/tseries/period.py | 17 ++++++++++++++--- pandas/tseries/tests/test_base.py | 9 ++++----- pandas/tseries/tests/test_period.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/doc/source/whatsnew/v0.19.0.txt b/doc/source/whatsnew/v0.19.0.txt index 9ac265a20073a..c7f0beb439596 100644 --- a/doc/source/whatsnew/v0.19.0.txt +++ b/doc/source/whatsnew/v0.19.0.txt @@ -1065,6 +1065,7 @@ Bug Fixes - Bug in ``concat`` and ``groupby`` for hierarchical frames with ``RangeIndex`` levels (:issue:`13542`). - Bug in ``agg()`` function on groupby dataframe changes dtype of ``datetime64[ns]`` column to ``float64`` (:issue:`12821`) +- 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`) - Bug in operations on ``NaT`` returning ``float`` instead of ``datetime64[ns]`` (:issue:`12941`) diff --git a/pandas/tseries/period.py b/pandas/tseries/period.py index 486cf52f188a9..3db3c8198a5f7 100644 --- a/pandas/tseries/period.py +++ b/pandas/tseries/period.py @@ -359,9 +359,15 @@ def __array_wrap__(self, result, context=None): if isinstance(context, tuple) and len(context) > 0: func = context[0] if (func is np.add): - return self._add_delta(context[1][1]) + try: + return self._add_delta(context[1][1]) + except IncompatibleFrequency: + raise TypeError elif (func is np.subtract): - return self._add_delta(-context[1][1]) + try: + return self._add_delta(-context[1][1]) + except IncompatibleFrequency: + raise TypeError elif isinstance(func, np.ufunc): if 'M->M' not in func.types: msg = "ufunc '{0}' not supported for the PeriodIndex" @@ -371,7 +377,7 @@ def __array_wrap__(self, result, context=None): if is_bool_dtype(result): return result - return PeriodIndex(result, freq=self.freq, name=self.name) + return self._shallow_copy(result) @property def _box_func(self): @@ -628,6 +634,11 @@ def _maybe_convert_timedelta(self, other): offset_nanos = tslib._delta_to_nanoseconds(offset) if (nanos % offset_nanos).all() == 0: return nanos // offset_nanos + elif is_integer(other): + # integer is passed to .shift via + # _add_datetimelike_methods basically + # but ufunc may pass integer to _add_delta + return other # raise when input doesn't have freq msg = "Input has different freq from PeriodIndex(freq={0})" raise IncompatibleFrequency(msg.format(self.freqstr)) diff --git a/pandas/tseries/tests/test_base.py b/pandas/tseries/tests/test_base.py index 45a5feec7c949..0d6c991f00c8b 100644 --- a/pandas/tseries/tests/test_base.py +++ b/pandas/tseries/tests/test_base.py @@ -1758,12 +1758,11 @@ def test_representation(self): idx1 = PeriodIndex([], freq='D') idx2 = PeriodIndex(['2011-01-01'], freq='D') idx3 = PeriodIndex(['2011-01-01', '2011-01-02'], freq='D') - idx4 = PeriodIndex( - ['2011-01-01', '2011-01-02', '2011-01-03'], freq='D') + idx4 = PeriodIndex(['2011-01-01', '2011-01-02', '2011-01-03'], + freq='D') idx5 = PeriodIndex(['2011', '2012', '2013'], freq='A') - idx6 = PeriodIndex( - ['2011-01-01 09:00', '2012-02-01 10:00', 'NaT'], freq='H') - + idx6 = PeriodIndex(['2011-01-01 09:00', '2012-02-01 10:00', + 'NaT'], freq='H') idx7 = pd.period_range('2013Q1', periods=1, freq="Q") idx8 = pd.period_range('2013Q1', periods=2, freq="Q") idx9 = pd.period_range('2013Q1', periods=3, freq="Q") diff --git a/pandas/tseries/tests/test_period.py b/pandas/tseries/tests/test_period.py index 2044d44b35d0b..7ba8720345c8b 100644 --- a/pandas/tseries/tests/test_period.py +++ b/pandas/tseries/tests/test_period.py @@ -4125,6 +4125,7 @@ def test_pi_ops_errors(self): s = pd.Series(idx) msg = "unsupported operand type\(s\)" + for obj in [idx, s]: for ng in ["str", 1.5]: with tm.assertRaisesRegexp(TypeError, msg): @@ -4137,6 +4138,20 @@ def test_pi_ops_errors(self): with tm.assertRaisesRegexp(TypeError, msg): obj - ng + # ToDo: currently, it accepts float because PeriodIndex.values + # is internally int. Should be fixed after GH13988 + # msg is different depending on NumPy version + if not _np_version_under1p9: + for ng in ["str"]: + with tm.assertRaises(TypeError): + np.add(obj, ng) + + with tm.assertRaises(TypeError): + np.add(ng, obj) + + with tm.assertRaises(TypeError): + np.subtract(ng, obj) + def test_pi_ops_nat(self): idx = PeriodIndex(['2011-01', '2011-02', 'NaT', '2011-04'], freq='M', name='idx') @@ -4144,8 +4159,22 @@ def test_pi_ops_nat(self): 'NaT', '2011-06'], freq='M', name='idx') self._check(idx, lambda x: x + 2, expected) self._check(idx, lambda x: 2 + x, expected) + self._check(idx, lambda x: np.add(x, 2), expected) self._check(idx + 2, lambda x: x - 2, idx) + self._check(idx + 2, lambda x: np.subtract(x, 2), idx) + + # freq with mult + idx = PeriodIndex(['2011-01', '2011-02', 'NaT', + '2011-04'], freq='2M', name='idx') + expected = PeriodIndex(['2011-07', '2011-08', + 'NaT', '2011-10'], freq='2M', name='idx') + self._check(idx, lambda x: x + 3, expected) + self._check(idx, lambda x: 3 + x, expected) + self._check(idx, lambda x: np.add(x, 3), expected) + + self._check(idx + 3, lambda x: x - 3, idx) + self._check(idx + 3, lambda x: np.subtract(x, 3), idx) def test_pi_ops_array_int(self): idx = PeriodIndex(['2011-01', '2011-02', 'NaT',