From 341212f5be3993cea18fdfff55d0aea1bf05600a Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 7 Oct 2018 15:27:07 -0700 Subject: [PATCH 01/13] Fix PeriodIndex +- TimedeltaIndex --- pandas/core/arrays/datetimelike.py | 5 ++- pandas/core/arrays/period.py | 45 +++++++++++++++++--------- pandas/tests/arithmetic/test_period.py | 20 +++++++++--- pandas/util/testing.py | 3 ++ 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 1ce60510c6a69..f7bd96d0df773 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -631,11 +631,14 @@ def __add__(self, other): return self._add_datelike(other) elif is_integer_dtype(other): result = self._addsub_int_array(other, operator.add) - elif is_float_dtype(other) or is_period_dtype(other): + elif is_float_dtype(other): # Explicitly catch invalid dtypes raise TypeError("cannot add {dtype}-dtype to {cls}" .format(dtype=other.dtype, cls=type(self).__name__)) + elif is_period_dtype(other): + # PeriodIndex + TimedeltaIndex _sometimes_ is valid + return NotImplemented elif is_extension_array_dtype(other): # Categorical op will raise; defer explicitly return NotImplemented diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 92803ab5f52e0..9239e71eced4c 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -17,7 +17,7 @@ from pandas.util._decorators import cache_readonly from pandas.core.dtypes.common import ( - is_integer_dtype, is_float_dtype, is_period_dtype) + is_integer_dtype, is_float_dtype, is_period_dtype, is_timedelta64_dtype) from pandas.core.dtypes.dtypes import PeriodDtype from pandas.core.dtypes.generic import ABCSeries @@ -316,8 +316,7 @@ def _add_delta_td(self, other): freqstr=self.freqstr)) def _add_delta(self, other): - ordinal_delta = self._maybe_convert_timedelta(other) - return self._time_shift(ordinal_delta) + return self._time_shift(other) def shift(self, n): """ @@ -335,7 +334,8 @@ def shift(self, n): return self._time_shift(n) def _time_shift(self, n): - values = self._ndarray_values + n * self.freq.n + n = self._maybe_convert_timedelta(n) + values = self._ndarray_values + n if self.hasnans: values[self._isnan] = iNaT return self._shallow_copy(values=values) @@ -346,7 +346,8 @@ def _maybe_convert_timedelta(self, other): Parameters ---------- - other : timedelta, np.timedelta64, DateOffset, int, np.ndarray + other : timedelta, np.timedelta64, DateOffset, int, + np.ndarray[timedelta64], TimedeltaIndex Returns ------- @@ -357,30 +358,42 @@ def _maybe_convert_timedelta(self, other): IncompatibleFrequency : if the input cannot be written as a multiple of self.freq. Note IncompatibleFrequency subclasses ValueError. """ - if isinstance( - other, (timedelta, np.timedelta64, Tick, np.ndarray)): - offset = frequencies.to_offset(self.freq.rule_code) - if isinstance(offset, Tick): + if isinstance(other, (timedelta, np.timedelta64, Tick)): + base_offset = frequencies.to_offset(self.freq.rule_code) + if isinstance(base_offset, Tick): + other_nanos = delta_to_nanoseconds(other) + base_nanos = delta_to_nanoseconds(base_offset) + check = np.all(other_nanos % base_nanos == 0) + if check: + return other_nanos // base_nanos + elif (isinstance(other, (np.ndarray, DatetimeLikeArrayMixin)) and + is_timedelta64_dtype(other)): + # i.e. TimedeltaArray/TimedeltaIndex + if isinstance(self.freq, Tick): + base_offset = frequencies.to_offset(self.freq.rule_code) + base_nanos = base_offset.nanos if isinstance(other, np.ndarray): - nanos = np.vectorize(delta_to_nanoseconds)(other) + other_nanos = other.astype('m8[ns]').view('i8') else: - nanos = delta_to_nanoseconds(other) - offset_nanos = delta_to_nanoseconds(offset) - check = np.all(nanos % offset_nanos == 0) + other_nanos = other.asi8 + check = np.all(other_nanos % base_nanos == 0) if check: - return nanos // offset_nanos + return other_nanos // base_nanos elif isinstance(other, DateOffset): freqstr = other.rule_code base = frequencies.get_base_alias(freqstr) if base == self.freq.rule_code: - return other.n + return other.n * self.freq.n msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) raise IncompatibleFrequency(msg) elif lib.is_integer(other): # integer is passed to .shift via # _add_datetimelike_methods basically # but ufunc may pass integer to _add_delta - return other + return other * self.freq.n + elif is_integer_dtype(other) and isinstance(other, np.ndarray): + # TODO: Also need to get Index + return other * self.freq.n # raise when input doesn't have freq msg = "Input has different freq from {cls}(freq={freqstr})" diff --git a/pandas/tests/arithmetic/test_period.py b/pandas/tests/arithmetic/test_period.py index 3210290b9c5c8..184c0b2ccaa1f 100644 --- a/pandas/tests/arithmetic/test_period.py +++ b/pandas/tests/arithmetic/test_period.py @@ -446,26 +446,36 @@ def test_pi_add_sub_td64_array_non_tick_raises(self): with pytest.raises(period.IncompatibleFrequency): tdarr - rng - @pytest.mark.xfail(reason='op with TimedeltaIndex raises, with ndarray OK', - strict=True) def test_pi_add_sub_td64_array_tick(self): - rng = pd.period_range('1/1/2000', freq='Q', periods=3) + # PeriodIndex + Timedelta-like is allowed only with + # tick-like frequencies + rng = pd.period_range('1/1/2000', freq='90D', periods=3) tdi = pd.TimedeltaIndex(['-1 Day', '-1 Day', '-1 Day']) tdarr = tdi.values - expected = rng + tdi + expected = pd.period_range('12/31/1999', freq='90D', periods=3) + result = rng + tdi + tm.assert_index_equal(result, expected) result = rng + tdarr tm.assert_index_equal(result, expected) + result = tdi + rng + tm.assert_index_equal(result, expected) result = tdarr + rng tm.assert_index_equal(result, expected) - expected = rng - tdi + expected = pd.period_range('1/2/2000', freq='90D', periods=3) + + result = rng - tdi + tm.assert_index_equal(result, expected) result = rng - tdarr tm.assert_index_equal(result, expected) with pytest.raises(TypeError): tdarr - rng + with pytest.raises(TypeError): + tdi - rng + # ----------------------------------------------------------------- # operations with array/Index of DateOffset objects diff --git a/pandas/util/testing.py b/pandas/util/testing.py index 4e01e0feb004c..61a3c4bb6934e 100644 --- a/pandas/util/testing.py +++ b/pandas/util/testing.py @@ -805,6 +805,7 @@ def assert_index_equal(left, right, exact='equiv', check_names=True, Specify object name being compared, internally used to show appropriate assertion message """ + __tracebackhide__ = True def _check_types(l, r, obj='Index'): if exact: @@ -1048,6 +1049,8 @@ def assert_interval_array_equal(left, right, exact='equiv', def raise_assert_detail(obj, message, left, right, diff=None): + __tracebackhide__ = True + if isinstance(left, np.ndarray): left = pprint_thing(left) elif is_categorical_dtype(left): From 0704e53395d7962312f129e95826c0570406733e Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 9 Oct 2018 08:59:12 -0700 Subject: [PATCH 02/13] better comment --- pandas/core/arrays/datetimelike.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index f7bd96d0df773..bde259ecece5e 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -637,7 +637,10 @@ def __add__(self, other): .format(dtype=other.dtype, cls=type(self).__name__)) elif is_period_dtype(other): - # PeriodIndex + TimedeltaIndex _sometimes_ is valid + # if self is a TimedeltaArray and other is a PeriodArray with + # a timedelta-like (i.e. Tick) freq, this operation is valid. + # Defer to the PeriodArray implementation. + # In remaining cases, this will end up raising TypeError. return NotImplemented elif is_extension_array_dtype(other): # Categorical op will raise; defer explicitly From 97f6798af292849e4a500654bf35909beca7db4d Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 9 Oct 2018 16:44:55 -0700 Subject: [PATCH 03/13] typo fixup --- pandas/core/arrays/datetimes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index a0a9b57712249..dffbc15af4c4f 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -502,7 +502,7 @@ def _add_delta(self, delta): Parameters ---------- delta : {timedelta, np.timedelta64, DateOffset, - TimedelaIndex, ndarray[timedelta64]} + TimedeltaIndex, ndarray[timedelta64]} Returns ------- From a4d2da20019750e8a54b48a41aa7ab0fc3d064ad Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 9 Oct 2018 16:45:37 -0700 Subject: [PATCH 04/13] Allow _add_delta_tdi to handle ndarray[timedelta64] gracefully --- pandas/core/arrays/datetimelike.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index bde259ecece5e..4af1600eb77cf 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -382,6 +382,11 @@ def _add_delta_tdi(self, other): if not len(self) == len(other): raise ValueError("cannot add indices of unequal length") + if isinstance(other, np.ndarray): + # ndarray[timedelta64]; wrap in TimedeltaIndex for op + from pandas import TimedeltaIndex + other = TimedeltaIndex(other) + self_i8 = self.asi8 other_i8 = other.asi8 new_values = checked_add_with_arr(self_i8, other_i8, From 01d3af6f38bf8379b63446f0b30583364f30a34e Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 9 Oct 2018 16:46:40 -0700 Subject: [PATCH 05/13] Implement specialized versions of _add_delta, _add_delta_tdi --- pandas/core/arrays/period.py | 127 ++++++++++++++++++++++++----------- 1 file changed, 87 insertions(+), 40 deletions(-) diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 9239e71eced4c..95a32832f47ad 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from datetime import timedelta +import operator import warnings import numpy as np @@ -17,6 +18,7 @@ from pandas.util._decorators import cache_readonly from pandas.core.dtypes.common import ( + _TD_DTYPE, is_integer_dtype, is_float_dtype, is_period_dtype, is_timedelta64_dtype) from pandas.core.dtypes.dtypes import PeriodDtype from pandas.core.dtypes.generic import ABCSeries @@ -300,23 +302,54 @@ def _add_offset(self, other): return self._time_shift(other.n) def _add_delta_td(self, other): + assert isinstance(self.freq, Tick) # checked by calling function assert isinstance(other, (timedelta, np.timedelta64, Tick)) - nanos = delta_to_nanoseconds(other) - own_offset = frequencies.to_offset(self.freq.rule_code) - if isinstance(own_offset, Tick): - offset_nanos = delta_to_nanoseconds(own_offset) - if np.all(nanos % offset_nanos == 0): - return self._time_shift(nanos // offset_nanos) + delta = self._check_timedeltalike_freq_compat(other) - # raise when input doesn't have freq - raise IncompatibleFrequency("Input has different freq from " - "{cls}(freq={freqstr})" - .format(cls=type(self).__name__, - freqstr=self.freqstr)) + # Note: when calling parent class's _add_delta_td, it will call + # delta_to_nanoseconds(delta). Because delta here is an integer, + # delta_to_nanoseconds will return it unchanged. + return DatetimeLikeArrayMixin._add_delta_td(self, delta) + + def _add_delta_tdi(self, other): + assert isinstance(self.freq, Tick) # checked by calling function + + delta = self._check_timedeltalike_freq_compat(other) + return self._addsub_int_array(delta, operator.add) def _add_delta(self, other): - return self._time_shift(other) + """ + Add a timedelta-like, Tick, or TimedeltaIndex-like object + to self. + + Parameters + ---------- + other : {timedelta, np.timedelta64, Tick, + TimedeltaIndex, ndarray[timedelta64]} + + Returns + ------- + result : same type as self + """ + if not isinstance(self.freq, Tick): + # We cannot add timedelta-like to non-tick PeriodArray + raise IncompatibleFrequency("Input has different freq from " + "{cls}(freq={freqstr})" + .format(cls=type(self).__name__, + freqstr=self.freqstr)) + + # TODO: standardize across datetimelike subclasses whether to return + # i8 view or _shallow_copy + if isinstance(other, (Tick, timedelta, np.timedelta64)): + new_values = self._add_delta_td(other) + return self._shallow_copy(new_values) + elif is_timedelta64_dtype(other): + # ndarray[timedelta64] or TimedeltaArray/index + new_values = self._add_delta_tdi(other) + return self._shallow_copy(new_values) + else: # pragma: no cover + raise TypeError(type(other).__name__) def shift(self, n): """ @@ -334,8 +367,7 @@ def shift(self, n): return self._time_shift(n) def _time_shift(self, n): - n = self._maybe_convert_timedelta(n) - values = self._ndarray_values + n + values = self._ndarray_values + n * self.freq.n if self.hasnans: values[self._isnan] = iNaT return self._shallow_copy(values=values) @@ -358,48 +390,63 @@ def _maybe_convert_timedelta(self, other): IncompatibleFrequency : if the input cannot be written as a multiple of self.freq. Note IncompatibleFrequency subclasses ValueError. """ - if isinstance(other, (timedelta, np.timedelta64, Tick)): - base_offset = frequencies.to_offset(self.freq.rule_code) - if isinstance(base_offset, Tick): - other_nanos = delta_to_nanoseconds(other) - base_nanos = delta_to_nanoseconds(base_offset) - check = np.all(other_nanos % base_nanos == 0) - if check: - return other_nanos // base_nanos - elif (isinstance(other, (np.ndarray, DatetimeLikeArrayMixin)) and - is_timedelta64_dtype(other)): - # i.e. TimedeltaArray/TimedeltaIndex - if isinstance(self.freq, Tick): - base_offset = frequencies.to_offset(self.freq.rule_code) - base_nanos = base_offset.nanos - if isinstance(other, np.ndarray): - other_nanos = other.astype('m8[ns]').view('i8') - else: - other_nanos = other.asi8 - check = np.all(other_nanos % base_nanos == 0) - if check: - return other_nanos // base_nanos + if isinstance( + other, (timedelta, np.timedelta64, Tick, np.ndarray)): + offset = frequencies.to_offset(self.freq.rule_code) + if isinstance(offset, Tick): + # _check_timedeltalike_freq_compat will raise if incompatible + delta = self._check_timedeltalike_freq_compat(other) + return delta elif isinstance(other, DateOffset): freqstr = other.rule_code base = frequencies.get_base_alias(freqstr) if base == self.freq.rule_code: - return other.n * self.freq.n + return other.n msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) raise IncompatibleFrequency(msg) elif lib.is_integer(other): # integer is passed to .shift via # _add_datetimelike_methods basically # but ufunc may pass integer to _add_delta - return other * self.freq.n - elif is_integer_dtype(other) and isinstance(other, np.ndarray): - # TODO: Also need to get Index - return other * self.freq.n + return other # raise when input doesn't have freq msg = "Input has different freq from {cls}(freq={freqstr})" raise IncompatibleFrequency(msg.format(cls=type(self).__name__, freqstr=self.freqstr)) + def _check_timedeltalike_freq_compat(self, other): + assert isinstance(self.freq, Tick) # checked by calling function + own_offset = frequencies.to_offset(self.freq.rule_code) + base_nanos = delta_to_nanoseconds(own_offset) + + if isinstance(other, (timedelta, np.timedelta64, Tick)): + nanos = delta_to_nanoseconds(other) + + elif isinstance(other, np.ndarray): + assert other.dtype.kind == 'm' + if other.dtype != _TD_DTYPE: + # i.e. non-nano unit + # TODO: disallow unit-less timedelta64 + other = other.astype(_TD_DTYPE) + nanos = other.view('i8') + else: + # TimedeltaArray/Index + nanos = other.asi8 + + if np.all(nanos % base_nanos == 0): + # nanos being added is an integer multiple of the + # base-frequency to self.freq + delta = nanos // base_nanos + # delta is the integer (or integer-array) number of periods + # by which will be added to self. + return delta + + raise IncompatibleFrequency("Input has different freq from " + "{cls}(freq={freqstr})" + .format(cls=type(self).__name__, + freqstr=self.freqstr)) + PeriodArrayMixin._add_comparison_ops() PeriodArrayMixin._add_datetimelike_methods() From 9393f5defb9cb0090b9da7462979959b1ff12aa4 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 9 Oct 2018 16:46:56 -0700 Subject: [PATCH 06/13] Add tests for adding offset scalar --- pandas/tests/arithmetic/test_period.py | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pandas/tests/arithmetic/test_period.py b/pandas/tests/arithmetic/test_period.py index 184c0b2ccaa1f..33501c5594e63 100644 --- a/pandas/tests/arithmetic/test_period.py +++ b/pandas/tests/arithmetic/test_period.py @@ -606,6 +606,39 @@ def test_pi_sub_intarray(self, box): # Timedelta-like (timedelta, timedelta64, Timedelta, Tick) # TODO: Some of these are misnomers because of non-Tick DateOffsets + def test_pi_add_timedeltalike_minute_gt1(self, three_days): + # GH#23031 adding a time-delta-like offset to a PeriodArray that has + # minute frequency with n != 1. A more general case is tested below + # in test_pi_add_timedeltalike_tick_gt1, but here we write out the + # expected result more explicitly. + other = three_days + rng = pd.period_range('2014-05-01', periods=3, freq='2D') + + expected = pd.PeriodIndex(['2014-05-04', '2014-05-06', '2014-05-08'], + freq='2D') + + result = rng + other + tm.assert_index_equal(result, expected) + + result = other + rng + tm.assert_index_equal(result, expected) + + @pytest.mark.parametrize('freqstr', ['5ns', '5us', '5ms', + '5s', '5T', '5h', '5d']) + def test_pi_add_timedeltalike_tick_gt1(self, three_days, freqstr): + # GH#23031 adding a time-delta-like offset to a PeriodArray that has + # tick-like frequency with n != 1 + other = three_days + rng = pd.period_range('2014-05-01', periods=6, freq=freqstr) + + expected = pd.period_range(rng[0] + other, periods=6, freq=freqstr) + + result = rng + other + tm.assert_index_equal(result, expected) + + result = other + rng + tm.assert_index_equal(result, expected) + def test_pi_add_iadd_timedeltalike_daily(self, three_days): # Tick other = three_days From 0573f3f2e18fcf92c101725fe7b0062bcf8fef8a Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 9 Oct 2018 16:52:18 -0700 Subject: [PATCH 07/13] add tests for subtracting offset scalar --- pandas/tests/arithmetic/test_period.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pandas/tests/arithmetic/test_period.py b/pandas/tests/arithmetic/test_period.py index 33501c5594e63..d81ab2b3a2ec3 100644 --- a/pandas/tests/arithmetic/test_period.py +++ b/pandas/tests/arithmetic/test_period.py @@ -623,6 +623,15 @@ def test_pi_add_timedeltalike_minute_gt1(self, three_days): result = other + rng tm.assert_index_equal(result, expected) + # subtraction + expected = pd.PeriodIndex(['2014-04-28', '2014-04-30', '2014-05-02'], + freq='2D') + result = rng - other + tm.assert_index_equal(result, expected) + + with pytest.raises(TypeError): + other - rng + @pytest.mark.parametrize('freqstr', ['5ns', '5us', '5ms', '5s', '5T', '5h', '5d']) def test_pi_add_timedeltalike_tick_gt1(self, three_days, freqstr): @@ -639,6 +648,14 @@ def test_pi_add_timedeltalike_tick_gt1(self, three_days, freqstr): result = other + rng tm.assert_index_equal(result, expected) + # subtraction + expected = pd.period_range(rng[0] - other, periods=6, freq=freqstr) + result = rng - other + tm.assert_index_equal(result, expected) + + with pytest.raises(TypeError): + other - rng + def test_pi_add_iadd_timedeltalike_daily(self, three_days): # Tick other = three_days From 30d9f6b020e44c416e820a3cf3d8a6c15adb2020 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 9 Oct 2018 16:58:10 -0700 Subject: [PATCH 08/13] remove superfluous comment --- pandas/core/arrays/period.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 952387fea176b..57c8f275e0aa0 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -458,8 +458,6 @@ def _maybe_convert_timedelta(self, other): """ if isinstance( other, (timedelta, np.timedelta64, Tick, np.ndarray)): - # TODO: is the np.ndarray case still relevant now that Arithmetic - # methods don't call this method? offset = frequencies.to_offset(self.freq.rule_code) if isinstance(offset, Tick): # _check_timedeltalike_freq_compat will raise if incompatible From 9bac7465fd5ab852c0fce7323e17d2188573185d Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 9 Oct 2018 17:07:55 -0700 Subject: [PATCH 09/13] whatsnew --- doc/source/whatsnew/v0.24.0.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index a4209ba90aaee..0eeaff2fe62a1 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -729,6 +729,7 @@ Datetimelike - Bug in :class:`DatetimeIndex` where frequency was being set if original frequency was ``None`` (:issue:`22150`) - Bug in rounding methods of :class:`DatetimeIndex` (:meth:`~DatetimeIndex.round`, :meth:`~DatetimeIndex.ceil`, :meth:`~DatetimeIndex.floor`) and :class:`Timestamp` (:meth:`~Timestamp.round`, :meth:`~Timestamp.ceil`, :meth:`~Timestamp.floor`) could give rise to loss of precision (:issue:`22591`) - Bug in :func:`to_datetime` with an :class:`Index` argument that would drop the ``name`` from the result (:issue:`21697`) +- Bug in :class:`PeriodIndex` where adding or subtracting a :class:`timedelta` or :class:`Tick` object produced incorrect results (:issue:`22988`) Timedelta ^^^^^^^^^ From 59748cdb3b7a2e0f75f581b35f58523d3de08e5a Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 9 Oct 2018 17:15:30 -0700 Subject: [PATCH 10/13] Revert incorrect docstring change --- pandas/core/arrays/period.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 57c8f275e0aa0..4d593fce7f368 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -444,8 +444,7 @@ def _maybe_convert_timedelta(self, other): Parameters ---------- - other : timedelta, np.timedelta64, DateOffset, int, - np.ndarray[timedelta64], TimedeltaIndex + other : timedelta, np.timedelta64, DateOffset, int, np.ndarray Returns ------- From eb252bff0e8f12a228477d7cae507c538cd0abc5 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 14 Oct 2018 11:29:26 -0700 Subject: [PATCH 11/13] docstring --- pandas/core/arrays/period.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 4d593fce7f368..46eee55553e35 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -481,6 +481,25 @@ def _maybe_convert_timedelta(self, other): freqstr=self.freqstr)) def _check_timedeltalike_freq_compat(self, other): + """ + Arithmetic operations with timedelta-like scalars or array `other` + are only valid if `other` is an integer multiple of `self.freq`. + If the operation is valid, find that integer multiple. Otherwise, + raise because the operation is invalid. + + Parameters + ---------- + other : timedelta, np.timedelta64, Tick, + ndarray[timedelta64], TimedeltaArray, TimedeltaIndex + + Returns + ------- + multiple : int or ndarray[int64] + + Raises + ------ + IncompatibleFrequency + """ assert isinstance(self.freq, Tick) # checked by calling function own_offset = frequencies.to_offset(self.freq.rule_code) base_nanos = delta_to_nanoseconds(own_offset) From 8f5fd5547bfb2a450fca0a1e87d2084bb6a5a885 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 14 Oct 2018 11:30:17 -0700 Subject: [PATCH 12/13] comment --- pandas/core/arrays/period.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 46eee55553e35..b61cd8daa47c0 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -508,6 +508,7 @@ def _check_timedeltalike_freq_compat(self, other): nanos = delta_to_nanoseconds(other) elif isinstance(other, np.ndarray): + # numpy timedelta64 array; return an integer array instead of int assert other.dtype.kind == 'm' if other.dtype != _TD_DTYPE: # i.e. non-nano unit From ed50b9f92c6f04c1fba452a27a76b26c2e8231c1 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 14 Oct 2018 11:30:40 -0700 Subject: [PATCH 13/13] comment --- pandas/core/arrays/period.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index b61cd8daa47c0..7aaf3ddbb9c67 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -508,7 +508,7 @@ def _check_timedeltalike_freq_compat(self, other): nanos = delta_to_nanoseconds(other) elif isinstance(other, np.ndarray): - # numpy timedelta64 array; return an integer array instead of int + # numpy timedelta64 array; all entries must be compatible assert other.dtype.kind == 'm' if other.dtype != _TD_DTYPE: # i.e. non-nano unit