Skip to content

Commit

Permalink
CLN: enforce deprecation of frequencies deprecated for offsets (#57986)
Browse files Browse the repository at this point in the history
* enforce deprecation of offset deprecated freqstr

* fix tests

* fix mypy error

* add a note to v3.0.0

* remove c_REVERSE_OFFSET_REMOVED_FREQSTR, correct tests

* fix test_to_period_offsets_not_supported

* correct to_offset, fix tests

* add dict PERIOD_TO_OFFSET_FREQSTR, corect meth to_offset, fix tests

* add a comment

* create dictionary PERIOD_AND_OFFSET_ALIASES

* correct def to_offset

* fixup

* fixup

* replace c_OFFSET_RENAMED_FREQSTR with c_PERIOD_TO_OFFSET_FREQSTR

* add cimport c_PERIOD_TO_OFFSET_FREQSTR

* add a helper function for error reporting

* minor whatsnew fix

---------

Co-authored-by: Marco Edward Gorelli <[email protected]>
Co-authored-by: Marco Gorelli <[email protected]>
  • Loading branch information
3 people authored Jun 7, 2024
1 parent ee127fc commit c95716a
Show file tree
Hide file tree
Showing 17 changed files with 274 additions and 301 deletions.
30 changes: 29 additions & 1 deletion doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,34 @@ Other Deprecations

Removal of prior version deprecations/changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Enforced deprecation of aliases ``M``, ``Q``, ``Y``, etc. in favour of ``ME``, ``QE``, ``YE``, etc. for offsets
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Renamed the following offset aliases (:issue:`57986`):

+-------------------------------+------------------+------------------+
| offset | removed alias | new alias |
+===============================+==================+==================+
|:class:`MonthEnd` | ``M`` | ``ME`` |
+-------------------------------+------------------+------------------+
|:class:`BusinessMonthEnd` | ``BM`` | ``BME`` |
+-------------------------------+------------------+------------------+
|:class:`SemiMonthEnd` | ``SM`` | ``SME`` |
+-------------------------------+------------------+------------------+
|:class:`CustomBusinessMonthEnd`| ``CBM`` | ``CBME`` |
+-------------------------------+------------------+------------------+
|:class:`QuarterEnd` | ``Q`` | ``QE`` |
+-------------------------------+------------------+------------------+
|:class:`BQuarterEnd` | ``BQ`` | ``BQE`` |
+-------------------------------+------------------+------------------+
|:class:`YearEnd` | ``Y`` | ``YE`` |
+-------------------------------+------------------+------------------+
|:class:`BYearEnd` | ``BY`` | ``BYE`` |
+-------------------------------+------------------+------------------+

Other Removals
^^^^^^^^^^^^^^
- :class:`.DataFrameGroupBy.idxmin`, :class:`.DataFrameGroupBy.idxmax`, :class:`.SeriesGroupBy.idxmin`, and :class:`.SeriesGroupBy.idxmax` will now raise a ``ValueError`` when used with ``skipna=False`` and an NA value is encountered (:issue:`10694`)
- :func:`concat` no longer ignores empty objects when determining output dtypes (:issue:`39122`)
- :func:`concat` with all-NA entries no longer ignores the dtype of those entries when determining the result dtype (:issue:`40893`)
Expand Down Expand Up @@ -343,7 +371,7 @@ Removal of prior version deprecations/changes
- Enforced deprecation of string ``A`` denoting frequency in :class:`YearEnd` and strings ``A-DEC``, ``A-JAN``, etc. denoting annual frequencies with various fiscal year ends (:issue:`57699`)
- Enforced deprecation of string ``BAS`` denoting frequency in :class:`BYearBegin` and strings ``BAS-DEC``, ``BAS-JAN``, etc. denoting annual frequencies with various fiscal year starts (:issue:`57793`)
- Enforced deprecation of string ``BA`` denoting frequency in :class:`BYearEnd` and strings ``BA-DEC``, ``BA-JAN``, etc. denoting annual frequencies with various fiscal year ends (:issue:`57793`)
- Enforced deprecation of strings ``T``, ``L``, ``U``, and ``N`` denoting frequencies in :class:`Minute`, :class:`Second`, :class:`Milli`, :class:`Micro`, :class:`Nano` (:issue:`57627`)
- Enforced deprecation of strings ``T``, ``L``, ``U``, and ``N`` denoting frequencies in :class:`Minute`, :class:`Milli`, :class:`Micro`, :class:`Nano` (:issue:`57627`)
- Enforced deprecation of strings ``T``, ``L``, ``U``, and ``N`` denoting units in :class:`Timedelta` (:issue:`57627`)
- Enforced deprecation of the behavior of :func:`concat` when ``len(keys) != len(objs)`` would truncate to the shorter of the two. Now this raises a ``ValueError`` (:issue:`43485`)
- Enforced deprecation of values "pad", "ffill", "bfill", and "backfill" for :meth:`Series.interpolate` and :meth:`DataFrame.interpolate` (:issue:`57869`)
Expand Down
5 changes: 3 additions & 2 deletions pandas/_libs/tslibs/dtypes.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ cdef NPY_DATETIMEUNIT get_supported_reso(NPY_DATETIMEUNIT reso)
cdef bint is_supported_unit(NPY_DATETIMEUNIT reso)

cdef dict c_OFFSET_TO_PERIOD_FREQSTR
cdef dict c_OFFSET_DEPR_FREQSTR
cdef dict c_REVERSE_OFFSET_DEPR_FREQSTR
cdef dict c_PERIOD_TO_OFFSET_FREQSTR
cdef dict c_OFFSET_RENAMED_FREQSTR
cdef dict c_DEPR_ABBREVS
cdef dict c_PERIOD_AND_OFFSET_DEPR_FREQSTR
cdef dict attrname_to_abbrevs
cdef dict npy_unit_to_attrname
cdef dict attrname_to_npy_unit
Expand Down
45 changes: 40 additions & 5 deletions pandas/_libs/tslibs/dtypes.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ OFFSET_TO_PERIOD_FREQSTR: dict = {
"EOM": "M",
"BME": "M",
"SME": "M",
"BMS": "M",
"CBME": "M",
"CBMS": "M",
"SMS": "M",
"BQS": "Q",
"QS": "Q",
"BQE": "Q",
Expand Down Expand Up @@ -228,7 +232,6 @@ OFFSET_TO_PERIOD_FREQSTR: dict = {
"YE-NOV": "Y-NOV",
"W": "W",
"ME": "M",
"Y": "Y",
"BYE": "Y",
"BYE-DEC": "Y-DEC",
"BYE-JAN": "Y-JAN",
Expand All @@ -245,7 +248,7 @@ OFFSET_TO_PERIOD_FREQSTR: dict = {
"YS": "Y",
"BYS": "Y",
}
cdef dict c_OFFSET_DEPR_FREQSTR = {
cdef dict c_OFFSET_RENAMED_FREQSTR = {
"M": "ME",
"Q": "QE",
"Q-DEC": "QE-DEC",
Expand Down Expand Up @@ -303,10 +306,37 @@ cdef dict c_OFFSET_DEPR_FREQSTR = {
"BQ-OCT": "BQE-OCT",
"BQ-NOV": "BQE-NOV",
}
cdef dict c_OFFSET_TO_PERIOD_FREQSTR = OFFSET_TO_PERIOD_FREQSTR
cdef dict c_REVERSE_OFFSET_DEPR_FREQSTR = {
v: k for k, v in c_OFFSET_DEPR_FREQSTR.items()
PERIOD_TO_OFFSET_FREQSTR = {
"M": "ME",
"Q": "QE",
"Q-DEC": "QE-DEC",
"Q-JAN": "QE-JAN",
"Q-FEB": "QE-FEB",
"Q-MAR": "QE-MAR",
"Q-APR": "QE-APR",
"Q-MAY": "QE-MAY",
"Q-JUN": "QE-JUN",
"Q-JUL": "QE-JUL",
"Q-AUG": "QE-AUG",
"Q-SEP": "QE-SEP",
"Q-OCT": "QE-OCT",
"Q-NOV": "QE-NOV",
"Y": "YE",
"Y-DEC": "YE-DEC",
"Y-JAN": "YE-JAN",
"Y-FEB": "YE-FEB",
"Y-MAR": "YE-MAR",
"Y-APR": "YE-APR",
"Y-MAY": "YE-MAY",
"Y-JUN": "YE-JUN",
"Y-JUL": "YE-JUL",
"Y-AUG": "YE-AUG",
"Y-SEP": "YE-SEP",
"Y-OCT": "YE-OCT",
"Y-NOV": "YE-NOV",
}
cdef dict c_OFFSET_TO_PERIOD_FREQSTR = OFFSET_TO_PERIOD_FREQSTR
cdef dict c_PERIOD_TO_OFFSET_FREQSTR = PERIOD_TO_OFFSET_FREQSTR

# Map deprecated resolution abbreviations to correct resolution abbreviations
cdef dict c_DEPR_ABBREVS = {
Expand All @@ -316,6 +346,11 @@ cdef dict c_DEPR_ABBREVS = {
"S": "s",
}

cdef dict c_PERIOD_AND_OFFSET_DEPR_FREQSTR = {
"w": "W",
"MIN": "min",
}


class FreqGroup(Enum):
# Mirrors c_FreqGroup in the .pxd file
Expand Down
96 changes: 49 additions & 47 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ from pandas._libs.tslibs.ccalendar cimport (
from pandas._libs.tslibs.conversion cimport localize_pydatetime
from pandas._libs.tslibs.dtypes cimport (
c_DEPR_ABBREVS,
c_OFFSET_DEPR_FREQSTR,
c_REVERSE_OFFSET_DEPR_FREQSTR,
c_OFFSET_RENAMED_FREQSTR,
c_OFFSET_TO_PERIOD_FREQSTR,
c_PERIOD_AND_OFFSET_DEPR_FREQSTR,
c_PERIOD_TO_OFFSET_FREQSTR,
periods_per_day,
)
from pandas._libs.tslibs.nattype cimport (
Expand Down Expand Up @@ -4711,6 +4713,34 @@ INVALID_FREQ_ERR_MSG = "Invalid frequency: {0}"
_offset_map = {}


def _validate_to_offset_alias(alias: str, is_period: bool) -> None:
if not is_period:
if alias.upper() in c_OFFSET_RENAMED_FREQSTR:
raise ValueError(
f"\'{alias}\' is no longer supported for offsets. Please "
f"use \'{c_OFFSET_RENAMED_FREQSTR.get(alias.upper())}\' "
f"instead."
)
if (alias.upper() != alias and
alias.lower() not in {"s", "ms", "us", "ns"} and
alias.upper().split("-")[0].endswith(("S", "E"))):
raise ValueError(INVALID_FREQ_ERR_MSG.format(alias))
if (is_period and
alias.upper() in c_OFFSET_TO_PERIOD_FREQSTR and
alias != "ms" and
alias.upper().split("-")[0].endswith(("S", "E"))):
if (alias.upper().startswith("B") or
alias.upper().startswith("S") or
alias.upper().startswith("C")):
raise ValueError(INVALID_FREQ_ERR_MSG.format(alias))
else:
alias_msg = "".join(alias.upper().split("E", 1))
raise ValueError(
f"for Period, please use \'{alias_msg}\' "
f"instead of \'{alias}\'"
)


# TODO: better name?
def _get_offset(name: str) -> BaseOffset:
"""
Expand Down Expand Up @@ -4850,54 +4880,26 @@ cpdef to_offset(freq, bint is_period=False):

tups = zip(split[0::4], split[1::4], split[2::4])
for n, (sep, stride, name) in enumerate(tups):
if not is_period and name.upper() in c_OFFSET_DEPR_FREQSTR:
warnings.warn(
f"\'{name}\' is deprecated and will be removed "
f"in a future version, please use "
f"\'{c_OFFSET_DEPR_FREQSTR.get(name.upper())}\' instead.",
FutureWarning,
stacklevel=find_stack_level(),
)
name = c_OFFSET_DEPR_FREQSTR[name.upper()]
if (not is_period and
name != name.upper() and
name.lower() not in {"s", "ms", "us", "ns"} and
name.upper().split("-")[0].endswith(("S", "E"))):
warnings.warn(
f"\'{name}\' is deprecated and will be removed "
f"in a future version, please use "
f"\'{name.upper()}\' instead.",
FutureWarning,
stacklevel=find_stack_level(),
)
name = name.upper()
if is_period and name.upper() in c_REVERSE_OFFSET_DEPR_FREQSTR:
if name.upper().startswith("Y"):
raise ValueError(
f"for Period, please use \'Y{name.upper()[2:]}\' "
f"instead of \'{name}\'"
)
if (name.upper().startswith("B") or
name.upper().startswith("S") or
name.upper().startswith("C")):
raise ValueError(INVALID_FREQ_ERR_MSG.format(name))
else:
raise ValueError(
f"for Period, please use "
f"\'{c_REVERSE_OFFSET_DEPR_FREQSTR.get(name.upper())}\' "
f"instead of \'{name}\'"
)
elif is_period and name.upper() in c_OFFSET_DEPR_FREQSTR:
if name.upper() != name:
_validate_to_offset_alias(name, is_period)
if is_period:
if name.upper() in c_PERIOD_TO_OFFSET_FREQSTR:
if name.upper() != name:
raise ValueError(
f"\'{name}\' is no longer supported, "
f"please use \'{name.upper()}\' instead.",
)
name = c_PERIOD_TO_OFFSET_FREQSTR.get(name.upper())

if name in c_PERIOD_AND_OFFSET_DEPR_FREQSTR:
warnings.warn(
f"\'{name}\' is deprecated and will be removed in "
f"a future version, please use \'{name.upper()}\' "
f"instead.",
f"\'{name}\' is deprecated and will be removed "
f"in a future version, please use "
f"\'{c_PERIOD_AND_OFFSET_DEPR_FREQSTR.get(name)}\' "
f" instead.",
FutureWarning,
stacklevel=find_stack_level(),
)
name = c_OFFSET_DEPR_FREQSTR.get(name.upper())

)
name = c_PERIOD_AND_OFFSET_DEPR_FREQSTR.get(name)
if sep != "" and not sep.isspace():
raise ValueError("separator must be spaces")
prefix = _lite_rule_alias.get(name) or name
Expand Down
45 changes: 17 additions & 28 deletions pandas/tests/arrays/test_datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,29 +764,14 @@ def test_iter_zoneinfo_fold(self, tz):
assert left.utcoffset() == right2.utcoffset()

@pytest.mark.parametrize(
"freq, freq_depr",
[
("2ME", "2M"),
("2SME", "2SM"),
("2SME", "2sm"),
("2QE", "2Q"),
("2QE-SEP", "2Q-SEP"),
("1YE", "1Y"),
("2YE-MAR", "2Y-MAR"),
("2ME", "2m"),
("2QE-SEP", "2q-sep"),
("2YE", "2y"),
],
"freq",
["2M", "2SM", "2sm", "2Q", "2Q-SEP", "1Y", "2Y-MAR", "2m", "2q-sep", "2y"],
)
def test_date_range_frequency_M_Q_Y_A_deprecated(self, freq, freq_depr):
# GH#9586, GH#54275
depr_msg = f"'{freq_depr[1:]}' is deprecated and will be removed "
f"in a future version, please use '{freq[1:]}' instead."
def test_date_range_frequency_M_Q_Y_raises(self, freq):
msg = f"Invalid frequency: {freq}"

expected = pd.date_range("1/1/2000", periods=4, freq=freq)
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
result = pd.date_range("1/1/2000", periods=4, freq=freq_depr)
tm.assert_index_equal(result, expected)
with pytest.raises(ValueError, match=msg):
pd.date_range("1/1/2000", periods=4, freq=freq)

@pytest.mark.parametrize("freq_depr", ["2H", "2CBH", "2MIN", "2S", "2mS", "2Us"])
def test_date_range_uppercase_frequency_deprecated(self, freq_depr):
Expand All @@ -800,7 +785,7 @@ def test_date_range_uppercase_frequency_deprecated(self, freq_depr):
tm.assert_index_equal(result, expected)

@pytest.mark.parametrize(
"freq_depr",
"freq",
[
"2ye-mar",
"2ys",
Expand All @@ -811,17 +796,21 @@ def test_date_range_uppercase_frequency_deprecated(self, freq_depr):
"2bms",
"2cbme",
"2me",
"2w",
],
)
def test_date_range_lowercase_frequency_deprecated(self, freq_depr):
def test_date_range_lowercase_frequency_raises(self, freq):
msg = f"Invalid frequency: {freq}"

with pytest.raises(ValueError, match=msg):
pd.date_range("1/1/2000", periods=4, freq=freq)

def test_date_range_lowercase_frequency_deprecated(self):
# GH#9586, GH#54939
depr_msg = f"'{freq_depr[1:]}' is deprecated and will be removed in a "
f"future version, please use '{freq_depr.upper()[1:]}' instead."
depr_msg = "'w' is deprecated and will be removed in a future version"

expected = pd.date_range("1/1/2000", periods=4, freq=freq_depr.upper())
expected = pd.date_range("1/1/2000", periods=4, freq="2W")
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
result = pd.date_range("1/1/2000", periods=4, freq=freq_depr)
result = pd.date_range("1/1/2000", periods=4, freq="2w")
tm.assert_index_equal(result, expected)

@pytest.mark.parametrize("freq", ["1A", "2A-MAR", "2a-mar"])
Expand Down
18 changes: 8 additions & 10 deletions pandas/tests/frame/methods/test_asfreq.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,32 +236,30 @@ def test_asfreq_2ME(self, freq, freq_half):
"freq, freq_depr",
[
("2ME", "2M"),
("2ME", "2m"),
("2QE", "2Q"),
("2QE-SEP", "2Q-SEP"),
("1BQE", "1BQ"),
("2BQE-SEP", "2BQ-SEP"),
("1YE", "1Y"),
("2BQE-SEP", "2bq-sep"),
("1YE", "1y"),
("2YE-MAR", "2Y-MAR"),
],
)
def test_asfreq_frequency_M_Q_Y_deprecated(self, freq, freq_depr):
# GH#9586, #55978
depr_msg = f"'{freq_depr[1:]}' is deprecated and will be removed "
f"in a future version, please use '{freq[1:]}' instead."
def test_asfreq_frequency_M_Q_Y_raises(self, freq, freq_depr):
msg = f"Invalid frequency: {freq_depr}"

index = date_range("1/1/2000", periods=4, freq=f"{freq[1:]}")
df = DataFrame({"s": Series([0.0, 1.0, 2.0, 3.0], index=index)})
expected = df.asfreq(freq=freq)
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
result = df.asfreq(freq=freq_depr)
tm.assert_frame_equal(result, expected)
with pytest.raises(ValueError, match=msg):
df.asfreq(freq=freq_depr)

@pytest.mark.parametrize(
"freq, error_msg",
[
(
"2MS",
"MS is not supported as period frequency",
"Invalid frequency: 2MS",
),
(
offsets.MonthBegin(),
Expand Down
Loading

0 comments on commit c95716a

Please sign in to comment.