diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 5fd7c3e217928..119dd894abe4c 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -370,6 +370,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 incorrect results (:issue:`19012`) +- Bug in :func:`Series.__add__` adding Series with dtype ``timedelta64[ns]`` to a timezone-aware ``DatetimeIndex`` incorrectly dropped timezone information (:issue:`13905`) - Categorical diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 2a77a23c2cfa1..ee2fdd213dd9a 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -671,7 +671,9 @@ def __add__(self, other): from pandas.tseries.offsets import DateOffset other = lib.item_from_zerodim(other) - if is_timedelta64_dtype(other): + if isinstance(other, ABCSeries): + return NotImplemented + elif is_timedelta64_dtype(other): return self._add_delta(other) elif isinstance(self, TimedeltaIndex) and isinstance(other, Index): if hasattr(other, '_add_delta'): @@ -702,7 +704,9 @@ def __sub__(self, other): from pandas.tseries.offsets import DateOffset other = lib.item_from_zerodim(other) - if is_timedelta64_dtype(other): + if isinstance(other, ABCSeries): + return NotImplemented + elif is_timedelta64_dtype(other): return self._add_delta(-other) elif isinstance(self, TimedeltaIndex) and isinstance(other, Index): if not isinstance(other, TimedeltaIndex): diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index b17682b6c3448..ef0406a4b9f9d 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -854,6 +854,9 @@ def _maybe_update_attributes(self, attrs): return attrs def _add_delta(self, delta): + if isinstance(delta, ABCSeries): + return NotImplemented + from pandas import TimedeltaIndex name = self.name diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 89d793a586e74..0229f7c256464 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -39,7 +39,7 @@ from pandas.core.dtypes.generic import ( ABCSeries, ABCDataFrame, - ABCIndex, + ABCIndex, ABCDatetimeIndex, ABCPeriodIndex) # ----------------------------------------------------------------------------- @@ -514,8 +514,9 @@ def _convert_to_array(self, values, name=None, other=None): values[:] = iNaT # a datelike - elif isinstance(values, pd.DatetimeIndex): - values = values.to_series() + elif isinstance(values, ABCDatetimeIndex): + # TODO: why are we casting to_series in the first place? + values = values.to_series(keep_tz=True) # datetime with tz elif (isinstance(ovalues, datetime.datetime) and hasattr(ovalues, 'tzinfo')): @@ -535,6 +536,11 @@ def _convert_to_array(self, values, name=None, other=None): elif inferred_type in ('timedelta', 'timedelta64'): # have a timedelta, convert to to ns here values = to_timedelta(values, errors='coerce', box=False) + if isinstance(other, ABCDatetimeIndex): + # GH#13905 + # Defer to DatetimeIndex/TimedeltaIndex operations where + # timezones are handled carefully. + values = pd.TimedeltaIndex(values) elif inferred_type == 'integer': # py3 compat where dtype is 'm' but is an integer if values.dtype.kind == 'm': @@ -754,25 +760,26 @@ def wrapper(left, right, name=name, na_op=na_op): na_op = converted.na_op if isinstance(rvalues, ABCSeries): - name = _maybe_match_name(left, rvalues) lvalues = getattr(lvalues, 'values', lvalues) rvalues = getattr(rvalues, 'values', rvalues) # _Op aligns left and right else: - 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)): + not isinstance(lvalues, ABCDatetimeIndex)): lvalues = lvalues.values + if isinstance(right, (ABCSeries, pd.Index)): + # `left` is always a Series object + res_name = _maybe_match_name(left, right) + else: + res_name = left.name + result = wrap_results(safe_na_op(lvalues, rvalues)) return construct_result( left, result, index=left.index, - name=name, + name=res_name, dtype=dtype, ) diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index 4684eb89557bf..381e2ef3041e7 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -364,6 +364,33 @@ def test_datetimeindex_sub_timestamp_overflow(self): with pytest.raises(OverflowError): dtimin - variant + @pytest.mark.parametrize('names', [('foo', None, None), + ('baz', 'bar', None), + ('bar', 'bar', 'bar')]) + @pytest.mark.parametrize('tz', [None, 'America/Chicago']) + def test_dti_add_series(self, tz, names): + # GH#13905 + index = DatetimeIndex(['2016-06-28 05:30', '2016-06-28 05:31'], + tz=tz, name=names[0]) + ser = Series([Timedelta(seconds=5)] * 2, + index=index, name=names[1]) + expected = Series(index + Timedelta(seconds=5), + index=index, name=names[2]) + + # passing name arg isn't enough when names[2] is None + expected.name = names[2] + assert expected.dtype == index.dtype + result = ser + index + tm.assert_series_equal(result, expected) + result2 = index + ser + tm.assert_series_equal(result2, expected) + + expected = index + Timedelta(seconds=5) + result3 = ser.values + index + tm.assert_index_equal(result3, expected) + result4 = index + ser.values + tm.assert_index_equal(result4, expected) + @pytest.mark.parametrize('box', [np.array, pd.Index]) def test_dti_add_offset_array(self, tz, box): # GH#18849