Skip to content

BUG: DTA/TDA/PA add/sub object-dtype #30594

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

Merged
merged 1 commit into from
Jan 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
is_integer_dtype,
is_list_like,
is_object_dtype,
is_offsetlike,
is_period_dtype,
is_string_dtype,
is_timedelta64_dtype,
Expand Down Expand Up @@ -1075,8 +1074,6 @@ def _sub_period_array(self, other):
f"cannot subtract {other.dtype}-dtype from {type(self).__name__}"
)

if len(self) != len(other):
raise ValueError("cannot subtract arrays/indices of unequal length")
if self.freq != other.freq:
msg = DIFFERENT_FREQ.format(
cls=type(self).__name__, own_freq=self.freqstr, other_freq=other.freqstr
Expand All @@ -1093,14 +1090,13 @@ def _sub_period_array(self, other):
new_values[mask] = NaT
return new_values

def _addsub_offset_array(self, other, op):
def _addsub_object_array(self, other: np.ndarray, op):
"""
Add or subtract array-like of DateOffset objects

Parameters
----------
other : Index, np.ndarray
object-dtype containing pd.DateOffset objects
other : np.ndarray[object]
op : {operator.add, operator.sub}

Returns
Expand All @@ -1124,7 +1120,12 @@ def _addsub_offset_array(self, other, op):
kwargs = {}
if not is_period_dtype(self):
kwargs["freq"] = "infer"
return self._from_sequence(res_values, **kwargs)
try:
res = type(self)._from_sequence(res_values, **kwargs)
except ValueError:
# e.g. we've passed a Timestamp to TimedeltaArray
res = res_values
return res

def _time_shift(self, periods, freq=None):
"""
Expand Down Expand Up @@ -1187,9 +1188,9 @@ def __add__(self, other):
elif is_timedelta64_dtype(other):
# TimedeltaIndex, ndarray[timedelta64]
result = self._add_delta(other)
elif is_offsetlike(other):
# Array/Index of DateOffset objects
result = self._addsub_offset_array(other, operator.add)
elif is_object_dtype(other):
# e.g. Array/Index of DateOffset objects
result = self._addsub_object_array(other, operator.add)
elif is_datetime64_dtype(other) or is_datetime64tz_dtype(other):
# DatetimeIndex, ndarray[datetime64]
return self._add_datetime_arraylike(other)
Expand Down Expand Up @@ -1242,9 +1243,9 @@ def __sub__(self, other):
elif is_timedelta64_dtype(other):
# TimedeltaIndex, ndarray[timedelta64]
result = self._add_delta(-other)
elif is_offsetlike(other):
# Array/Index of DateOffset objects
result = self._addsub_offset_array(other, operator.sub)
elif is_object_dtype(other):
# e.g. Array/Index of DateOffset objects
result = self._addsub_object_array(other, operator.sub)
elif is_datetime64_dtype(other) or is_datetime64tz_dtype(other):
# DatetimeIndex, ndarray[datetime64]
result = self._sub_datetime_arraylike(other)
Expand Down
6 changes: 3 additions & 3 deletions pandas/core/arrays/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,13 +510,13 @@ def _add_datetimelike_scalar(self, other):
dtype = DatetimeTZDtype(tz=other.tz) if other.tz else _NS_DTYPE
return DatetimeArray(result, dtype=dtype, freq=self.freq)

def _addsub_offset_array(self, other, op):
# Add or subtract Array-like of DateOffset objects
def _addsub_object_array(self, other, op):
# Add or subtract Array-like of objects
try:
# TimedeltaIndex can only operate with a subset of DateOffset
# subclasses. Incompatible classes will raise AttributeError,
# which we re-raise as TypeError
return super()._addsub_offset_array(other, op)
return super()._addsub_object_array(other, op)
except AttributeError:
raise TypeError(
f"Cannot add/subtract non-tick DateOffset to {type(self).__name__}"
Expand Down
32 changes: 0 additions & 32 deletions pandas/core/dtypes/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
)
from pandas.core.dtypes.generic import (
ABCCategorical,
ABCDateOffset,
ABCDatetimeIndex,
ABCIndexClass,
ABCPeriodArray,
Expand Down Expand Up @@ -368,37 +367,6 @@ def is_categorical(arr) -> bool:
return isinstance(arr, ABCCategorical) or is_categorical_dtype(arr)


def is_offsetlike(arr_or_obj) -> bool:
"""
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

Examples
--------
>>> is_offsetlike(pd.DateOffset(days=1))
True
>>> is_offsetlike('offset')
False
>>> is_offsetlike([pd.offsets.Minute(4), pd.offsets.MonthEnd()])
True
>>> is_offsetlike(np.array([pd.DateOffset(months=3), pd.Timestamp.now()]))
False
"""
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_datetime64_dtype(arr_or_dtype) -> bool:
"""
Check whether an array-like or dtype is of the datetime64 dtype.
Expand Down
26 changes: 26 additions & 0 deletions pandas/tests/arithmetic/test_datetime64.py
Original file line number Diff line number Diff line change
Expand Up @@ -2307,6 +2307,32 @@ def test_dti_addsub_offset_arraylike(
expected = tm.box_expected(expected, xbox)
tm.assert_equal(res, expected)

@pytest.mark.parametrize("other_box", [pd.Index, np.array])
def test_dti_addsub_object_arraylike(
self, tz_naive_fixture, box_with_array, other_box
):
tz = tz_naive_fixture

dti = pd.date_range("2017-01-01", periods=2, tz=tz)
dtarr = tm.box_expected(dti, box_with_array)
other = other_box([pd.offsets.MonthEnd(), pd.Timedelta(days=4)])
xbox = get_upcast_box(box_with_array, other)

expected = pd.DatetimeIndex(["2017-01-31", "2017-01-06"], tz=tz_naive_fixture)
expected = tm.box_expected(expected, xbox)

warn = None if box_with_array is pd.DataFrame else PerformanceWarning
with tm.assert_produces_warning(warn):
result = dtarr + other
tm.assert_equal(result, expected)

expected = pd.DatetimeIndex(["2016-12-31", "2016-12-29"], tz=tz_naive_fixture)
expected = tm.box_expected(expected, xbox)

with tm.assert_produces_warning(warn):
result = dtarr - other
tm.assert_equal(result, expected)


@pytest.mark.parametrize("years", [-1, 0, 1])
@pytest.mark.parametrize("months", [-2, 0, 2])
Expand Down
20 changes: 20 additions & 0 deletions pandas/tests/arithmetic/test_period.py
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,26 @@ def test_parr_add_sub_index(self):
expected = pi - pi
tm.assert_index_equal(result, expected)

def test_parr_add_sub_object_array(self):
pi = pd.period_range("2000-12-31", periods=3, freq="D")
parr = pi.array

other = np.array([pd.Timedelta(days=1), pd.offsets.Day(2), 3])

with tm.assert_produces_warning(PerformanceWarning):
result = parr + other

expected = pd.PeriodIndex(
["2001-01-01", "2001-01-03", "2001-01-05"], freq="D"
).array
tm.assert_equal(result, expected)

with tm.assert_produces_warning(PerformanceWarning):
result = parr - other

expected = pd.PeriodIndex(["2000-12-30"] * 3, freq="D").array
tm.assert_equal(result, expected)


class TestPeriodSeriesArithmetic:
def test_ops_series_timedelta(self):
Expand Down
34 changes: 34 additions & 0 deletions pandas/tests/arithmetic/test_timedelta64.py
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,40 @@ def test_td64arr_addsub_anchored_offset_arraylike(self, obox, box_with_array):
with tm.assert_produces_warning(PerformanceWarning):
anchored - tdi

# ------------------------------------------------------------------
# Unsorted

def test_td64arr_add_sub_object_array(self, box_with_array):
tdi = pd.timedelta_range("1 day", periods=3, freq="D")
tdarr = tm.box_expected(tdi, box_with_array)

other = np.array(
[pd.Timedelta(days=1), pd.offsets.Day(2), pd.Timestamp("2000-01-04")]
)

warn = PerformanceWarning if box_with_array is not pd.DataFrame else None
with tm.assert_produces_warning(warn):
result = tdarr + other

expected = pd.Index(
[pd.Timedelta(days=2), pd.Timedelta(days=4), pd.Timestamp("2000-01-07")]
)
expected = tm.box_expected(expected, box_with_array)
tm.assert_equal(result, expected)

with pytest.raises(TypeError):
with tm.assert_produces_warning(warn):
tdarr - other

with tm.assert_produces_warning(warn):
result = other - tdarr

expected = pd.Index(
[pd.Timedelta(0), pd.Timedelta(0), pd.Timestamp("2000-01-01")]
)
expected = tm.box_expected(expected, box_with_array)
tm.assert_equal(result, expected)


class TestTimedeltaArraylikeMulDivOps:
# Tests for timedelta64[ns]
Expand Down
12 changes: 0 additions & 12 deletions pandas/tests/dtypes/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,18 +625,6 @@ 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.Index([pd.DateOffset(second=1)]))

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(np.array([pd.DateOffset(), pd.Timestamp(0)]))


@pytest.mark.parametrize(
"input_param,result",
[
Expand Down