-
-
Notifications
You must be signed in to change notification settings - Fork 18.4k
delegate (most) datetimelike Series arithmetic ops to DatetimeIndex #18817
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 2 commits
8fb1a0d
6d7c6fb
dd88a2c
d86af39
bbea697
99e6cc6
61f22ba
cf677a1
7be6a97
ab75018
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 |
---|---|---|
|
@@ -363,7 +363,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_offset(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 +373,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_offset(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 | ||
|
@@ -489,6 +489,7 @@ def _convert_to_array(self, values, name=None, other=None): | |
# datetime with tz | ||
elif (isinstance(ovalues, datetime.datetime) and | ||
hasattr(ovalues, 'tzinfo')): | ||
# TODO: does this mean to say `ovalues.tzinfo is not None`? | ||
values = pd.DatetimeIndex(values) | ||
# datetime array with tz | ||
elif is_datetimetz(values): | ||
|
@@ -515,7 +516,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_offset(values): | ||
return values | ||
else: | ||
raise TypeError("incompatible type [{dtype}] for a " | ||
|
@@ -618,14 +619,15 @@ 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 _is_offset(arr_or_obj): | ||
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. better to move this to pandas.core.dtypes.common and de-privatize 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. and add tests 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. Agreed on all three points. |
||
""" 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): | ||
|
@@ -663,6 +665,15 @@ def _construct_divmod_result(left, result, index, name, dtype): | |
) | ||
|
||
|
||
def _get_series_result_name(left, rvalues): | ||
# TODO: Can we just use right instead of rvalues? | ||
if isinstance(rvalues, ABCSeries): | ||
name = _maybe_match_name(left, rvalues) | ||
else: | ||
name = left.name | ||
return name | ||
|
||
|
||
def _arith_method_SERIES(op, name, str_rep, fill_zeros=None, default_axis=None, | ||
construct_result=_construct_result, **eval_kwargs): | ||
""" | ||
|
@@ -715,6 +726,29 @@ def wrapper(left, right, name=name, na_op=na_op): | |
if isinstance(right, ABCDataFrame): | ||
return NotImplemented | ||
|
||
if _is_offset(right): | ||
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 logic does not belong here. this is what happens in Timeop. 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. This is preventing this case from being dispatched to DatetimeIndex so that it continues to be handled by TimeOp for now. DatetimeIndex makes a mess of this case (try it...), but I decided to fix that separately. |
||
# special handling for alignment | ||
pass | ||
elif isinstance(right, pd.PeriodIndex): | ||
# not supported for DatetimeIndex | ||
pass | ||
elif is_datetime64_dtype(left) or is_datetime64tz_dtype(left): | ||
# Dispatch to DatetimeIndex method | ||
if right is pd.NaT: | ||
# DatetimeIndex and Series handle this differently, so | ||
# until that is resolved we need to special-case here | ||
return construct_result(left, pd.NaT, index=left.index, | ||
name=left.name, dtype=left.dtype) | ||
# TODO: double-check that the tz part of the dtype | ||
# is supposed to be retained | ||
left, right = _align_method_SERIES(left, right) | ||
name = _get_series_result_name(left, right) | ||
result = op(pd.DatetimeIndex(left), right) | ||
result.name = name # Needs to be overriden if name is None | ||
return construct_result(left, result, | ||
index=left.index, name=name, | ||
dtype=result.dtype) | ||
|
||
left, right = _align_method_SERIES(left, right) | ||
|
||
converted = _Op.get_op(left, right, name, na_op) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -107,7 +107,7 @@ def test_shift(self): | |
# incompat tz | ||
s2 = Series(date_range('2000-01-01 09:00:00', periods=5, | ||
tz='CET'), name='foo') | ||
pytest.raises(ValueError, lambda: s - s2) | ||
pytest.raises(TypeError, lambda: s - s2) | ||
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 an API change 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. as in needs to be noted in whatsnew or needs to be avoided? |
||
|
||
def test_shift2(self): | ||
ts = Series(np.random.randn(5), | ||
|
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.
huh? I believe we already check is_integer_dtype(other). this check is much too specific
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.
Under the status quo adding
np.array(1, dtype=np.int64)
to aDatetimeIndex
behaves like addingTimedelta(nanosecond=1)
.Series
raises (and aSeries
test fails unless this is caught here).I'm open to ideas for how to make this less hacky.
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.
An alternative would be to -- for the time being -- check for this case in the Series op and avoid dispatching to
DatetimeIndex
.