Skip to content
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

Fixed apply_index #35165

Merged
merged 12 commits into from
Jul 15, 2020
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,7 @@ Deprecations
- :meth:`DatetimeIndex.week` and `DatetimeIndex.weekofyear` are deprecated and will be removed in a future version, use :meth:`DatetimeIndex.isocalendar().week` instead (:issue:`33595`)
- :meth:`DatetimeArray.week` and `DatetimeArray.weekofyear` are deprecated and will be removed in a future version, use :meth:`DatetimeArray.isocalendar().week` instead (:issue:`33595`)
- :meth:`DateOffset.__call__` is deprecated and will be removed in a future version, use ``offset + other`` instead (:issue:`34171`)
- :meth:`~BusinessDay.apply_index` is deprecated and will be removed in a future version. Use ``offset + other`` instead (:issue:`34580`)
TomAugspurger marked this conversation as resolved.
Show resolved Hide resolved
- :meth:`DataFrame.tshift` and :meth:`Series.tshift` are deprecated and will be removed in a future version, use :meth:`DataFrame.shift` and :meth:`Series.shift` instead (:issue:`11631`)
- Indexing an :class:`Index` object with a float key is deprecated, and will
raise an ``IndexError`` in the future. You can manually convert to an integer key
Expand Down
82 changes: 77 additions & 5 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,45 @@ cdef bint _is_normalized(datetime dt):
return True


def apply_wrapper_core(func, self, other):
TomAugspurger marked this conversation as resolved.
Show resolved Hide resolved
result = func(self, other)
result = np.asarray(result)

if self.normalize:
result = normalize_i8_timestamps(result.view("i8"), None)

return result


def apply_index_wraps(func):
# Note: normally we would use `@functools.wraps(func)`, but this does
# not play nicely with cython class methods
def wrapper(self, other) -> np.ndarray:
def wrapper(self, other):
# other is a DatetimeArray
result = apply_wrapper_core(func, self, other)
result = type(other)(result)
warnings.warn("'Offset.apply_index(other)' is deprecated. "
"Use 'offset + other' instead.", FutureWarning)
return result

result = func(self, other)
result = np.asarray(result)
# do @functools.wraps(func) manually since it doesn't work on cdef funcs
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
try:
wrapper.__module__ = func.__module__
except AttributeError:
# AttributeError: 'method_descriptor' object has no
# attribute '__module__'
pass
TomAugspurger marked this conversation as resolved.
Show resolved Hide resolved
return wrapper

if self.normalize:
result = normalize_i8_timestamps(result.view("i8"), None)

def apply_array_wraps(func):
# Note: normally we would use `@functools.wraps(func)`, but this does
# not play nicely with cython class methods
def wrapper(self, other) -> np.ndarray:
# other is a DatetimeArray
result = apply_wrapper_core(func, self, other)
return result

# do @functools.wraps(func) manually since it doesn't work on cdef funcs
Expand Down Expand Up @@ -554,6 +582,10 @@ cdef class BaseOffset:
raises NotImplementedError for offsets without a
vectorized implementation.

.. deprecated:: 1.1.0

Use ``offset + dtindex`` instead.

Parameters
----------
index : DatetimeIndex
Expand All @@ -567,6 +599,13 @@ cdef class BaseOffset:
"does not have a vectorized implementation"
)

@apply_array_wraps
def _apply_array(self, dtindex):
raise NotImplementedError(
f"DateOffset subclass {type(self).__name__} "
"does not have a vectorized implementation"
)

def rollback(self, dt) -> datetime:
"""
Roll provided date backward to next offset only if not on offset.
Expand Down Expand Up @@ -1031,6 +1070,10 @@ cdef class RelativeDeltaOffset(BaseOffset):
-------
ndarray[datetime64[ns]]
"""
return self._apply_array(dtindex)

@apply_array_wraps
def _apply_array(self, dtindex):
dt64other = np.asarray(dtindex)
kwds = self.kwds
relativedelta_fast = {
Expand Down Expand Up @@ -1360,6 +1403,10 @@ cdef class BusinessDay(BusinessMixin):

@apply_index_wraps
def apply_index(self, dtindex):
return self._apply_array(dtindex)

@apply_array_wraps
def _apply_array(self, dtindex):
i8other = dtindex.view("i8")
return shift_bdays(i8other, self.n)

Expand Down Expand Up @@ -1843,6 +1890,10 @@ cdef class YearOffset(SingleConstructorOffset):

@apply_index_wraps
def apply_index(self, dtindex):
return self._apply_array(dtindex)

@apply_array_wraps
def _apply_array(self, dtindex):
shifted = shift_quarters(
dtindex.view("i8"), self.n, self.month, self._day_opt, modby=12
)
Expand Down Expand Up @@ -1996,6 +2047,10 @@ cdef class QuarterOffset(SingleConstructorOffset):

@apply_index_wraps
def apply_index(self, dtindex):
return self._apply_array(dtindex)

@apply_array_wraps
def _apply_array(self, dtindex):
shifted = shift_quarters(
dtindex.view("i8"), self.n, self.startingMonth, self._day_opt
)
Expand Down Expand Up @@ -2111,6 +2166,10 @@ cdef class MonthOffset(SingleConstructorOffset):

@apply_index_wraps
def apply_index(self, dtindex):
return self._apply_array(dtindex)

@apply_array_wraps
def _apply_array(self, dtindex):
shifted = shift_months(dtindex.view("i8"), self.n, self._day_opt)
return shifted

Expand Down Expand Up @@ -2248,6 +2307,12 @@ cdef class SemiMonthOffset(SingleConstructorOffset):
@cython.wraparound(False)
@cython.boundscheck(False)
def apply_index(self, dtindex):
return self._apply_array(dtindex)

@apply_array_wraps
@cython.wraparound(False)
@cython.boundscheck(False)
def _apply_array(self, dtindex):
cdef:
int64_t[:] i8other = dtindex.view("i8")
Py_ssize_t i, count = len(i8other)
Expand Down Expand Up @@ -2407,6 +2472,10 @@ cdef class Week(SingleConstructorOffset):

@apply_index_wraps
def apply_index(self, dtindex):
return self._apply_array(dtindex)

@apply_array_wraps
def _apply_array(self, dtindex):
if self.weekday is None:
td = timedelta(days=7 * self.n)
td64 = np.timedelta64(td, "ns")
Expand Down Expand Up @@ -3185,6 +3254,9 @@ cdef class CustomBusinessDay(BusinessDay):
def apply_index(self, dtindex):
raise NotImplementedError

def _apply_array(self, dtindex):
TomAugspurger marked this conversation as resolved.
Show resolved Hide resolved
raise NotImplementedError

def is_on_offset(self, dt: datetime) -> bool:
if self.normalize and not _is_normalized(dt):
return False
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/arrays/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ def _add_offset(self, offset):
values = self.tz_localize(None)
else:
values = self
result = offset.apply_index(values)
result = offset._apply_array(values)
result = DatetimeArray._simple_new(result)
result = result.tz_localize(self.tz)

Expand Down
7 changes: 6 additions & 1 deletion pandas/tests/tseries/offsets/test_offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3663,14 +3663,19 @@ def test_offset(self, case):

@pytest.mark.parametrize("case", offset_cases)
def test_apply_index(self, case):
# https://github.com/pandas-dev/pandas/issues/34580
offset, cases = case
s = DatetimeIndex(cases.keys())
exp = DatetimeIndex(cases.values())

with tm.assert_produces_warning(None):
# GH#22535 check that we don't get a FutureWarning from adding
# an integer array to PeriodIndex
result = offset + s
tm.assert_index_equal(result, exp)

exp = DatetimeIndex(cases.values())
with tm.assert_produces_warning(FutureWarning):
result = offset.apply_index(s)
tm.assert_index_equal(result, exp)

on_offset_cases = [
Expand Down