Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

DEPR: the method is_anchored() for offsets #56594

Merged
merged 7 commits into from
Jan 10, 2024
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
2 changes: 2 additions & 0 deletions doc/source/whatsnew/v2.2.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -587,11 +587,13 @@ Other Deprecations
- Deprecated :func:`pd.core.internals.api.make_block`, use public APIs instead (:issue:`40226`)
- Deprecated :func:`read_gbq` and :meth:`DataFrame.to_gbq`. Use ``pandas_gbq.read_gbq`` and ``pandas_gbq.to_gbq`` instead https://pandas-gbq.readthedocs.io/en/latest/api.html (:issue:`55525`)
- Deprecated :meth:`.DataFrameGroupBy.fillna` and :meth:`.SeriesGroupBy.fillna`; use :meth:`.DataFrameGroupBy.ffill`, :meth:`.DataFrameGroupBy.bfill` for forward and backward filling or :meth:`.DataFrame.fillna` to fill with a single value (or the Series equivalents) (:issue:`55718`)
- Deprecated :meth:`DateOffset.is_anchored`, use ``obj.n == 1`` for non-Tick subclasses (for Tick this was always False) (:issue:`55388`)
- Deprecated :meth:`DatetimeArray.__init__` and :meth:`TimedeltaArray.__init__`, use :func:`array` instead (:issue:`55623`)
- Deprecated :meth:`Index.format`, use ``index.astype(str)`` or ``index.map(formatter)`` instead (:issue:`55413`)
- Deprecated :meth:`Series.ravel`, the underlying array is already 1D, so ravel is not necessary (:issue:`52511`)
- Deprecated :meth:`Series.resample` and :meth:`DataFrame.resample` with a :class:`PeriodIndex` (and the 'convention' keyword), convert to :class:`DatetimeIndex` (with ``.to_timestamp()``) before resampling instead (:issue:`53481`)
- Deprecated :meth:`Series.view`, use :meth:`Series.astype` instead to change the dtype (:issue:`20251`)
- Deprecated :meth:`offsets.Tick.is_anchored`, use ``False`` instead (:issue:`55388`)
- Deprecated ``core.internals`` members ``Block``, ``ExtensionBlock``, and ``DatetimeTZBlock``, use public APIs instead (:issue:`55139`)
- Deprecated ``year``, ``month``, ``quarter``, ``day``, ``hour``, ``minute``, and ``second`` keywords in the :class:`PeriodIndex` constructor, use :meth:`PeriodIndex.from_fields` instead (:issue:`55960`)
- Deprecated accepting a type as an argument in :meth:`Index.view`, call without any arguments instead (:issue:`55709`)
Expand Down
54 changes: 52 additions & 2 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -756,18 +756,27 @@ cdef class BaseOffset:
raise ValueError(f"{self} is a non-fixed frequency")

def is_anchored(self) -> bool:
# TODO: Does this make sense for the general case? It would help
# if there were a canonical docstring for what is_anchored means.
# GH#55388
"""
Return boolean whether the frequency is a unit frequency (n=1).

.. deprecated:: 2.2.0
is_anchored is deprecated and will be removed in a future version.
Use ``obj.n == 1`` instead.

Examples
--------
>>> pd.DateOffset().is_anchored()
True
>>> pd.DateOffset(2).is_anchored()
False
"""
warnings.warn(
f"{type(self).__name__}.is_anchored is deprecated and will be removed "
f"in a future version, please use \'obj.n == 1\' instead.",
FutureWarning,
stacklevel=find_stack_level(),
)
return self.n == 1

# ------------------------------------------------------------------
Expand Down Expand Up @@ -954,6 +963,27 @@ cdef class Tick(SingleConstructorOffset):
return True

def is_anchored(self) -> bool:
# GH#55388
"""
Return False.

.. deprecated:: 2.2.0
is_anchored is deprecated and will be removed in a future version.
Use ``False`` instead.

Examples
--------
>>> pd.offsets.Hour().is_anchored()
False
>>> pd.offsets.Hour(2).is_anchored()
False
"""
warnings.warn(
f"{type(self).__name__}.is_anchored is deprecated and will be removed "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this deprecation, how can I check for the equivalent attribute here? Use False is not really helpful

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for this class it's always False

for others you typically need to check that obj.n==1 and obj.weekday is None (if it has a weekday attribute) and obj.startingMonth is None (if it has a startingMonth attribute)

what are you using it for?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are generally looking for freqs that aren’t anchored

I thought a little bit about this and the deprecations isn’t great, it makes those checks significantly more complex than before for users

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that we can advise users use False, because is_anchored always returns False for Tick subclasses.
Do you think it would be better to use some expression that is always False instead of False, like pd.offsets.Hour().n == None?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mostly don’t want to have different check for every group, that’s what makes this ugly

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That helps, thx

We have to make a decision about shifting divisions, this basically goes back how the offset behaves on different values, e.g. if it can shift different values to the same target (if it's anchored for example) then we have to make a different decision compared to if every value maps to a distinct target value

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.g. if it can shift different values to the same target (if it's anchored for example)

An example might help here. I think what you're referring to is what past-me thought is_anchored meant (xref #44025), e.g "W-SUN" would be anchored, but "W" would not (it would act like "7D"). pd.DateOffset(day=1) would, but pd.DateOffset(days=1) would not.

NB: the "is equivalent to" i gave about isn't quite right.

Copy link
Member

@phofl phofl Jan 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idx = pd.Index([pd.Timestamp("2020-01-01"), pd.Timestamp("2020-01-02")])
print(idx.shift(1, freq=pd.tseries.offsets.Week(1)))
print(idx.shift(1, freq=pd.tseries.offsets.Week(1, weekday=2)))
DatetimeIndex(['2020-01-08', '2020-01-09'], dtype='datetime64[ns]', freq=None)
DatetimeIndex(['2020-01-08', '2020-01-08'], dtype='datetime64[ns]', freq=None)

The first one just shifts 7 days, so everything is fine

The second one shifts to a specific weekday though, which is an issue for us, because it maps 2 different days to the same target

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That matches what I think a more-useful is_anchored would mean. My guess is we can come up with examples (e.g. the day=1 vs days=1 above) where checking is_anchored would be wrong for your use case.

Copy link
Member

@MarcoGorelli MarcoGorelli Jan 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idx = pd.Index([pd.Timestamp("2020-01-01"), pd.Timestamp("2020-01-02")])
print(idx.shift(1, freq=pd.tseries.offsets.Week(2)))
print(idx.shift(1, freq=pd.tseries.offsets.Week(2, weekday=2)))
DatetimeIndex(['2020-01-15', '2020-01-16'], dtype='datetime64[ns]', freq=None)
DatetimeIndex(['2020-01-15', '2020-01-15'], dtype='datetime64[ns]', freq=None)

The first one just shifts 14 days, and the second one shifts to a specific weekday

Yet neither "is_anchored":

In [21]: pd.tseries.offsets.Week(2).is_anchored()
Out[21]: False

In [22]: pd.tseries.offsets.Week(2, weekday=2).is_anchored()
Out[22]: False

f"in a future version, please use False instead.",
FutureWarning,
stacklevel=find_stack_level(),
)
return False

# This is identical to BaseOffset.__hash__, but has to be redefined here
Expand Down Expand Up @@ -2663,6 +2693,13 @@ cdef class QuarterOffset(SingleConstructorOffset):
return f"{self._prefix}-{month}"

def is_anchored(self) -> bool:
warnings.warn(
f"{type(self).__name__}.is_anchored is deprecated and will be removed "
f"in a future version, please use \'obj.n == 1 "
f"and obj.startingMonth is not None\' instead.",
FutureWarning,
stacklevel=find_stack_level(),
)
return self.n == 1 and self.startingMonth is not None

def is_on_offset(self, dt: datetime) -> bool:
Expand Down Expand Up @@ -3308,6 +3345,13 @@ cdef class Week(SingleConstructorOffset):
self._cache = state.pop("_cache", {})

def is_anchored(self) -> bool:
warnings.warn(
f"{type(self).__name__}.is_anchored is deprecated and will be removed "
f"in a future version, please use \'obj.n == 1 "
f"and obj.weekday is not None\' instead.",
FutureWarning,
stacklevel=find_stack_level(),
)
return self.n == 1 and self.weekday is not None

@apply_wraps
Expand Down Expand Up @@ -3597,6 +3641,12 @@ cdef class FY5253Mixin(SingleConstructorOffset):
self.variation = state.pop("variation")

def is_anchored(self) -> bool:
warnings.warn(
f"{type(self).__name__}.is_anchored is deprecated and will be removed "
f"in a future version, please use \'obj.n == 1\' instead.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we not want to say about self.startingMonth is not None and self.weekday is not None in this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about this. I think, there is no need to check if parameters startingMonth and weekday are not None. For subclasses of the class FY5253Mixin we initialize these parameters in constructor and set them 0 and 1 accordingly.

def __init__(
self, n=1, normalize=False, weekday=0, startingMonth=1, variation="nearest"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, thanks, they can't actually be None, so the is_anchored definition was too complex to begin with

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we check:

>>> pd.offsets.FY5253().is_anchored()
FutureWarning: FY5253.is_anchored is deprecated and will be removed in a future version, please use 'obj.n == 1' instead.
True
>>> pd.offsets.FY5253(n=1, startingMonth=1, weekday=1).is_anchored()
True

On the other hand for the class Week we have

>>> pd.offsets.Week(1).is_anchored()
FutureWarning: Week.is_anchored is deprecated and will be removed in a future version, please use 'obj.n == 1 and obj.weekday is not None' instead.
False
>>> pd.offsets.Week(n=1, weekday=0).is_anchored()
True

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but also, trying to pass None just raises

In [12]: FY5253(n=1, weekday=1, startingMonth=1).is_anchored()
<ipython-input-12-37644e23496a>:1: FutureWarning: FY5253.is_anchored is deprecated and will be removed in a future version, please use 'obj.n == 1' instead.
  FY5253(n=1, weekday=1, startingMonth=1).is_anchored()
Out[12]: True

In [13]: FY5253(n=1, weekday=None, startingMonth=None).is_anchored()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[13], line 1
----> 1 FY5253(n=1, weekday=None, startingMonth=None).is_anchored()

File offsets.pyx:3627, in pandas._libs.tslibs.offsets.FY5253Mixin.__init__()

TypeError: an integer is required

In [14]: FY5253(n=1, weekday=None, startingMonth=1).is_anchored()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[14], line 1
----> 1 FY5253(n=1, weekday=None, startingMonth=1).is_anchored()

File offsets.pyx:3628, in pandas._libs.tslibs.offsets.FY5253Mixin.__init__()

TypeError: an integer is required

In [15]: FY5253(n=1, weekday=1, startingMonth=None).is_anchored()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[15], line 1
----> 1 FY5253(n=1, weekday=1, startingMonth=None).is_anchored()

File offsets.pyx:3627, in pandas._libs.tslibs.offsets.FY5253Mixin.__init__()

TypeError: an integer is required

FutureWarning,
stacklevel=find_stack_level(),
)
return (
self.n == 1 and self.startingMonth is not None and self.weekday is not None
)
Expand Down
4 changes: 1 addition & 3 deletions pandas/tests/indexes/interval/test_interval_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,7 @@ def test_constructor_timestamp(self, closed, name, freq, periods, tz):
tm.assert_index_equal(result, expected)

# GH 20976: linspace behavior defined from start/end/periods
if not breaks.freq.is_anchored() and tz is None:
# matches expected only for non-anchored offsets and tz naive
# (anchored/DST transitions cause unequal spacing in expected)
if not breaks.freq.n == 1 and tz is None:
result = interval_range(
start=start, end=end, periods=periods, name=name, closed=closed
)
Expand Down
19 changes: 13 additions & 6 deletions pandas/tests/tseries/offsets/test_business_quarter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import pytest

import pandas._testing as tm
from pandas.tests.tseries.offsets.common import (
assert_is_on_offset,
assert_offset_equal,
Expand Down Expand Up @@ -54,9 +55,12 @@ def test_repr(self):
assert repr(BQuarterBegin(startingMonth=1)) == expected

def test_is_anchored(self):
assert BQuarterBegin(startingMonth=1).is_anchored()
assert BQuarterBegin().is_anchored()
assert not BQuarterBegin(2, startingMonth=1).is_anchored()
msg = "BQuarterBegin.is_anchored is deprecated "

with tm.assert_produces_warning(FutureWarning, match=msg):
assert BQuarterBegin(startingMonth=1).is_anchored()
assert BQuarterBegin().is_anchored()
assert not BQuarterBegin(2, startingMonth=1).is_anchored()

def test_offset_corner_case(self):
# corner
Expand Down Expand Up @@ -177,9 +181,12 @@ def test_repr(self):
assert repr(BQuarterEnd(startingMonth=1)) == expected

def test_is_anchored(self):
assert BQuarterEnd(startingMonth=1).is_anchored()
assert BQuarterEnd().is_anchored()
assert not BQuarterEnd(2, startingMonth=1).is_anchored()
msg = "BQuarterEnd.is_anchored is deprecated "

with tm.assert_produces_warning(FutureWarning, match=msg):
assert BQuarterEnd(startingMonth=1).is_anchored()
assert BQuarterEnd().is_anchored()
assert not BQuarterEnd(2, startingMonth=1).is_anchored()

def test_offset_corner_case(self):
# corner
Expand Down
22 changes: 13 additions & 9 deletions pandas/tests/tseries/offsets/test_fiscal.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest

from pandas import Timestamp
import pandas._testing as tm
from pandas.tests.tseries.offsets.common import (
WeekDay,
assert_is_on_offset,
Expand Down Expand Up @@ -295,15 +296,18 @@ def test_apply(self):

class TestFY5253LastOfMonthQuarter:
def test_is_anchored(self):
assert makeFY5253LastOfMonthQuarter(
startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4
).is_anchored()
assert makeFY5253LastOfMonthQuarter(
weekday=WeekDay.SAT, startingMonth=3, qtr_with_extra_week=4
).is_anchored()
assert not makeFY5253LastOfMonthQuarter(
2, startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4
).is_anchored()
msg = "FY5253Quarter.is_anchored is deprecated "

with tm.assert_produces_warning(FutureWarning, match=msg):
assert makeFY5253LastOfMonthQuarter(
startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4
).is_anchored()
assert makeFY5253LastOfMonthQuarter(
weekday=WeekDay.SAT, startingMonth=3, qtr_with_extra_week=4
).is_anchored()
assert not makeFY5253LastOfMonthQuarter(
2, startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4
).is_anchored()

def test_equality(self):
assert makeFY5253LastOfMonthQuarter(
Expand Down
7 changes: 5 additions & 2 deletions pandas/tests/tseries/offsets/test_offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,8 +625,11 @@ def test_default_constructor(self, dt):
assert (dt + DateOffset(2)) == datetime(2008, 1, 4)

def test_is_anchored(self):
assert not DateOffset(2).is_anchored()
assert DateOffset(1).is_anchored()
msg = "DateOffset.is_anchored is deprecated "

with tm.assert_produces_warning(FutureWarning, match=msg):
assert not DateOffset(2).is_anchored()
assert DateOffset(1).is_anchored()

def test_copy(self):
assert DateOffset(months=2).copy() == DateOffset(months=2)
Expand Down
19 changes: 13 additions & 6 deletions pandas/tests/tseries/offsets/test_quarter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import pytest

import pandas._testing as tm
from pandas.tests.tseries.offsets.common import (
assert_is_on_offset,
assert_offset_equal,
Expand Down Expand Up @@ -53,9 +54,12 @@ def test_repr(self):
assert repr(QuarterBegin(startingMonth=1)) == expected

def test_is_anchored(self):
assert QuarterBegin(startingMonth=1).is_anchored()
assert QuarterBegin().is_anchored()
assert not QuarterBegin(2, startingMonth=1).is_anchored()
msg = "QuarterBegin.is_anchored is deprecated "

with tm.assert_produces_warning(FutureWarning, match=msg):
assert QuarterBegin(startingMonth=1).is_anchored()
assert QuarterBegin().is_anchored()
assert not QuarterBegin(2, startingMonth=1).is_anchored()

def test_offset_corner_case(self):
# corner
Expand Down Expand Up @@ -161,9 +165,12 @@ def test_repr(self):
assert repr(QuarterEnd(startingMonth=1)) == expected

def test_is_anchored(self):
assert QuarterEnd(startingMonth=1).is_anchored()
assert QuarterEnd().is_anchored()
assert not QuarterEnd(2, startingMonth=1).is_anchored()
msg = "QuarterEnd.is_anchored is deprecated "

with tm.assert_produces_warning(FutureWarning, match=msg):
assert QuarterEnd(startingMonth=1).is_anchored()
assert QuarterEnd().is_anchored()
assert not QuarterEnd(2, startingMonth=1).is_anchored()

def test_offset_corner_case(self):
# corner
Expand Down
5 changes: 4 additions & 1 deletion pandas/tests/tseries/offsets/test_ticks.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,10 @@ def test_tick_equalities(cls):

@pytest.mark.parametrize("cls", tick_classes)
def test_tick_offset(cls):
assert not cls().is_anchored()
msg = f"{cls.__name__}.is_anchored is deprecated "

with tm.assert_produces_warning(FutureWarning, match=msg):
assert not cls().is_anchored()


@pytest.mark.parametrize("cls", tick_classes)
Expand Down
12 changes: 8 additions & 4 deletions pandas/tests/tseries/offsets/test_week.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
WeekOfMonth,
)

import pandas._testing as tm
from pandas.tests.tseries.offsets.common import (
WeekDay,
assert_is_on_offset,
Expand All @@ -42,10 +43,13 @@ def test_corner(self):
Week(weekday=-1)

def test_is_anchored(self):
assert Week(weekday=0).is_anchored()
assert not Week().is_anchored()
assert not Week(2, weekday=2).is_anchored()
assert not Week(2).is_anchored()
msg = "Week.is_anchored is deprecated "

with tm.assert_produces_warning(FutureWarning, match=msg):
assert Week(weekday=0).is_anchored()
assert not Week().is_anchored()
assert not Week(2, weekday=2).is_anchored()
assert not Week(2).is_anchored()

offset_cases = []
# not business week
Expand Down
Loading