Skip to content

DEPR: DateOffset.apply #44522

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 5 commits into from
Nov 20, 2021
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
23 changes: 11 additions & 12 deletions doc/source/user_guide/timeseries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,7 @@ savings time. However, all :class:`DateOffset` subclasses that are an hour or sm

The basic :class:`DateOffset` acts similar to ``dateutil.relativedelta`` (`relativedelta documentation`_)
that shifts a date time by the corresponding calendar duration specified. The
arithmetic operator (``+``) or the ``apply`` method can be used to perform the shift.
arithmetic operator (``+``) can be used to perform the shift.

.. ipython:: python

Expand All @@ -866,7 +866,6 @@ arithmetic operator (``+``) or the ``apply`` method can be used to perform the s
friday.day_name()
# Add 2 business days (Friday --> Tuesday)
two_business_days = 2 * pd.offsets.BDay()
two_business_days.apply(friday)
friday + two_business_days
(friday + two_business_days).day_name()

Expand Down Expand Up @@ -938,14 +937,14 @@ in the operation).

ts = pd.Timestamp("2014-01-01 09:00")
day = pd.offsets.Day()
day.apply(ts)
day.apply(ts).normalize()
day + ts
(day + ts).normalize()

ts = pd.Timestamp("2014-01-01 22:00")
hour = pd.offsets.Hour()
hour.apply(ts)
hour.apply(ts).normalize()
hour.apply(pd.Timestamp("2014-01-01 23:30")).normalize()
hour + ts
(hour + ts).normalize()
(hour + pd.Timestamp("2014-01-01 23:30")).normalize()

.. _relativedelta documentation: https://dateutil.readthedocs.io/en/stable/relativedelta.html

Expand Down Expand Up @@ -1185,16 +1184,16 @@ under the default business hours (9:00 - 17:00), there is no gap (0 minutes) bet
pd.offsets.BusinessHour().rollback(pd.Timestamp("2014-08-02 15:00"))
pd.offsets.BusinessHour().rollforward(pd.Timestamp("2014-08-02 15:00"))

# It is the same as BusinessHour().apply(pd.Timestamp('2014-08-01 17:00')).
# And it is the same as BusinessHour().apply(pd.Timestamp('2014-08-04 09:00'))
pd.offsets.BusinessHour().apply(pd.Timestamp("2014-08-02 15:00"))
# It is the same as BusinessHour() + pd.Timestamp('2014-08-01 17:00').
# And it is the same as BusinessHour() + pd.Timestamp('2014-08-04 09:00')
pd.offsets.BusinessHour() + pd.Timestamp("2014-08-02 15:00")

# BusinessDay results (for reference)
pd.offsets.BusinessHour().rollforward(pd.Timestamp("2014-08-02"))

# It is the same as BusinessDay().apply(pd.Timestamp('2014-08-01'))
# It is the same as BusinessDay() + pd.Timestamp('2014-08-01')
# The result is the same as rollworward because BusinessDay never overlap.
pd.offsets.BusinessHour().apply(pd.Timestamp("2014-08-02"))
pd.offsets.BusinessHour() + pd.Timestamp("2014-08-02")

``BusinessHour`` regards Saturday and Sunday as holidays. To use arbitrary
holidays, you can use ``CustomBusinessHour`` offset, as explained in the
Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.4.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ Other Deprecations
- Deprecated casting behavior when passing an item with mismatched-timezone to :meth:`DatetimeIndex.insert`, :meth:`DatetimeIndex.putmask`, :meth:`DatetimeIndex.where` :meth:`DatetimeIndex.fillna`, :meth:`Series.mask`, :meth:`Series.where`, :meth:`Series.fillna`, :meth:`Series.shift`, :meth:`Series.replace`, :meth:`Series.reindex` (and :class:`DataFrame` column analogues). In the past this has cast to object dtype. In a future version, these will cast the passed item to the index or series's timezone (:issue:`37605`)
- Deprecated the 'errors' keyword argument in :meth:`Series.where`, :meth:`DataFrame.where`, :meth:`Series.mask`, and meth:`DataFrame.mask`; in a future version the argument will be removed (:issue:`44294`)
- Deprecated :meth:`PeriodIndex.astype` to ``datetime64[ns]`` or ``DatetimeTZDtype``, use ``obj.to_timestamp(how).tz_localize(dtype.tz)`` instead (:issue:`44398`)
- Deprecated :meth:`DateOffset.apply`, use ``offset + other`` instead (:issue:`44522`)
-

.. ---------------------------------------------------------------------------
Expand Down
46 changes: 28 additions & 18 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ cdef class BaseOffset:
# cython semantics; this is __radd__
return other.__add__(self)
try:
return self.apply(other)
return self._apply(other)
except ApplyTypeError:
return NotImplemented

Expand All @@ -458,7 +458,17 @@ cdef class BaseOffset:
FutureWarning,
stacklevel=1,
)
return self.apply(other)
return self._apply(other)

def apply(self, other):
# GH#44522
warnings.warn(
f"{type(self).__name__}.apply is deprecated and will be removed "
"in a future version. Use `offset + other` instead",
FutureWarning,
stacklevel=2,
)
return self._apply(other)

def __mul__(self, other):
if util.is_array(other):
Expand Down Expand Up @@ -889,7 +899,7 @@ cdef class Tick(SingleConstructorOffset):
else:
return delta_to_tick(self.delta + other.delta)
try:
return self.apply(other)
return self._apply(other)
except ApplyTypeError:
# Includes pd.Period
return NotImplemented
Expand All @@ -898,7 +908,7 @@ cdef class Tick(SingleConstructorOffset):
f"the add operation between {self} and {other} will overflow"
) from err

def apply(self, other):
def _apply(self, other):
# Timestamp can handle tz and nano sec, thus no need to use apply_wraps
if isinstance(other, _Timestamp):
# GH#15126
Expand Down Expand Up @@ -1041,7 +1051,7 @@ cdef class RelativeDeltaOffset(BaseOffset):
self.__dict__.update(state)

@apply_wraps
def apply(self, other: datetime) -> datetime:
def _apply(self, other: datetime) -> datetime:
if self._use_relativedelta:
other = _as_datetime(other)

Expand Down Expand Up @@ -1371,7 +1381,7 @@ cdef class BusinessDay(BusinessMixin):
return "+" + repr(self.offset)

@apply_wraps
def apply(self, other):
def _apply(self, other):
if PyDateTime_Check(other):
n = self.n
wday = other.weekday()
Expand Down Expand Up @@ -1684,7 +1694,7 @@ cdef class BusinessHour(BusinessMixin):
return dt

@apply_wraps
def apply(self, other: datetime) -> datetime:
def _apply(self, other: datetime) -> datetime:
# used for detecting edge condition
nanosecond = getattr(other, "nanosecond", 0)
# reset timezone and nanosecond
Expand Down Expand Up @@ -1833,7 +1843,7 @@ cdef class WeekOfMonthMixin(SingleConstructorOffset):
raise ValueError(f"Day must be 0<=day<=6, got {weekday}")

@apply_wraps
def apply(self, other: datetime) -> datetime:
def _apply(self, other: datetime) -> datetime:
compare_day = self._get_offset_day(other)

months = self.n
Expand Down Expand Up @@ -1913,7 +1923,7 @@ cdef class YearOffset(SingleConstructorOffset):
return get_day_of_month(&dts, self._day_opt)

@apply_wraps
def apply(self, other: datetime) -> datetime:
def _apply(self, other: datetime) -> datetime:
years = roll_qtrday(other, self.n, self.month, self._day_opt, modby=12)
months = years * 12 + (self.month - other.month)
return shift_month(other, months, self._day_opt)
Expand Down Expand Up @@ -2062,7 +2072,7 @@ cdef class QuarterOffset(SingleConstructorOffset):
return mod_month == 0 and dt.day == self._get_offset_day(dt)

@apply_wraps
def apply(self, other: datetime) -> datetime:
def _apply(self, other: datetime) -> datetime:
# months_since: find the calendar quarter containing other.month,
# e.g. if other.month == 8, the calendar quarter is [Jul, Aug, Sep].
# Then find the month in that quarter containing an is_on_offset date for
Expand Down Expand Up @@ -2189,7 +2199,7 @@ cdef class MonthOffset(SingleConstructorOffset):
return dt.day == self._get_offset_day(dt)

@apply_wraps
def apply(self, other: datetime) -> datetime:
def _apply(self, other: datetime) -> datetime:
compare_day = self._get_offset_day(other)
n = roll_convention(other.day, self.n, compare_day)
return shift_month(other, n, self._day_opt)
Expand Down Expand Up @@ -2307,7 +2317,7 @@ cdef class SemiMonthOffset(SingleConstructorOffset):
return self._prefix + suffix

@apply_wraps
def apply(self, other: datetime) -> datetime:
def _apply(self, other: datetime) -> datetime:
is_start = isinstance(self, SemiMonthBegin)

# shift `other` to self.day_of_month, incrementing `n` if necessary
Expand Down Expand Up @@ -2482,7 +2492,7 @@ cdef class Week(SingleConstructorOffset):
return self.n == 1 and self.weekday is not None

@apply_wraps
def apply(self, other):
def _apply(self, other):
if self.weekday is None:
return other + self.n * self._inc

Expand Down Expand Up @@ -2833,7 +2843,7 @@ cdef class FY5253(FY5253Mixin):
return year_end == dt

@apply_wraps
def apply(self, other: datetime) -> datetime:
def _apply(self, other: datetime) -> datetime:
norm = Timestamp(other).normalize()

n = self.n
Expand Down Expand Up @@ -3082,7 +3092,7 @@ cdef class FY5253Quarter(FY5253Mixin):
return start, num_qtrs, tdelta

@apply_wraps
def apply(self, other: datetime) -> datetime:
def _apply(self, other: datetime) -> datetime:
# Note: self.n == 0 is not allowed.

n = self.n
Expand Down Expand Up @@ -3173,7 +3183,7 @@ cdef class Easter(SingleConstructorOffset):
self.normalize = state.pop("normalize")

@apply_wraps
def apply(self, other: datetime) -> datetime:
def _apply(self, other: datetime) -> datetime:
current_easter = easter(other.year)
current_easter = datetime(
current_easter.year, current_easter.month, current_easter.day
Expand Down Expand Up @@ -3252,7 +3262,7 @@ cdef class CustomBusinessDay(BusinessDay):
BusinessDay.__setstate__(self, state)

@apply_wraps
def apply(self, other):
def _apply(self, other):
if self.n <= 0:
roll = "forward"
else:
Expand Down Expand Up @@ -3415,7 +3425,7 @@ cdef class _CustomBusinessMonth(BusinessMixin):
return roll_func

@apply_wraps
def apply(self, other: datetime) -> datetime:
def _apply(self, other: datetime) -> datetime:
# First move to month offset
cur_month_offset_date = self.month_roll(other)

Expand Down
4 changes: 2 additions & 2 deletions pandas/core/arrays/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2566,7 +2566,7 @@ def generate_range(start=None, end=None, periods=None, offset=BDay()):
break

# faster than cur + offset
next_date = offset.apply(cur)
next_date = offset._apply(cur)
if next_date <= cur:
raise ValueError(f"Offset {offset} did not increment date")
cur = next_date
Expand All @@ -2580,7 +2580,7 @@ def generate_range(start=None, end=None, periods=None, offset=BDay()):
break

# faster than cur + offset
next_date = offset.apply(cur)
next_date = offset._apply(cur)
if next_date >= cur:
raise ValueError(f"Offset {offset} did not decrement date")
cur = next_date
4 changes: 2 additions & 2 deletions pandas/tests/tseries/offsets/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
def assert_offset_equal(offset, base, expected):
actual = offset + base
actual_swapped = base + offset
actual_apply = offset.apply(base)
actual_apply = offset._apply(base)
try:
assert actual == expected
assert actual_swapped == expected
Expand Down Expand Up @@ -155,7 +155,7 @@ def test_rsub(self):
# i.e. skip for TestCommon and YQM subclasses that do not have
# offset2 attr
return
assert self.d - self.offset2 == (-self.offset2).apply(self.d)
assert self.d - self.offset2 == (-self.offset2)._apply(self.d)

def test_radd(self):
if self._offset is None or not hasattr(self, "offset2"):
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/tseries/offsets/test_business_day.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,4 @@ def test_apply_corner(self):
"with datetime, datetime64 or timedelta"
)
with pytest.raises(ApplyTypeError, match=msg):
self._offset().apply(BMonthEnd())
self._offset()._apply(BMonthEnd())
2 changes: 1 addition & 1 deletion pandas/tests/tseries/offsets/test_business_hour.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ def test_roll_date_object(self):
def test_normalize(self, case):
offset, cases = case
for dt, expected in cases.items():
assert offset.apply(dt) == expected
assert offset._apply(dt) == expected

on_offset_cases = []
on_offset_cases.append(
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/tseries/offsets/test_custom_business_hour.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def test_roll_date_object(self):
def test_normalize(self, norm_cases):
offset, cases = norm_cases
for dt, expected in cases.items():
assert offset.apply(dt) == expected
assert offset._apply(dt) == expected

def test_is_on_offset(self):
tests = [
Expand Down
6 changes: 3 additions & 3 deletions pandas/tests/tseries/offsets/test_fiscal.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,18 +643,18 @@ def test_bunched_yearends():
fy = FY5253(n=1, weekday=5, startingMonth=12, variation="nearest")
dt = Timestamp("2004-01-01")
assert fy.rollback(dt) == Timestamp("2002-12-28")
assert (-fy).apply(dt) == Timestamp("2002-12-28")
assert (-fy)._apply(dt) == Timestamp("2002-12-28")
assert dt - fy == Timestamp("2002-12-28")

assert fy.rollforward(dt) == Timestamp("2004-01-03")
assert fy.apply(dt) == Timestamp("2004-01-03")
assert fy._apply(dt) == Timestamp("2004-01-03")
assert fy + dt == Timestamp("2004-01-03")
assert dt + fy == Timestamp("2004-01-03")

# Same thing, but starting from a Timestamp in the previous year.
dt = Timestamp("2003-12-31")
assert fy.rollback(dt) == Timestamp("2002-12-28")
assert (-fy).apply(dt) == Timestamp("2002-12-28")
assert (-fy)._apply(dt) == Timestamp("2002-12-28")
assert dt - fy == Timestamp("2002-12-28")


Expand Down
26 changes: 19 additions & 7 deletions pandas/tests/tseries/offsets/test_offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def test_return_type(self, offset_types):
assert offset + NaT is NaT

assert NaT - offset is NaT
assert (-offset).apply(NaT) is NaT
assert (-offset)._apply(NaT) is NaT

def test_offset_n(self, offset_types):
offset = self._get_offset(offset_types)
Expand Down Expand Up @@ -188,14 +188,25 @@ def _check_offsetfunc_works(self, offset, funcname, dt, expected, normalize=Fals

if (
type(offset_s).__name__ == "DateOffset"
and (funcname == "apply" or normalize)
and (funcname in ["apply", "_apply"] or normalize)
and ts.nanosecond > 0
):
exp_warning = UserWarning

# test nanosecond is preserved
with tm.assert_produces_warning(exp_warning):
result = func(ts)

if exp_warning is None and funcname == "_apply":
# GH#44522
# Check in this particular case to avoid headaches with
# testing for multiple warnings produced by the same call.
with tm.assert_produces_warning(FutureWarning, match="apply is deprecated"):
res2 = offset_s.apply(ts)

assert type(res2) is type(result)
assert res2 == result

assert isinstance(result, Timestamp)
if normalize is False:
assert result == expected + Nano(5)
Expand Down Expand Up @@ -225,7 +236,7 @@ def _check_offsetfunc_works(self, offset, funcname, dt, expected, normalize=Fals

if (
type(offset_s).__name__ == "DateOffset"
and (funcname == "apply" or normalize)
and (funcname in ["apply", "_apply"] or normalize)
and ts.nanosecond > 0
):
exp_warning = UserWarning
Expand All @@ -243,13 +254,14 @@ def test_apply(self, offset_types):
sdt = datetime(2011, 1, 1, 9, 0)
ndt = np_datetime64_compat("2011-01-01 09:00Z")

expected = self.expecteds[offset_types.__name__]
expected_norm = Timestamp(expected.date())

for dt in [sdt, ndt]:
expected = self.expecteds[offset_types.__name__]
self._check_offsetfunc_works(offset_types, "apply", dt, expected)
self._check_offsetfunc_works(offset_types, "_apply", dt, expected)

expected = Timestamp(expected.date())
self._check_offsetfunc_works(
offset_types, "apply", dt, expected, normalize=True
offset_types, "_apply", dt, expected_norm, normalize=True
)

def test_rollforward(self, offset_types):
Expand Down
Loading