From 01316c18178f1c0f70e373b97cc18f8eb3d9bffa Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 19 Dec 2017 08:35:14 -0800 Subject: [PATCH 1/8] implement datetimeindex ops with array of dateoffsets --- pandas/core/dtypes/common.py | 24 ++++++++++- pandas/core/indexes/datetimelike.py | 40 ++++++++++++++++--- pandas/core/ops.py | 20 +++------- pandas/tests/dtypes/test_common.py | 14 +++++++ .../indexes/datetimes/test_arithmetic.py | 21 ++++++++++ 5 files changed, 96 insertions(+), 23 deletions(-) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 5b1335c1a834e..7925c6ff25929 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -12,8 +12,8 @@ from .generic import (ABCCategorical, ABCPeriodIndex, ABCDatetimeIndex, ABCSeries, ABCSparseArray, ABCSparseSeries, ABCCategoricalIndex, - ABCIndexClass) -from .inference import is_string_like + ABCIndexClass, ABCDateOffset) +from .inference import is_string_like, is_list_like from .inference import * # noqa @@ -1174,6 +1174,26 @@ def is_datetime_or_timedelta_dtype(arr_or_dtype): return issubclass(tipo, (np.datetime64, np.timedelta64)) +def is_offsetlike(arr_or_obj): + """ + Check if obj or all elements of list-like is DateOffset + + Parameters + ---------- + arr_or_obj : object + + Returns + ------- + boolean : Whether the object is a DateOffset or listlike of DatetOffsets + """ + if isinstance(arr_or_obj, ABCDateOffset): + return True + elif (is_list_like(arr_or_obj) and len(arr_or_obj) and + is_object_dtype(arr_or_obj)): + return all(isinstance(x, ABCDateOffset) for x in arr_or_obj) + return False + + def _is_unorderable_exception(e): """ Check if the exception raised is an unorderable exception. diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 8cc996285fbbd..a149a3af1c65b 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -13,7 +13,7 @@ from pandas.core.dtypes.common import ( is_integer, is_float, is_bool_dtype, _ensure_int64, - is_scalar, is_dtype_equal, + is_scalar, is_dtype_equal, is_offsetlike, is_list_like, is_timedelta64_dtype) from pandas.core.dtypes.generic import ( ABCIndex, ABCSeries, @@ -650,6 +650,7 @@ def _add_datetimelike_methods(cls): def __add__(self, other): from pandas.core.index import Index from pandas.core.indexes.timedeltas import TimedeltaIndex + from pandas.core.indexes.datetimes import DatetimeIndex from pandas.tseries.offsets import DateOffset if is_timedelta64_dtype(other): return self._add_delta(other) @@ -662,7 +663,21 @@ def __add__(self, other): return self._add_delta(other) elif is_integer(other): return self.shift(other) - elif isinstance(other, (Index, datetime, np.datetime64)): + elif isinstance(other, (datetime, np.datetime64)): + return self._add_datelike(other) + elif (isinstance(self, DatetimeIndex) and is_offsetlike(other) and + not isinstance(other, ABCSeries)): + # Array of DateOffset objects + if len(other) == 1: + return self + other[0] + else: + from pandas.errors import PerformanceWarning + warnings.warn("Adding/subtracting array of DateOffsets to " + "{} not vectorized".format(type(self)), + PerformanceWarning) + + return self.astype('O') + np.array(other) + elif isinstance(other, Index): return self._add_datelike(other) else: # pragma: no cover return NotImplemented @@ -683,10 +698,6 @@ def __sub__(self, other): return self._add_delta(-other) elif isinstance(other, DatetimeIndex): return self._sub_datelike(other) - elif isinstance(other, Index): - raise TypeError("cannot subtract {typ1} and {typ2}" - .format(typ1=type(self).__name__, - typ2=type(other).__name__)) elif isinstance(other, (DateOffset, timedelta)): return self._add_delta(-other) elif is_integer(other): @@ -695,6 +706,23 @@ def __sub__(self, other): return self._sub_datelike(other) elif isinstance(other, Period): return self._sub_period(other) + elif (isinstance(self, DatetimeIndex) and is_offsetlike(other) and + not isinstance(other, ABCSeries)): + # Array of DateOffset objects + if len(other) == 1: + return self - other[0] + else: + from pandas.errors import PerformanceWarning + warnings.warn("Adding/subtracting array of DateOffsets to " + "{} not vectorized".format(type(self)), + PerformanceWarning) + res_values = self.astype('O').values - np.array(other) + return self.__class__(res_values, freq='infer') + elif isinstance(other, Index): + raise TypeError("cannot subtract {typ1} and {typ2}" + .format(typ1=type(self).__name__, + typ2=type(other).__name__)) + else: # pragma: no cover return NotImplemented cls.__sub__ = __sub__ diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 2fb0cbb14c225..aa188db689a0e 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -25,7 +25,7 @@ from pandas.core.dtypes.missing import notna, isna from pandas.core.dtypes.common import ( needs_i8_conversion, - is_datetimelike_v_numeric, + is_datetimelike_v_numeric, is_offsetlike, is_integer_dtype, is_categorical_dtype, is_object_dtype, is_timedelta64_dtype, is_datetime64_dtype, is_datetime64tz_dtype, @@ -38,8 +38,7 @@ ABCSeries, ABCDataFrame, ABCIndex, - ABCPeriodIndex, - ABCDateOffset) + ABCPeriodIndex) # ----------------------------------------------------------------------------- # Functions that add arithmetic methods to objects, given arithmetic factory @@ -363,7 +362,7 @@ def __init__(self, left, right, name, na_op): rvalues = self._convert_to_array(right, name=name, other=lvalues) # left - self.is_offset_lhs = self._is_offset(left) + self.is_offset_lhs = is_offsetlike(left) self.is_timedelta_lhs = is_timedelta64_dtype(lvalues) self.is_datetime64_lhs = is_datetime64_dtype(lvalues) self.is_datetime64tz_lhs = is_datetime64tz_dtype(lvalues) @@ -373,7 +372,7 @@ def __init__(self, left, right, name, na_op): self.is_floating_lhs = left.dtype.kind == 'f' # right - self.is_offset_rhs = self._is_offset(right) + self.is_offset_rhs = is_offsetlike(right) self.is_datetime64_rhs = is_datetime64_dtype(rvalues) self.is_datetime64tz_rhs = is_datetime64tz_dtype(rvalues) self.is_datetime_rhs = (self.is_datetime64_rhs or @@ -515,7 +514,7 @@ def _convert_to_array(self, values, name=None, other=None): values = np.empty(values.shape, dtype=other.dtype) values[:] = iNaT return values - elif self._is_offset(values): + elif is_offsetlike(values): return values else: raise TypeError("incompatible type [{dtype}] for a " @@ -618,15 +617,6 @@ def f(x): return lvalues, rvalues - def _is_offset(self, arr_or_obj): - """ check if obj or all elements of list-like is DateOffset """ - if isinstance(arr_or_obj, ABCDateOffset): - return True - elif (is_list_like(arr_or_obj) and len(arr_or_obj) and - is_object_dtype(arr_or_obj)): - return all(isinstance(x, ABCDateOffset) for x in arr_or_obj) - return False - def _align_method_SERIES(left, right, align_asobject=False): """ align lhs and rhs Series """ diff --git a/pandas/tests/dtypes/test_common.py b/pandas/tests/dtypes/test_common.py index 2146704fea95f..f461d8410d460 100644 --- a/pandas/tests/dtypes/test_common.py +++ b/pandas/tests/dtypes/test_common.py @@ -537,6 +537,20 @@ def test_is_complex_dtype(): assert com.is_complex_dtype(np.array([1 + 1j, 5])) +def test_is_offsetlike(): + assert com.is_offsetlike(np.array([pd.DateOffset(month=3), + pd.offsets.Nano()])) + assert com.is_offsetlike(pd.offsets.MonthEnd()) + assert com.is_offsetlike([pd.offsets.MonthBegin(), + pd.offsets.YearBegin()]) + + assert not com.is_offsetlike(pd.Timedelta(1)) + assert not com.is_offsetlike(np.array([1 + 1j, 5])) + + # mixed case + assert not com.is_offsetlike([pd.DateOffset(), pd.Timestamp(0)]) + + @pytest.mark.parametrize('input_param,result', [ (int, np.dtype(int)), ('int32', np.dtype('int32')), diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index a46462e91a866..2238ed5893efe 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -363,6 +363,27 @@ def test_datetimeindex_sub_timestamp_overflow(self): with pytest.raises(OverflowError): dtimin - variant + @pytest.mark.parametrize('box', [np.array, pd.Index]) + def test_dti_add_offset_array(self, tz, box): + dti = pd.date_range('2017-01-01', periods=2, tz=tz) + other = box([pd.offsets.MonthEnd(), pd.offsets.Day(n=2)]) + res = dti + other + expected = DatetimeIndex([dti[n] + other[n] for n in range(len(dti))], + name=dti.name, freq='infer') + tm.assert_index_equal(res, expected) + + res2 = other + dti + tm.assert_index_equal(res2, expected) + + @pytest.mark.parametrize('box', [np.array, pd.Index]) + def test_dti_sub_offset_array(self, tz, box): + dti = pd.date_range('2017-01-01', periods=2, tz=tz) + other = box([pd.offsets.MonthEnd(), pd.offsets.Day(n=2)]) + res = dti - other + expected = DatetimeIndex([dti[n] - other[n] for n in range(len(dti))], + name=dti.name, freq='infer') + tm.assert_index_equal(res, expected) + # GH 10699 @pytest.mark.parametrize('klass,assert_func', zip([Series, DatetimeIndex], From 179e640011924c5a46b44345be32f0a5eab77c32 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 19 Dec 2017 08:55:19 -0800 Subject: [PATCH 2/8] add test for op against Series --- doc/source/whatsnew/v0.22.0.txt | 1 + pandas/core/indexes/datetimelike.py | 3 +++ .../tests/indexes/datetimes/test_arithmetic.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/doc/source/whatsnew/v0.22.0.txt b/doc/source/whatsnew/v0.22.0.txt index 0579a80aad28e..9270eb631dac8 100644 --- a/doc/source/whatsnew/v0.22.0.txt +++ b/doc/source/whatsnew/v0.22.0.txt @@ -354,3 +354,4 @@ Other - Improved error message when attempting to use a Python keyword as an identifier in a ``numexpr`` backed query (:issue:`18221`) - Bug in :class:`Timestamp` where comparison with an array of ``Timestamp`` objects would result in a ``RecursionError`` (:issue:`15183`) +- Bug in :class:`DatetimeIndex` where adding or subtracting an array-like of ``DateOffset`` objects either raised (``np.array``, ``pd.Index``) or broadcast incorrectly (``pd.Series``) (:issue:`18224`) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index a149a3af1c65b..34d764a9b0a66 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -677,6 +677,7 @@ def __add__(self, other): PerformanceWarning) return self.astype('O') + np.array(other) + # TODO: What if other is an Index and has a name? elif isinstance(other, Index): return self._add_datelike(other) else: # pragma: no cover @@ -717,7 +718,9 @@ def __sub__(self, other): "{} not vectorized".format(type(self)), PerformanceWarning) res_values = self.astype('O').values - np.array(other) + return self.__class__(res_values, freq='infer') + # TODO: What if other is an Index and has a name? elif isinstance(other, Index): raise TypeError("cannot subtract {typ1} and {typ2}" .format(typ1=type(self).__name__, diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index 2238ed5893efe..f33230312193c 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -384,6 +384,24 @@ def test_dti_sub_offset_array(self, tz, box): name=dti.name, freq='infer') tm.assert_index_equal(res, expected) + def test_dti_with_offset_series(self, tz): + dti = pd.date_range('2017-01-01', periods=2, tz=tz) + other = pd.Series([pd.offsets.MonthEnd(), pd.offsets.Day(n=2)], + name='foo') + + expected_add = pd.Series([dti[n] + other[n] for n in range(len(dti))], + name='foo') + res = dti + other + tm.assert_series_equal(res, expected_add) + res2 = other + dti + tm.assert_series_equal(res2, expected_add) + + expected_sub = pd.Series([dti[n] - other[n] for n in range(len(dti))], + name='foo') + + res3 = dti - other + tm.assert_series_equal(res3, expected_sub) + # GH 10699 @pytest.mark.parametrize('klass,assert_func', zip([Series, DatetimeIndex], From 30c7ef6f0096e1d44415205d03265c40afcbcf2e Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 19 Dec 2017 09:04:30 -0800 Subject: [PATCH 3/8] remove duplicated is_offsetlike --- pandas/core/dtypes/common.py | 20 -------------------- pandas/core/ops.py | 2 +- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 91ffba8045f9a..e2ee3deb5396e 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -1205,26 +1205,6 @@ def is_datetime_or_timedelta_dtype(arr_or_dtype): return issubclass(tipo, (np.datetime64, np.timedelta64)) -def is_offsetlike(arr_or_obj): - """ - Check if obj or all elements of list-like is DateOffset - - Parameters - ---------- - arr_or_obj : object - - Returns - ------- - boolean : Whether the object is a DateOffset or listlike of DatetOffsets - """ - if isinstance(arr_or_obj, ABCDateOffset): - return True - elif (is_list_like(arr_or_obj) and len(arr_or_obj) and - is_object_dtype(arr_or_obj)): - return all(isinstance(x, ABCDateOffset) for x in arr_or_obj) - return False - - def _is_unorderable_exception(e): """ Check if the exception raised is an unorderable exception. diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 9205a93451895..e23609b23f529 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -25,7 +25,7 @@ from pandas.core.dtypes.missing import notna, isna from pandas.core.dtypes.common import ( needs_i8_conversion, - is_datetimelike_v_numeric, is_offsetlike, + is_datetimelike_v_numeric, is_integer_dtype, is_categorical_dtype, is_object_dtype, is_timedelta64_dtype, is_datetime64_dtype, is_datetime64tz_dtype, From e58849af2009d50badc26246aeb5419029c78bf8 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 19 Dec 2017 09:20:37 -0800 Subject: [PATCH 4/8] edit comments --- pandas/core/indexes/datetimelike.py | 5 +---- pandas/tests/indexes/datetimes/test_arithmetic.py | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 34d764a9b0a66..45463d726887d 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -675,9 +675,8 @@ def __add__(self, other): warnings.warn("Adding/subtracting array of DateOffsets to " "{} not vectorized".format(type(self)), PerformanceWarning) - return self.astype('O') + np.array(other) - # TODO: What if other is an Index and has a name? + # FIXME: This works for __add__ but loses dtype in __sub__ elif isinstance(other, Index): return self._add_datelike(other) else: # pragma: no cover @@ -718,9 +717,7 @@ def __sub__(self, other): "{} not vectorized".format(type(self)), PerformanceWarning) res_values = self.astype('O').values - np.array(other) - return self.__class__(res_values, freq='infer') - # TODO: What if other is an Index and has a name? elif isinstance(other, Index): raise TypeError("cannot subtract {typ1} and {typ2}" .format(typ1=type(self).__name__, diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index f33230312193c..91f19b5ef4540 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -366,6 +366,7 @@ def test_datetimeindex_sub_timestamp_overflow(self): @pytest.mark.parametrize('box', [np.array, pd.Index]) def test_dti_add_offset_array(self, tz, box): dti = pd.date_range('2017-01-01', periods=2, tz=tz) + # TODO: check that `name` propogates correctly other = box([pd.offsets.MonthEnd(), pd.offsets.Day(n=2)]) res = dti + other expected = DatetimeIndex([dti[n] + other[n] for n in range(len(dti))], From 2fbebc9b3d5f2ec4b3e5b32f1a8d61be5fc761ec Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 20 Dec 2017 10:24:04 -0800 Subject: [PATCH 5/8] implement add_offset_array, sub_offset_array, tests for TimedeltaIndex and PeriodIndex --- pandas/core/indexes/datetimelike.py | 38 +++++++------------ pandas/core/indexes/datetimes.py | 26 +++++++++++++ .../indexes/datetimes/test_arithmetic.py | 2 + .../tests/indexes/period/test_arithmetic.py | 24 ++++++++++++ .../indexes/timedeltas/test_arithmetic.py | 17 +++++++++ 5 files changed, 83 insertions(+), 24 deletions(-) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 45463d726887d..dba9ffd222a72 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -640,6 +640,14 @@ def _sub_datelike(self, other): def _sub_period(self, other): return NotImplemented + def _add_offset_array(self, other): + # Array/Index of DateOffset objects + return NotImplemented + + def _sub_offset_array(self, other): + # Array/Index of DateOffset objects + return NotImplemented + @classmethod def _add_datetimelike_methods(cls): """ @@ -665,18 +673,9 @@ def __add__(self, other): return self.shift(other) elif isinstance(other, (datetime, np.datetime64)): return self._add_datelike(other) - elif (isinstance(self, DatetimeIndex) and is_offsetlike(other) and - not isinstance(other, ABCSeries)): - # Array of DateOffset objects - if len(other) == 1: - return self + other[0] - else: - from pandas.errors import PerformanceWarning - warnings.warn("Adding/subtracting array of DateOffsets to " - "{} not vectorized".format(type(self)), - PerformanceWarning) - return self.astype('O') + np.array(other) - # FIXME: This works for __add__ but loses dtype in __sub__ + elif is_offsetlike(other): + # Array/Index of DateOffset objects + return self._add_offset_array(other) elif isinstance(other, Index): return self._add_datelike(other) else: # pragma: no cover @@ -706,18 +705,9 @@ def __sub__(self, other): return self._sub_datelike(other) elif isinstance(other, Period): return self._sub_period(other) - elif (isinstance(self, DatetimeIndex) and is_offsetlike(other) and - not isinstance(other, ABCSeries)): - # Array of DateOffset objects - if len(other) == 1: - return self - other[0] - else: - from pandas.errors import PerformanceWarning - warnings.warn("Adding/subtracting array of DateOffsets to " - "{} not vectorized".format(type(self)), - PerformanceWarning) - res_values = self.astype('O').values - np.array(other) - return self.__class__(res_values, freq='infer') + elif is_offsetlike(other): + # Array/Index of DateOffset objects + return self._sub_offset_array(other) elif isinstance(other, Index): raise TypeError("cannot subtract {typ1} and {typ2}" .format(typ1=type(self).__name__, diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index ec5c20d341b50..2b8f34b216f1e 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -894,6 +894,32 @@ def _add_offset(self, offset): "or DatetimeIndex", PerformanceWarning) return self.astype('O') + offset + def _add_offset_array(self, other): + # Array/Index of DateOffset objects + if isinstance(other, ABCSeries): + return NotImplemented + elif len(other) == 1: + return self + other[0] + else: + warnings.warn("Adding/subtracting array of DateOffsets to " + "{} not vectorized".format(type(self)), + PerformanceWarning) + return self.astype('O') + np.array(other) + # TODO: This works for __add__ but loses dtype in __sub__ + + def _sub_offset_array(self, other): + # Array/Index of DateOffset objects + if isinstance(other, ABCSeries): + return NotImplemented + elif len(other) == 1: + return self - other[0] + else: + warnings.warn("Adding/subtracting array of DateOffsets to " + "{} not vectorized".format(type(self)), + PerformanceWarning) + res_values = self.astype('O').values - np.array(other) + return self.__class__(res_values, freq='infer') + def _format_native_types(self, na_rep='NaT', date_format=None, **kwargs): from pandas.io.formats.format import _get_format_datetime64_from_values format = _get_format_datetime64_from_values(self, date_format) diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index 91f19b5ef4540..78b3b13967fdc 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -378,6 +378,7 @@ def test_dti_add_offset_array(self, tz, box): @pytest.mark.parametrize('box', [np.array, pd.Index]) def test_dti_sub_offset_array(self, tz, box): + # GH#18824 dti = pd.date_range('2017-01-01', periods=2, tz=tz) other = box([pd.offsets.MonthEnd(), pd.offsets.Day(n=2)]) res = dti - other @@ -386,6 +387,7 @@ def test_dti_sub_offset_array(self, tz, box): tm.assert_index_equal(res, expected) def test_dti_with_offset_series(self, tz): + # GH#18824 dti = pd.date_range('2017-01-01', periods=2, tz=tz) other = pd.Series([pd.offsets.MonthEnd(), pd.offsets.Day(n=2)], name='foo') diff --git a/pandas/tests/indexes/period/test_arithmetic.py b/pandas/tests/indexes/period/test_arithmetic.py index 66aa5d2db6569..cb5639c1603a3 100644 --- a/pandas/tests/indexes/period/test_arithmetic.py +++ b/pandas/tests/indexes/period/test_arithmetic.py @@ -12,6 +12,30 @@ class TestPeriodIndexArithmetic(object): + def test_pi_add_offset_array(self): + pi = pd.PeriodIndex([pd.Period('2015Q1'), pd.Period('2016Q2')]) + offs = np.array([pd.offsets.QuarterEnd(n=1, startingMonth=12), + pd.offsets.QuarterEnd(n=-2, startingMonth=12)]) + res = pi + offs + expected = pd.PeriodIndex([pd.Period('2015Q2'), pd.Period('2015Q4')]) + tm.assert_index_equal(res, expected) + + unanchored = np.array([pd.offsets.Hour(n=1), + pd.offsets.Minute(n=-2)]) + with pytest.raises(period.IncompatibleFrequency): + pi + unanchored + with pytest.raises(TypeError): + unanchored + pi + + @pytest.mark.xfail(reason='GH#18824 radd doesnt implement this case') + def test_pi_radd_offset_array(self): + pi = pd.PeriodIndex([pd.Period('2015Q1'), pd.Period('2016Q2')]) + offs = np.array([pd.offsets.QuarterEnd(n=1, startingMonth=12), + pd.offsets.QuarterEnd(n=-2, startingMonth=12)]) + res = offs + pi + expected = pd.PeriodIndex([pd.Period('2015Q2'), pd.Period('2015Q4')]) + tm.assert_index_equal(res, expected) + def test_add_iadd(self): rng = pd.period_range('1/1/2000', freq='D', periods=5) other = pd.period_range('1/6/2000', freq='D', periods=5) diff --git a/pandas/tests/indexes/timedeltas/test_arithmetic.py b/pandas/tests/indexes/timedeltas/test_arithmetic.py index 087567354d32d..671d634447a7e 100644 --- a/pandas/tests/indexes/timedeltas/test_arithmetic.py +++ b/pandas/tests/indexes/timedeltas/test_arithmetic.py @@ -28,6 +28,23 @@ def freq(request): class TestTimedeltaIndexArithmetic(object): _holder = TimedeltaIndex + @pytest.mark.xfail(reason='GH#18824 ufunc add cannot use operands...') + def test_tdi_with_offset_array(self): + tdi = pd.TimedeltaIndex(['1 days 00:00:00', '3 days 04:00:00']) + offs = np.array([pd.offsets.Hour(n=1), pd.offsets.Minute(n=-2)]) + expected = pd.TimedeltaIndex(['1 days 01:00:00', '3 days 04:02:00']) + + res = tdi + offs + tm.assert_index_equal(res, expected) + + res2 = offs + tdi + tm.assert_index_equal(res2, expected) + + anchored = np.array([pd.offsets.QuarterEnd(), + pd.offsets.Week(weekday=2)]) + with pytest.raises(TypeError): + tdi + anchored + # TODO: Split by ops, better name def test_numeric_compat(self): idx = self._holder(np.arange(5, dtype='int64')) From 4160c07b46d503a3a7608f4dcc3eaf2d889645e9 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 20 Dec 2017 11:50:58 -0800 Subject: [PATCH 6/8] fixup missing import --- pandas/core/indexes/datetimelike.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index dba9ffd222a72..a97acb63708ba 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -658,7 +658,6 @@ def _add_datetimelike_methods(cls): def __add__(self, other): from pandas.core.index import Index from pandas.core.indexes.timedeltas import TimedeltaIndex - from pandas.core.indexes.datetimes import DatetimeIndex from pandas.tseries.offsets import DateOffset if is_timedelta64_dtype(other): return self._add_delta(other) From 0f233ba4dec59ce1508a67239c80d0836ef3fb27 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 23 Dec 2017 16:26:28 -0800 Subject: [PATCH 7/8] Add requested test for name propagation, fi name mathcing for Series+Index --- doc/source/whatsnew/v0.23.0.txt | 2 +- pandas/core/ops.py | 5 ++++- pandas/tests/indexes/datetimes/test_arithmetic.py | 15 +++++++++------ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 519fd181a7781..ef68bb242d2d4 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -285,7 +285,7 @@ Conversion - Bug in :class:`Series` constructor with an int or float list where specifying ``dtype=str``, ``dtype='str'`` or ``dtype='U'`` failed to convert the data elements to strings (:issue:`16605`) - Bug in :class:`Timestamp` where comparison with an array of ``Timestamp`` objects would result in a ``RecursionError`` (:issue:`15183`) - Bug in :class:`WeekOfMonth` and class:`Week` where addition and subtraction did not roll correctly (:issue:`18510`,:issue:`18672`,:issue:`18864`) -- Bug in :class:`DatetimeIndex` where adding or subtracting an array-like of ``DateOffset`` objects either raised (``np.array``, ``pd.Index``) or broadcast incorrectly (``pd.Series``) (:issue:`18224`) +- Bug in :class:`DatetimeIndex` where adding or subtracting an array-like of ``DateOffset`` objects either raised (``np.array``, ``pd.Index``) or broadcast incorrectly (``pd.Series``) (:issue:`18849`) Indexing diff --git a/pandas/core/ops.py b/pandas/core/ops.py index ac9ca03c13973..a627e8001e8fe 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -752,7 +752,10 @@ def wrapper(left, right, name=name, na_op=na_op): rvalues = getattr(rvalues, 'values', rvalues) # _Op aligns left and right else: - name = left.name + if isinstance(rvalues, pd.Index): + name = _maybe_match_name(left, rvalues) + else: + name = left.name if (hasattr(lvalues, 'values') and not isinstance(lvalues, pd.DatetimeIndex)): lvalues = lvalues.values diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index 78b3b13967fdc..bf94da66dadba 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -386,21 +386,24 @@ def test_dti_sub_offset_array(self, tz, box): name=dti.name, freq='infer') tm.assert_index_equal(res, expected) - def test_dti_with_offset_series(self, tz): - # GH#18824 - dti = pd.date_range('2017-01-01', periods=2, tz=tz) + @pytest.mark.parametrize('names', [(None, None, None), + ('foo', 'bar', None), + ('foo', 'foo', 'foo')]) + def test_dti_with_offset_series(self, tz, names): + # GH#18849 + dti = pd.date_range('2017-01-01', periods=2, tz=tz, name=names[0]) other = pd.Series([pd.offsets.MonthEnd(), pd.offsets.Day(n=2)], - name='foo') + name=names[1]) expected_add = pd.Series([dti[n] + other[n] for n in range(len(dti))], - name='foo') + name=names[2]) res = dti + other tm.assert_series_equal(res, expected_add) res2 = other + dti tm.assert_series_equal(res2, expected_add) expected_sub = pd.Series([dti[n] - other[n] for n in range(len(dti))], - name='foo') + name=names[2]) res3 = dti - other tm.assert_series_equal(res3, expected_sub) From d8d0af623a51fc3dc924fd7695c13293bd0c4679 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 27 Dec 2017 21:13:47 -0800 Subject: [PATCH 8/8] add GH issue references to tests --- pandas/tests/indexes/datetimes/test_arithmetic.py | 2 +- pandas/tests/indexes/period/test_arithmetic.py | 2 ++ pandas/tests/indexes/timedeltas/test_arithmetic.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index bf94da66dadba..6cfa083172921 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -365,8 +365,8 @@ def test_datetimeindex_sub_timestamp_overflow(self): @pytest.mark.parametrize('box', [np.array, pd.Index]) def test_dti_add_offset_array(self, tz, box): + # GH#18849 dti = pd.date_range('2017-01-01', periods=2, tz=tz) - # TODO: check that `name` propogates correctly other = box([pd.offsets.MonthEnd(), pd.offsets.Day(n=2)]) res = dti + other expected = DatetimeIndex([dti[n] + other[n] for n in range(len(dti))], diff --git a/pandas/tests/indexes/period/test_arithmetic.py b/pandas/tests/indexes/period/test_arithmetic.py index cb5639c1603a3..b64f9074c3cf0 100644 --- a/pandas/tests/indexes/period/test_arithmetic.py +++ b/pandas/tests/indexes/period/test_arithmetic.py @@ -13,6 +13,7 @@ class TestPeriodIndexArithmetic(object): def test_pi_add_offset_array(self): + # GH#18849 pi = pd.PeriodIndex([pd.Period('2015Q1'), pd.Period('2016Q2')]) offs = np.array([pd.offsets.QuarterEnd(n=1, startingMonth=12), pd.offsets.QuarterEnd(n=-2, startingMonth=12)]) @@ -29,6 +30,7 @@ def test_pi_add_offset_array(self): @pytest.mark.xfail(reason='GH#18824 radd doesnt implement this case') def test_pi_radd_offset_array(self): + # GH#18849 pi = pd.PeriodIndex([pd.Period('2015Q1'), pd.Period('2016Q2')]) offs = np.array([pd.offsets.QuarterEnd(n=1, startingMonth=12), pd.offsets.QuarterEnd(n=-2, startingMonth=12)]) diff --git a/pandas/tests/indexes/timedeltas/test_arithmetic.py b/pandas/tests/indexes/timedeltas/test_arithmetic.py index 671d634447a7e..3c567e52cccb5 100644 --- a/pandas/tests/indexes/timedeltas/test_arithmetic.py +++ b/pandas/tests/indexes/timedeltas/test_arithmetic.py @@ -30,6 +30,7 @@ class TestTimedeltaIndexArithmetic(object): @pytest.mark.xfail(reason='GH#18824 ufunc add cannot use operands...') def test_tdi_with_offset_array(self): + # GH#18849 tdi = pd.TimedeltaIndex(['1 days 00:00:00', '3 days 04:00:00']) offs = np.array([pd.offsets.Hour(n=1), pd.offsets.Minute(n=-2)]) expected = pd.TimedeltaIndex(['1 days 01:00:00', '3 days 04:02:00'])