From 3813b30381ad2c35bd8023d89e6314bb50a50a2d Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 30 Dec 2017 16:48:00 -0800 Subject: [PATCH 1/5] handle addition of zero-dim integer arrays --- pandas/core/indexes/datetimelike.py | 10 ++++++++-- pandas/tests/indexes/conftest.py | 7 +++++++ .../tests/indexes/datetimes/test_arithmetic.py | 16 ++++++++-------- pandas/tests/indexes/period/test_arithmetic.py | 14 ++++++++------ .../tests/indexes/timedeltas/test_arithmetic.py | 16 ++++++++-------- 5 files changed, 39 insertions(+), 24 deletions(-) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 10c9e8e7dd18f..e41946f12fd42 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -678,7 +678,8 @@ def __add__(self, other): .format(typ=type(other))) elif isinstance(other, (DateOffset, timedelta)): return self._add_delta(other) - elif is_integer(other): + elif is_integer(other) or (is_integer_dtype(other) and + is_zero_dim_array(other)): return self.shift(other) elif isinstance(other, (datetime, np.datetime64)): return self._add_datelike(other) @@ -708,7 +709,8 @@ def __sub__(self, other): return self._sub_datelike(other) elif isinstance(other, (DateOffset, timedelta)): return self._add_delta(-other) - elif is_integer(other): + elif is_integer(other) or (is_integer_dtype(other) and + is_zero_dim_array(other)): return self.shift(-other) elif isinstance(other, (datetime, np.datetime64)): return self._sub_datelike(other) @@ -903,6 +905,10 @@ def astype(self, dtype, copy=True): return super(DatetimeIndexOpsMixin, self).astype(dtype, copy=copy) +def is_zero_dim_array(obj): + return isinstance(obj, np.ndarray) and obj.ndim == 0 + + def _ensure_datetimelike_to_i8(other): """ helper for coercing an input scalar or array to i8 """ if is_scalar(other) and isna(other): diff --git a/pandas/tests/indexes/conftest.py b/pandas/tests/indexes/conftest.py index a0ee3e511ef37..217ee07affa84 100644 --- a/pandas/tests/indexes/conftest.py +++ b/pandas/tests/indexes/conftest.py @@ -1,4 +1,5 @@ import pytest +import numpy as np import pandas.util.testing as tm from pandas.core.indexes.api import Index, MultiIndex @@ -22,3 +23,9 @@ ids=lambda x: type(x).__name__) def indices(request): return request.param + + +@pytest.fixture(params=[1, np.array(1, dtype=np.int64)]) +def one(request): + # zero-dim integer array behaves like an integer + return request.param diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index 11a52267ed1b4..888ef8d578640 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -58,36 +58,36 @@ def test_dti_radd_timestamp_raises(self): # ------------------------------------------------------------- # Binary operations DatetimeIndex and int - def test_dti_add_int(self, tz): + def test_dti_add_int(self, tz, one): rng = pd.date_range('2000-01-01 09:00', freq='H', periods=10, tz=tz) - result = rng + 1 + result = rng + one expected = pd.date_range('2000-01-01 10:00', freq='H', periods=10, tz=tz) tm.assert_index_equal(result, expected) - def test_dti_iadd_int(self, tz): + def test_dti_iadd_int(self, tz, one): rng = pd.date_range('2000-01-01 09:00', freq='H', periods=10, tz=tz) expected = pd.date_range('2000-01-01 10:00', freq='H', periods=10, tz=tz) - rng += 1 + rng += one tm.assert_index_equal(rng, expected) - def test_dti_sub_int(self, tz): + def test_dti_sub_int(self, tz, one): rng = pd.date_range('2000-01-01 09:00', freq='H', periods=10, tz=tz) - result = rng - 1 + result = rng - one expected = pd.date_range('2000-01-01 08:00', freq='H', periods=10, tz=tz) tm.assert_index_equal(result, expected) - def test_dti_isub_int(self, tz): + def test_dti_isub_int(self, tz, one): rng = pd.date_range('2000-01-01 09:00', freq='H', periods=10, tz=tz) expected = pd.date_range('2000-01-01 08:00', freq='H', periods=10, tz=tz) - rng -= 1 + rng -= one tm.assert_index_equal(rng, expected) # ------------------------------------------------------------- diff --git a/pandas/tests/indexes/period/test_arithmetic.py b/pandas/tests/indexes/period/test_arithmetic.py index b64f9074c3cf0..3cd5d97c8f543 100644 --- a/pandas/tests/indexes/period/test_arithmetic.py +++ b/pandas/tests/indexes/period/test_arithmetic.py @@ -131,19 +131,21 @@ def test_add_iadd(self): period.IncompatibleFrequency, msg): rng += delta - # int + def test_pi_add_int(self, one): + # Int or int-like rng = pd.period_range('2000-01-01 09:00', freq='H', periods=10) - result = rng + 1 + result = rng + one expected = pd.period_range('2000-01-01 10:00', freq='H', periods=10) tm.assert_index_equal(result, expected) - rng += 1 + rng += one tm.assert_index_equal(rng, expected) - def test_sub(self): + @pytest.mark.parametrize('five', [5, np.array(5, dtype=np.int64)]) + def test_sub(self, five): rng = period_range('2007-01', periods=50) - result = rng - 5 - exp = rng + (-5) + result = rng - five + exp = rng + (-five) tm.assert_index_equal(result, exp) def test_sub_isub(self): diff --git a/pandas/tests/indexes/timedeltas/test_arithmetic.py b/pandas/tests/indexes/timedeltas/test_arithmetic.py index 3c567e52cccb5..f5b514d9635e4 100644 --- a/pandas/tests/indexes/timedeltas/test_arithmetic.py +++ b/pandas/tests/indexes/timedeltas/test_arithmetic.py @@ -121,28 +121,28 @@ def test_ufunc_coercions(self): # ------------------------------------------------------------- # Binary operations TimedeltaIndex and integer - def test_tdi_add_int(self): + def test_tdi_add_int(self, one): rng = timedelta_range('1 days 09:00:00', freq='H', periods=10) - result = rng + 1 + result = rng + one expected = timedelta_range('1 days 10:00:00', freq='H', periods=10) tm.assert_index_equal(result, expected) - def test_tdi_iadd_int(self): + def test_tdi_iadd_int(self, one): rng = timedelta_range('1 days 09:00:00', freq='H', periods=10) expected = timedelta_range('1 days 10:00:00', freq='H', periods=10) - rng += 1 + rng += one tm.assert_index_equal(rng, expected) - def test_tdi_sub_int(self): + def test_tdi_sub_int(self, one): rng = timedelta_range('1 days 09:00:00', freq='H', periods=10) - result = rng - 1 + result = rng - one expected = timedelta_range('1 days 08:00:00', freq='H', periods=10) tm.assert_index_equal(result, expected) - def test_tdi_isub_int(self): + def test_tdi_isub_int(self, one): rng = timedelta_range('1 days 09:00:00', freq='H', periods=10) expected = timedelta_range('1 days 08:00:00', freq='H', periods=10) - rng -= 1 + rng -= one tm.assert_index_equal(rng, expected) # ------------------------------------------------------------- From 1eb79c5eda656e2202823953c8846ae5e6f03828 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 30 Dec 2017 17:00:41 -0800 Subject: [PATCH 2/5] test for is_zero_dim_array, catch it in is_offsetlike --- pandas/core/dtypes/common.py | 4 +++- pandas/core/dtypes/inference.py | 16 ++++++++++++++++ pandas/core/indexes/datetimelike.py | 6 +----- pandas/tests/dtypes/test_inference.py | 9 +++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index e2ee3deb5396e..4abf6faa8ce59 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -13,7 +13,7 @@ ABCDatetimeIndex, ABCSeries, ABCSparseArray, ABCSparseSeries, ABCCategoricalIndex, ABCIndexClass, ABCDateOffset) -from .inference import is_string_like, is_list_like +from .inference import is_string_like, is_list_like, is_zero_dim_array from .inference import * # noqa @@ -291,6 +291,8 @@ def is_offsetlike(arr_or_obj): """ if isinstance(arr_or_obj, ABCDateOffset): return True + elif is_zero_dim_array(arr_or_obj): + return isinstance(arr_or_obj.item(), ABCDateOffset) 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) diff --git a/pandas/core/dtypes/inference.py b/pandas/core/dtypes/inference.py index 8010a213efaf0..5c3e880d3fde4 100644 --- a/pandas/core/dtypes/inference.py +++ b/pandas/core/dtypes/inference.py @@ -24,6 +24,22 @@ is_interval = lib.is_interval +def is_zero_dim_array(obj): + """ + Check if this is a numpy array with dimension zero, which in some + cases is treated like a scalar. + + Parameters + ---------- + obj : object + + Returns + ------- + is_zero_dim_array : bool + """ + return isinstance(obj, np.ndarray) and obj.ndim == 0 + + def is_number(obj): """ Check if the object is a number. diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index e41946f12fd42..4e01068270238 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -15,7 +15,7 @@ is_dtype_equal, is_float, is_integer, - is_list_like, + is_list_like, is_zero_dim_array, is_scalar, is_bool_dtype, is_offsetlike, @@ -905,10 +905,6 @@ def astype(self, dtype, copy=True): return super(DatetimeIndexOpsMixin, self).astype(dtype, copy=copy) -def is_zero_dim_array(obj): - return isinstance(obj, np.ndarray) and obj.ndim == 0 - - def _ensure_datetimelike_to_i8(other): """ helper for coercing an input scalar or array to i8 """ if is_scalar(other) and isna(other): diff --git a/pandas/tests/dtypes/test_inference.py b/pandas/tests/dtypes/test_inference.py index 33c570a814e7d..825b6c7bd2f2b 100644 --- a/pandas/tests/dtypes/test_inference.py +++ b/pandas/tests/dtypes/test_inference.py @@ -33,6 +33,7 @@ is_integer, is_float, is_bool, + is_zero_dim_array, is_scalar, is_scipy_sparse, _ensure_int32, @@ -1218,3 +1219,11 @@ def test_ensure_categorical(): values = Categorical(values) result = _ensure_categorical(values) tm.assert_categorical_equal(result, values) + + +def test_is_zero_dim_array(): + assert not is_zero_dim_array(1) + assert not is_zero_dim_array(False) + assert not is_zero_dim_array(np.array([])) + assert not is_zero_dim_array(np.array([1])) + assert is_zero_dim_array(np.array(1)) From bd7d1917a3e9805ea76e336b67500462f5e8b524 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 30 Dec 2017 17:18:26 -0800 Subject: [PATCH 3/5] Whatsnew note, issue reference --- doc/source/whatsnew/v0.23.0.txt | 2 +- pandas/tests/indexes/datetimes/test_arithmetic.py | 1 + pandas/tests/indexes/period/test_arithmetic.py | 2 +- pandas/tests/indexes/timedeltas/test_arithmetic.py | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 6407a33c442d0..69a1f10d6d488 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -368,7 +368,7 @@ Numeric ^^^^^^^ - Bug in :func:`Series.__sub__` subtracting a non-nanosecond ``np.datetime64`` object from a ``Series`` gave incorrect results (:issue:`7996`) -- +- Bug in :class:`DatetimeIndex`, :class:`TimedeltaIndex` addition and subtraction of zero-dimensional integer arrays gave incrrect results (:issue:`19012`) - Categorical diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index 888ef8d578640..4684eb89557bf 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -59,6 +59,7 @@ def test_dti_radd_timestamp_raises(self): # Binary operations DatetimeIndex and int def test_dti_add_int(self, tz, one): + # Variants of `one` for #19012 rng = pd.date_range('2000-01-01 09:00', freq='H', periods=10, tz=tz) result = rng + one diff --git a/pandas/tests/indexes/period/test_arithmetic.py b/pandas/tests/indexes/period/test_arithmetic.py index 3cd5d97c8f543..356ea5fc656de 100644 --- a/pandas/tests/indexes/period/test_arithmetic.py +++ b/pandas/tests/indexes/period/test_arithmetic.py @@ -132,7 +132,7 @@ def test_add_iadd(self): rng += delta def test_pi_add_int(self, one): - # Int or int-like + # Variants of `one` for #19012 rng = pd.period_range('2000-01-01 09:00', freq='H', periods=10) result = rng + one expected = pd.period_range('2000-01-01 10:00', freq='H', periods=10) diff --git a/pandas/tests/indexes/timedeltas/test_arithmetic.py b/pandas/tests/indexes/timedeltas/test_arithmetic.py index f5b514d9635e4..3ecfcaff63bc5 100644 --- a/pandas/tests/indexes/timedeltas/test_arithmetic.py +++ b/pandas/tests/indexes/timedeltas/test_arithmetic.py @@ -122,6 +122,7 @@ def test_ufunc_coercions(self): # Binary operations TimedeltaIndex and integer def test_tdi_add_int(self, one): + # Variants of `one` for #19012 rng = timedelta_range('1 days 09:00:00', freq='H', periods=10) result = rng + one expected = timedelta_range('1 days 10:00:00', freq='H', periods=10) From e07c43450f2703eed9e19ce1da96b913a5282466 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 30 Dec 2017 18:14:52 -0800 Subject: [PATCH 4/5] use lib.item_from_zero_dim --- pandas/core/dtypes/common.py | 4 +--- pandas/core/dtypes/inference.py | 16 ---------------- pandas/core/indexes/datetimelike.py | 10 +++++----- pandas/tests/dtypes/test_inference.py | 9 --------- 4 files changed, 6 insertions(+), 33 deletions(-) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 4abf6faa8ce59..e2ee3deb5396e 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -13,7 +13,7 @@ ABCDatetimeIndex, ABCSeries, ABCSparseArray, ABCSparseSeries, ABCCategoricalIndex, ABCIndexClass, ABCDateOffset) -from .inference import is_string_like, is_list_like, is_zero_dim_array +from .inference import is_string_like, is_list_like from .inference import * # noqa @@ -291,8 +291,6 @@ def is_offsetlike(arr_or_obj): """ if isinstance(arr_or_obj, ABCDateOffset): return True - elif is_zero_dim_array(arr_or_obj): - return isinstance(arr_or_obj.item(), ABCDateOffset) 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) diff --git a/pandas/core/dtypes/inference.py b/pandas/core/dtypes/inference.py index 5c3e880d3fde4..8010a213efaf0 100644 --- a/pandas/core/dtypes/inference.py +++ b/pandas/core/dtypes/inference.py @@ -24,22 +24,6 @@ is_interval = lib.is_interval -def is_zero_dim_array(obj): - """ - Check if this is a numpy array with dimension zero, which in some - cases is treated like a scalar. - - Parameters - ---------- - obj : object - - Returns - ------- - is_zero_dim_array : bool - """ - return isinstance(obj, np.ndarray) and obj.ndim == 0 - - def is_number(obj): """ Check if the object is a number. diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 4e01068270238..81a29409003d3 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -15,7 +15,7 @@ is_dtype_equal, is_float, is_integer, - is_list_like, is_zero_dim_array, + is_list_like, is_scalar, is_bool_dtype, is_offsetlike, @@ -669,6 +669,7 @@ def __add__(self, other): from pandas.core.index import Index from pandas.core.indexes.timedeltas import TimedeltaIndex from pandas.tseries.offsets import DateOffset + other = lib.item_from_zerodim(other) # GH#19011 if is_timedelta64_dtype(other): return self._add_delta(other) elif isinstance(self, TimedeltaIndex) and isinstance(other, Index): @@ -678,8 +679,7 @@ def __add__(self, other): .format(typ=type(other))) elif isinstance(other, (DateOffset, timedelta)): return self._add_delta(other) - elif is_integer(other) or (is_integer_dtype(other) and - is_zero_dim_array(other)): + elif is_integer(other): return self.shift(other) elif isinstance(other, (datetime, np.datetime64)): return self._add_datelike(other) @@ -698,6 +698,7 @@ def __sub__(self, other): from pandas.core.indexes.datetimes import DatetimeIndex from pandas.core.indexes.timedeltas import TimedeltaIndex from pandas.tseries.offsets import DateOffset + other = lib.item_from_zerodim(other) # GH#19011 if is_timedelta64_dtype(other): return self._add_delta(-other) elif isinstance(self, TimedeltaIndex) and isinstance(other, Index): @@ -709,8 +710,7 @@ def __sub__(self, other): return self._sub_datelike(other) elif isinstance(other, (DateOffset, timedelta)): return self._add_delta(-other) - elif is_integer(other) or (is_integer_dtype(other) and - is_zero_dim_array(other)): + elif is_integer(other): return self.shift(-other) elif isinstance(other, (datetime, np.datetime64)): return self._sub_datelike(other) diff --git a/pandas/tests/dtypes/test_inference.py b/pandas/tests/dtypes/test_inference.py index 825b6c7bd2f2b..33c570a814e7d 100644 --- a/pandas/tests/dtypes/test_inference.py +++ b/pandas/tests/dtypes/test_inference.py @@ -33,7 +33,6 @@ is_integer, is_float, is_bool, - is_zero_dim_array, is_scalar, is_scipy_sparse, _ensure_int32, @@ -1219,11 +1218,3 @@ def test_ensure_categorical(): values = Categorical(values) result = _ensure_categorical(values) tm.assert_categorical_equal(result, values) - - -def test_is_zero_dim_array(): - assert not is_zero_dim_array(1) - assert not is_zero_dim_array(False) - assert not is_zero_dim_array(np.array([])) - assert not is_zero_dim_array(np.array([1])) - assert is_zero_dim_array(np.array(1)) From 67522b38e114715e191ff82ece838841f83c9e23 Mon Sep 17 00:00:00 2001 From: Jeff Reback Date: Sun, 31 Dec 2017 09:48:43 -0500 Subject: [PATCH 5/5] lint --- doc/source/whatsnew/v0.23.0.txt | 2 +- pandas/core/indexes/datetimelike.py | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 69a1f10d6d488..b169d8600169e 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -368,7 +368,7 @@ Numeric ^^^^^^^ - Bug in :func:`Series.__sub__` subtracting a non-nanosecond ``np.datetime64`` object from a ``Series`` gave incorrect results (:issue:`7996`) -- Bug in :class:`DatetimeIndex`, :class:`TimedeltaIndex` addition and subtraction of zero-dimensional integer arrays gave incrrect results (:issue:`19012`) +- Bug in :class:`DatetimeIndex`, :class:`TimedeltaIndex` addition and subtraction of zero-dimensional integer arrays gave incorrect results (:issue:`19012`) - Categorical diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 81a29409003d3..2a77a23c2cfa1 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -669,7 +669,8 @@ def __add__(self, other): from pandas.core.index import Index from pandas.core.indexes.timedeltas import TimedeltaIndex from pandas.tseries.offsets import DateOffset - other = lib.item_from_zerodim(other) # GH#19011 + + other = lib.item_from_zerodim(other) if is_timedelta64_dtype(other): return self._add_delta(other) elif isinstance(self, TimedeltaIndex) and isinstance(other, Index): @@ -690,6 +691,7 @@ def __add__(self, other): return self._add_datelike(other) else: # pragma: no cover return NotImplemented + cls.__add__ = __add__ cls.__radd__ = __add__ @@ -698,7 +700,8 @@ def __sub__(self, other): from pandas.core.indexes.datetimes import DatetimeIndex from pandas.core.indexes.timedeltas import TimedeltaIndex from pandas.tseries.offsets import DateOffset - other = lib.item_from_zerodim(other) # GH#19011 + + other = lib.item_from_zerodim(other) if is_timedelta64_dtype(other): return self._add_delta(-other) elif isinstance(self, TimedeltaIndex) and isinstance(other, Index): @@ -726,6 +729,7 @@ def __sub__(self, other): else: # pragma: no cover return NotImplemented + cls.__sub__ = __sub__ def __rsub__(self, other): @@ -739,8 +743,10 @@ def _add_delta(self, other): return NotImplemented def _add_delta_td(self, other): - # add a delta of a timedeltalike - # return the i8 result view + """ + Add a delta of a timedeltalike + return the i8 result view + """ inc = delta_to_nanoseconds(other) new_values = checked_add_with_arr(self.asi8, inc, @@ -750,8 +756,10 @@ def _add_delta_td(self, other): return new_values.view('i8') def _add_delta_tdi(self, other): - # add a delta of a TimedeltaIndex - # return the i8 result view + """ + Add a delta of a TimedeltaIndex + return the i8 result view + """ # delta operation if not len(self) == len(other):