-
-
Notifications
You must be signed in to change notification settings - Fork 18.4k
BUG: fix tz-aware DatetimeIndex + TimedeltaIndex (#17558) #17583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,8 @@ | |
is_integer, is_float, | ||
is_bool_dtype, _ensure_int64, | ||
is_scalar, is_dtype_equal, | ||
is_list_like) | ||
is_timedelta64_dtype, is_datetime64tz_dtype, | ||
is_integer_dtype, is_list_like) | ||
from pandas.core.dtypes.generic import ( | ||
ABCIndex, ABCSeries, | ||
ABCPeriodIndex, ABCIndexClass) | ||
|
@@ -651,6 +652,18 @@ def __add__(self, other): | |
raise TypeError("cannot add {typ1} and {typ2}" | ||
.format(typ1=type(self).__name__, | ||
typ2=type(other).__name__)) | ||
elif isinstance(other, np.ndarray): | ||
if is_timedelta64_dtype(other): | ||
return self._add_delta(other) | ||
elif is_integer_dtype(other): | ||
# for internal use only: | ||
# allow PeriodIndex + np.array(int64) to | ||
# fallthrough to ufunc operator | ||
return NotImplemented | ||
else: | ||
raise TypeError("cannot add {typ1} and np.ndarray[{typ2}]" | ||
.format(typ1=type(self).__name__, | ||
typ2=other.dtype)) | ||
elif isinstance(other, (DateOffset, timedelta, np.timedelta64, | ||
Timedelta)): | ||
return self._add_delta(other) | ||
|
@@ -717,7 +730,7 @@ def _add_delta_td(self, other): | |
|
||
def _add_delta_tdi(self, other): | ||
# add a delta of a TimedeltaIndex | ||
# return the i8 result view | ||
# return the i8 result view for datetime64tz | ||
|
||
# delta operation | ||
if not len(self) == len(other): | ||
|
@@ -731,6 +744,8 @@ def _add_delta_tdi(self, other): | |
if self.hasnans or other.hasnans: | ||
mask = (self._isnan) | (other._isnan) | ||
new_values[mask] = iNaT | ||
if is_datetime64tz_dtype(self.dtype): | ||
return new_values.view('i8') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, see my comments |
||
return new_values.view(self.dtype) | ||
|
||
def isin(self, values): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
is_integer, is_float, | ||
is_integer_dtype, | ||
is_datetime64_ns_dtype, | ||
is_timedelta64_dtype, | ||
is_period_dtype, | ||
is_bool_dtype, | ||
is_string_dtype, | ||
|
@@ -801,6 +802,8 @@ def _add_delta(self, delta): | |
|
||
if isinstance(delta, (Tick, timedelta, np.timedelta64)): | ||
new_values = self._add_delta_td(delta) | ||
elif isinstance(delta, np.ndarray) and is_timedelta64_dtype(delta): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should simply be incorporated into the case right below There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. by below, do I understand you correctly that we can consolidate as: if isinstance(delta, (Tick, timedelta, np.timedelta64, np.ndarray)):
new_values = self._add_delta_td(delta)
elif ..: or are you suggesting to wrap There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes |
||
new_values = self._add_delta_td(delta) | ||
elif isinstance(delta, TimedeltaIndex): | ||
new_values = self._add_delta_tdi(delta) | ||
# update name when delta is Index | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -312,9 +312,11 @@ def _maybe_update_attributes(self, attrs): | |
return attrs | ||
|
||
def _add_delta(self, delta): | ||
name = self.name | ||
if isinstance(delta, (Tick, timedelta, np.timedelta64)): | ||
new_values = self._add_delta_td(delta) | ||
name = self.name | ||
elif isinstance(delta, np.ndarray) and is_timedelta64_dtype(delta): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same comment |
||
new_values = self._add_delta_td(delta) | ||
elif isinstance(delta, TimedeltaIndex): | ||
new_values = self._add_delta_tdi(delta) | ||
# update name when delta is index | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,11 @@ | ||
""" generic datetimelike tests """ | ||
|
||
from .common import Base | ||
import numpy as np | ||
import pandas as pd | ||
import pandas.util.testing as tm | ||
|
||
from .common import Base | ||
|
||
|
||
class DatetimeLike(Base): | ||
|
||
|
@@ -38,3 +41,30 @@ def test_view(self): | |
i_view = i.view(self._holder) | ||
result = self._holder(i) | ||
tm.assert_index_equal(result, i_view) | ||
|
||
def test_add_timedelta(self): | ||
# GH 17558 | ||
# Check that tz-aware DatetimeIndex + np.array(dtype="timedelta64") | ||
# and DatetimeIndex + TimedeltaIndex work as expected | ||
idx = self.create_index() | ||
idx.name = "x" | ||
if isinstance(idx, pd.DatetimeIndex): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't use an if like this. let's create a sub-class that specifically tests tz-aware (so .create_index()) works correctly. (add in datetimes/test_datetimelikes) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally agree with this, but unfortunately a tz-aware-based subclass fails a whole bunch of other common tests, so the scope of this PR would have to be expanded further (for which I'd probably need help). Here's one example to illustrate, basically def test_repr_roundtrip(self):
idx = self.create_index()
> tm.assert_index_equal(eval(repr(idx)), idx)
> raise AssertionError(msg)
E AssertionError: Index are different
E
E Index values are different (100.0 %)
E [left]: DatetimeIndex(['2013-01-01 05:00:00-05:00', '2013-01-02 05:00:00-05:00',
E '2013-01-03 05:00:00-05:00', '2013-01-04 05:00:00-05:00',
E '2013-01-05 05:00:00-05:00'],
E dtype='datetime64[ns, US/Eastern]', freq='D')
E [right]: DatetimeIndex(['2013-01-01 00:00:00-05:00', '2013-01-02 00:00:00-05:00',
E '2013-01-03 00:00:00-05:00', '2013-01-04 00:00:00-05:00',
E '2013-01-05 00:00:00-05:00'],
E dtype='datetime64[ns, US/Eastern]', freq='D')
(Pdb) repr(idx) # broke up output for readability
"DatetimeIndex(['2013-01-01 00:00:00-05:00', '2013-01-02 00:00:00-05:00',\n"
" '2013-01-03 00:00:00-05:00', '2013-01-04 00:00:00-05:00',\n"
" '2013-01-05 00:00:00-05:00'],\n"
" dtype='datetime64[ns, US/Eastern]', freq='D')"
(Pdb) eval(repr(idx))
DatetimeIndex(['2013-01-01 05:00:00-05:00', '2013-01-02 05:00:00-05:00',
'2013-01-03 05:00:00-05:00', '2013-01-04 05:00:00-05:00',
'2013-01-05 05:00:00-05:00'],
dtype='datetime64[ns, US/Eastern]', freq='D') Most of the other test failures can also be treated as variants of this, as these tests also construct a separate Also the tests related to union, intersection, symmetric_difference, etc try to do these operations against the underlying numpy There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I think the right way to do this is to do another PR which separates out the DTI tests to 2 classes for tz-naive and tz-aware and modifies create_index as appropriate. If there are failing tests we can look closer. Want to start on that? |
||
idx = idx.tz_localize("US/Eastern") | ||
|
||
expected = idx + np.timedelta64(1, 'D') | ||
tm.assert_index_equal(idx, expected - np.timedelta64(1, 'D')) | ||
|
||
deltas = np.array([np.timedelta64(1, 'D')] * len(idx), | ||
dtype="timedelta64[ns]") | ||
results = [idx + deltas, # add numpy array | ||
idx + deltas.astype(dtype="timedelta64[m]"), | ||
idx + pd.TimedeltaIndex(deltas, name=idx.name), | ||
idx + pd.to_timedelta(deltas[0]), | ||
] | ||
for actual in results: | ||
tm.assert_index_equal(actual, expected) | ||
|
||
errmsg = (r"cannot add {cls} and np.ndarray\[float64\]" | ||
.format(cls=idx.__class__.__name__)) | ||
with tm.assert_raises_regex(TypeError, errmsg): | ||
idx + np.array([0.1], dtype=np.float64) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -76,3 +76,27 @@ def test_union(self): | |
for case in cases: | ||
result = first.union(case) | ||
assert tm.equalContents(result, everything) | ||
|
||
def test_add_dti_td(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is almost the same as special case of the above test in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see my comments above you can simply re-define create_index to make this work (in another test class) |
||
# GH 17558 | ||
# Check that tz-aware DatetimeIndex + np.array(dtype="timedelta64") | ||
# and DatetimeIndex + TimedeltaIndex work as expected | ||
dti = pd.DatetimeIndex([pd.Timestamp("2017/01/01")], | ||
name="x").tz_localize('US/Eastern') | ||
|
||
expected = pd.DatetimeIndex([pd.Timestamp("2017/01/01 01:00")], | ||
name="x").tz_localize('US/Eastern') | ||
|
||
td_np = np.array([np.timedelta64(1, 'h')], dtype="timedelta64[ns]") | ||
results = [dti + td_np, # add numpy array | ||
dti + td_np.astype(dtype="timedelta64[m]"), | ||
dti + pd.TimedeltaIndex(td_np, name=dti.name), | ||
dti + td_np[0], # add timedelta scalar | ||
dti + pd.to_timedelta(td_np[0]), | ||
] | ||
for actual in results: | ||
tm.assert_index_equal(actual, expected) | ||
|
||
errmsg = r"cannot add DatetimeIndex and np.ndarray\[float64\]" | ||
with tm.assert_raises_regex(TypeError, errmsg): | ||
dti + np.array([0.1], dtype=np.float64) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think instead of this, we want to specifically define add in PeriodIndex which handles this case and calls super otherwise.