Skip to content

Commit 45fc954

Browse files
Backport PR #56945 on branch 2.2.x (ENH: raise ValueError if invalid period freq pass to asfreq when the index of df is a PeriodIndex) (#57292)
'Backport PR #56945: ENH: raise ValueError if invalid period freq pass to asfreq when the index of df is a PeriodIndex' (cherry picked from commit cb97ce6) Co-authored-by: Natalia Mokeeva <[email protected]>
1 parent c1b17ae commit 45fc954

File tree

10 files changed

+96
-41
lines changed

10 files changed

+96
-41
lines changed

doc/source/whatsnew/v2.2.1.rst

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Bug fixes
3535
- Fixed bug in :func:`pandas.api.interchange.from_dataframe` which was raising for empty inputs (:issue:`56700`)
3636
- Fixed bug in :func:`pandas.api.interchange.from_dataframe` which wasn't converting columns names to strings (:issue:`55069`)
3737
- Fixed bug in :meth:`DataFrame.__getitem__` for empty :class:`DataFrame` with Copy-on-Write enabled (:issue:`57130`)
38+
- Fixed bug in :meth:`PeriodIndex.asfreq` which was silently converting frequencies which are not supported as period frequencies instead of raising an error (:issue:`56945`)
3839

3940
.. ---------------------------------------------------------------------------
4041
.. _whatsnew_221.other:

pandas/_libs/tslibs/offsets.pyx

+18-14
Original file line numberDiff line numberDiff line change
@@ -4271,9 +4271,7 @@ cdef class CustomBusinessDay(BusinessDay):
42714271
@property
42724272
def _period_dtype_code(self):
42734273
# GH#52534
4274-
raise TypeError(
4275-
"CustomBusinessDay is not supported as period frequency"
4276-
)
4274+
raise ValueError(f"{self.base} is not supported as period frequency")
42774275

42784276
_apply_array = BaseOffset._apply_array
42794277

@@ -4822,19 +4820,19 @@ cpdef to_offset(freq, bint is_period=False):
48224820
if freq is None:
48234821
return None
48244822

4825-
if isinstance(freq, BaseOffset):
4826-
return freq
4827-
48284823
if isinstance(freq, tuple):
48294824
raise TypeError(
48304825
f"to_offset does not support tuples {freq}, pass as a string instead"
48314826
)
48324827

4828+
if isinstance(freq, BaseOffset):
4829+
result = freq
4830+
48334831
elif PyDelta_Check(freq):
4834-
return delta_to_tick(freq)
4832+
result = delta_to_tick(freq)
48354833

48364834
elif isinstance(freq, str):
4837-
delta = None
4835+
result = None
48384836
stride_sign = None
48394837

48404838
try:
@@ -4935,21 +4933,27 @@ cpdef to_offset(freq, bint is_period=False):
49354933
offset = _get_offset(prefix)
49364934
offset = offset * int(np.fabs(stride) * stride_sign)
49374935

4938-
if delta is None:
4939-
delta = offset
4936+
if result is None:
4937+
result = offset
49404938
else:
4941-
delta = delta + offset
4939+
result = result + offset
49424940
except (ValueError, TypeError) as err:
49434941
raise ValueError(INVALID_FREQ_ERR_MSG.format(
49444942
f"{freq}, failed to parse with error message: {repr(err)}")
49454943
)
49464944
else:
4947-
delta = None
4945+
result = None
49484946

4949-
if delta is None:
4947+
if result is None:
49504948
raise ValueError(INVALID_FREQ_ERR_MSG.format(freq))
49514949

4952-
return delta
4950+
if is_period and not hasattr(result, "_period_dtype_code"):
4951+
if isinstance(freq, str):
4952+
raise ValueError(f"{result.name} is not supported as period frequency")
4953+
else:
4954+
raise ValueError(f"{freq} is not supported as period frequency")
4955+
4956+
return result
49534957

49544958

49554959
# ----------------------------------------------------------------------

pandas/core/arrays/period.py

+3-8
Original file line numberDiff line numberDiff line change
@@ -733,8 +733,8 @@ def asfreq(self, freq=None, how: str = "E") -> Self:
733733
'2015-01'], dtype='period[M]')
734734
"""
735735
how = libperiod.validate_end_alias(how)
736-
if isinstance(freq, BaseOffset):
737-
freq = freq_to_period_freqstr(freq.n, freq.name)
736+
if isinstance(freq, BaseOffset) and hasattr(freq, "_period_dtype_code"):
737+
freq = PeriodDtype(freq)._freqstr
738738
freq = Period._maybe_convert_freq(freq)
739739

740740
base1 = self._dtype._dtype_code
@@ -1186,12 +1186,7 @@ def dt64arr_to_periodarr(
11861186

11871187
reso = get_unit_from_dtype(data.dtype)
11881188
freq = Period._maybe_convert_freq(freq)
1189-
try:
1190-
base = freq._period_dtype_code
1191-
except (AttributeError, TypeError):
1192-
# AttributeError: _period_dtype_code might not exist
1193-
# TypeError: _period_dtype_code might intentionally raise
1194-
raise TypeError(f"{freq.name} is not supported as period frequency")
1189+
base = freq._period_dtype_code
11951190
return c_dt64arr_to_periodarr(data.view("i8"), base, tz, reso=reso), freq
11961191

11971192

pandas/plotting/_matplotlib/timeseries.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,10 @@ def _get_ax_freq(ax: Axes):
205205

206206

207207
def _get_period_alias(freq: timedelta | BaseOffset | str) -> str | None:
208-
freqstr = to_offset(freq, is_period=True).rule_code
208+
if isinstance(freq, BaseOffset):
209+
freqstr = freq.name
210+
else:
211+
freqstr = to_offset(freq, is_period=True).rule_code
209212

210213
return get_period_alias(freqstr)
211214

pandas/tests/dtypes/test_dtypes.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -445,12 +445,12 @@ def test_construction(self):
445445

446446
def test_cannot_use_custom_businessday(self):
447447
# GH#52534
448-
msg = "CustomBusinessDay is not supported as period frequency"
448+
msg = "C is not supported as period frequency"
449+
msg1 = "<CustomBusinessDay> is not supported as period frequency"
449450
msg2 = r"PeriodDtype\[B\] is deprecated"
450-
with pytest.raises(TypeError, match=msg):
451-
with tm.assert_produces_warning(FutureWarning, match=msg2):
452-
PeriodDtype("C")
453-
with pytest.raises(TypeError, match=msg):
451+
with pytest.raises(ValueError, match=msg):
452+
PeriodDtype("C")
453+
with pytest.raises(ValueError, match=msg1):
454454
with tm.assert_produces_warning(FutureWarning, match=msg2):
455455
PeriodDtype(pd.offsets.CustomBusinessDay())
456456

pandas/tests/indexes/datetimes/methods/test_to_period.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -221,5 +221,5 @@ def test_to_period_offsets_not_supported(self, freq):
221221
# GH#56243
222222
msg = f"{freq[1:]} is not supported as period frequency"
223223
ts = date_range("1/1/2012", periods=4, freq=freq)
224-
with pytest.raises(TypeError, match=msg):
224+
with pytest.raises(ValueError, match=msg):
225225
ts.to_period()

pandas/tests/indexes/period/methods/test_asfreq.py

+51
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import re
2+
13
import pytest
24

35
from pandas import (
@@ -7,6 +9,8 @@
79
)
810
import pandas._testing as tm
911

12+
from pandas.tseries import offsets
13+
1014

1115
class TestPeriodIndex:
1216
def test_asfreq(self):
@@ -136,3 +140,50 @@ def test_asfreq_with_different_n(self):
136140

137141
excepted = Series([1, 2], index=PeriodIndex(["2020-02", "2020-04"], freq="M"))
138142
tm.assert_series_equal(result, excepted)
143+
144+
@pytest.mark.parametrize(
145+
"freq",
146+
[
147+
"2BMS",
148+
"2YS-MAR",
149+
"2bh",
150+
],
151+
)
152+
def test_pi_asfreq_not_supported_frequency(self, freq):
153+
# GH#55785
154+
msg = f"{freq[1:]} is not supported as period frequency"
155+
156+
pi = PeriodIndex(["2020-01-01", "2021-01-01"], freq="M")
157+
with pytest.raises(ValueError, match=msg):
158+
pi.asfreq(freq=freq)
159+
160+
@pytest.mark.parametrize(
161+
"freq",
162+
[
163+
"2BME",
164+
"2YE-MAR",
165+
"2QE",
166+
],
167+
)
168+
def test_pi_asfreq_invalid_frequency(self, freq):
169+
# GH#55785
170+
msg = f"Invalid frequency: {freq}"
171+
172+
pi = PeriodIndex(["2020-01-01", "2021-01-01"], freq="M")
173+
with pytest.raises(ValueError, match=msg):
174+
pi.asfreq(freq=freq)
175+
176+
@pytest.mark.parametrize(
177+
"freq",
178+
[
179+
offsets.MonthBegin(2),
180+
offsets.BusinessMonthEnd(2),
181+
],
182+
)
183+
def test_pi_asfreq_invalid_baseoffset(self, freq):
184+
# GH#56945
185+
msg = re.escape(f"{freq} is not supported as period frequency")
186+
187+
pi = PeriodIndex(["2020-01-01", "2021-01-01"], freq="M")
188+
with pytest.raises(ValueError, match=msg):
189+
pi.asfreq(freq=freq)

pandas/tests/resample/test_period_index.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1040,8 +1040,8 @@ def test_resample_lowercase_frequency_deprecated(
10401040
offsets.BusinessHour(2),
10411041
],
10421042
)
1043-
def test_asfreq_invalid_period_freq(self, offset, series_and_frame):
1044-
# GH#9586
1043+
def test_asfreq_invalid_period_offset(self, offset, series_and_frame):
1044+
# GH#55785
10451045
msg = f"Invalid offset: '{offset.base}' for converting time series "
10461046

10471047
df = series_and_frame

pandas/tests/scalar/period/test_asfreq.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -820,10 +820,9 @@ def test_asfreq_MS(self):
820820

821821
assert initial.asfreq(freq="M", how="S") == Period("2013-01", "M")
822822

823-
msg = INVALID_FREQ_ERR_MSG
823+
msg = "MS is not supported as period frequency"
824824
with pytest.raises(ValueError, match=msg):
825825
initial.asfreq(freq="MS", how="S")
826826

827-
msg = "MonthBegin is not supported as period frequency"
828-
with pytest.raises(TypeError, match=msg):
827+
with pytest.raises(ValueError, match=msg):
829828
Period("2013-01", "MS")

pandas/tests/scalar/period/test_period.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
datetime,
44
timedelta,
55
)
6+
import re
67

78
import numpy as np
89
import pytest
@@ -40,21 +41,22 @@ class TestPeriodDisallowedFreqs:
4041
)
4142
def test_offsets_not_supported(self, freq, freq_msg):
4243
# GH#55785
43-
msg = f"{freq_msg} is not supported as period frequency"
44-
with pytest.raises(TypeError, match=msg):
44+
msg = re.escape(f"{freq} is not supported as period frequency")
45+
with pytest.raises(ValueError, match=msg):
4546
Period(year=2014, freq=freq)
4647

4748
def test_custom_business_day_freq_raises(self):
4849
# GH#52534
49-
msg = "CustomBusinessDay is not supported as period frequency"
50-
with pytest.raises(TypeError, match=msg):
50+
msg = "C is not supported as period frequency"
51+
with pytest.raises(ValueError, match=msg):
5152
Period("2023-04-10", freq="C")
52-
with pytest.raises(TypeError, match=msg):
53+
msg = f"{offsets.CustomBusinessDay().base} is not supported as period frequency"
54+
with pytest.raises(ValueError, match=msg):
5355
Period("2023-04-10", freq=offsets.CustomBusinessDay())
5456

5557
def test_invalid_frequency_error_message(self):
56-
msg = "WeekOfMonth is not supported as period frequency"
57-
with pytest.raises(TypeError, match=msg):
58+
msg = "WOM-1MON is not supported as period frequency"
59+
with pytest.raises(ValueError, match=msg):
5860
Period("2012-01-02", freq="WOM-1MON")
5961

6062
def test_invalid_frequency_period_error_message(self):

0 commit comments

Comments
 (0)