diff --git a/doc/source/user_guide/timeseries.rst b/doc/source/user_guide/timeseries.rst index 6731b70fc3860..22e3921bbf06b 100644 --- a/doc/source/user_guide/timeseries.rst +++ b/doc/source/user_guide/timeseries.rst @@ -886,7 +886,7 @@ into ``freq`` keyword arguments. The available date offsets and associated frequ :class:`~pandas.tseries.offsets.SemiMonthBegin`, ``'SMS'``, "15th (or other day_of_month) and calendar month begin" :class:`~pandas.tseries.offsets.QuarterEnd`, ``'QE'``, "calendar quarter end" :class:`~pandas.tseries.offsets.QuarterBegin`, ``'QS'``, "calendar quarter begin" - :class:`~pandas.tseries.offsets.BQuarterEnd`, ``'BQ``, "business quarter end" + :class:`~pandas.tseries.offsets.BQuarterEnd`, ``'BQE``, "business quarter end" :class:`~pandas.tseries.offsets.BQuarterBegin`, ``'BQS'``, "business quarter begin" :class:`~pandas.tseries.offsets.FY5253Quarter`, ``'REQ'``, "retail (aka 52-53 week) quarter" :class:`~pandas.tseries.offsets.YearEnd`, ``'YE'``, "calendar year end" @@ -1249,7 +1249,7 @@ frequencies. We will refer to these aliases as *offset aliases*. "BMS", "business month start frequency" "CBMS", "custom business month start frequency" "QE", "quarter end frequency" - "BQ", "business quarter end frequency" + "BQE", "business quarter end frequency" "QS", "quarter start frequency" "BQS", "business quarter start frequency" "YE", "year end frequency" @@ -1686,7 +1686,7 @@ the end of the interval. .. warning:: The default values for ``label`` and ``closed`` is '**left**' for all - frequency offsets except for 'ME', 'YE', 'QE', 'BME', 'BY', 'BQ', and 'W' + frequency offsets except for 'ME', 'YE', 'QE', 'BME', 'BY', 'BQE', and 'W' which all have a default of 'right'. This might unintendedly lead to looking ahead, where the value for a later diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst index 9040dba238c88..cea4e82cf9c5c 100644 --- a/doc/source/whatsnew/v2.2.0.rst +++ b/doc/source/whatsnew/v2.2.0.rst @@ -287,6 +287,7 @@ Other Deprecations - Deprecated string ``A`` denoting frequency in :class:`YearEnd` and strings ``A-DEC``, ``A-JAN``, etc. denoting annual frequencies with various fiscal year ends (:issue:`54275`) - Deprecated string ``BAS`` denoting frequency in :class:`BYearBegin` and strings ``BAS-DEC``, ``BAS-JAN``, etc. denoting annual frequencies with various fiscal year starts (:issue:`54275`) - Deprecated string ``BA`` denoting frequency in :class:`BYearEnd` and strings ``BA-DEC``, ``BA-JAN``, etc. denoting annual frequencies with various fiscal year ends (:issue:`54275`) +- Deprecated string ``BQ`` denoting frequency in :class:`BQuarterEnd` (:issue:`52064`) - Deprecated strings ``BM``, and ``CBM`` denoting frequencies in :class:`BusinessMonthEnd`, :class:`CustomBusinessMonthEnd` (:issue:`52064`) - Deprecated strings ``H``, ``BH``, and ``CBH`` denoting frequencies in :class:`Hour`, :class:`BusinessHour`, :class:`CustomBusinessHour` (:issue:`52536`) - Deprecated strings ``H``, ``S``, ``U``, and ``N`` denoting units in :func:`to_timedelta` (:issue:`52536`) diff --git a/pandas/_libs/tslibs/dtypes.pyx b/pandas/_libs/tslibs/dtypes.pyx index a8dc6b12a54b5..73ae704167537 100644 --- a/pandas/_libs/tslibs/dtypes.pyx +++ b/pandas/_libs/tslibs/dtypes.pyx @@ -191,7 +191,19 @@ OFFSET_TO_PERIOD_FREQSTR: dict = { "BME": "M", "BQS": "Q", "QS": "Q", - "BQ": "Q", + "BQE": "Q", + "BQE-DEC": "Q-DEC", + "BQE-JAN": "Q-JAN", + "BQE-FEB": "Q-FEB", + "BQE-MAR": "Q-MAR", + "BQE-APR": "Q-APR", + "BQE-MAY": "Q-MAY", + "BQE-JUN": "Q-JUN", + "BQE-JUL": "Q-JUL", + "BQE-AUG": "Q-AUG", + "BQE-SEP": "Q-SEP", + "BQE-OCT": "Q-OCT", + "BQE-NOV": "Q-NOV", "MS": "M", "D": "D", "B": "B", @@ -275,6 +287,21 @@ OFFSET_DEPR_FREQSTR: dict[str, str]= { "A-SEP": "YE-SEP", "A-OCT": "YE-OCT", "A-NOV": "YE-NOV", + "BM": "BME", + "CBM": "CBME", + "BQ": "BQE", + "BQ-DEC": "BQE-DEC", + "BQ-JAN": "BQE-JAN", + "BQ-FEB": "BQE-FEB", + "BQ-MAR": "BQE-MAR", + "BQ-APR": "BQE-APR", + "BQ-MAY": "BQE-MAY", + "BQ-JUN": "BQE-JUN", + "BQ-JUL": "BQE-JUL", + "BQ-AUG": "BQE-AUG", + "BQ-SEP": "BQE-SEP", + "BQ-OCT": "BQE-OCT", + "BQ-NOV": "BQE-NOV", } cdef dict c_OFFSET_TO_PERIOD_FREQSTR = OFFSET_TO_PERIOD_FREQSTR cdef dict c_OFFSET_DEPR_FREQSTR = OFFSET_DEPR_FREQSTR diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 7f3a72178a359..773e9e53b60da 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -2693,7 +2693,7 @@ cdef class BQuarterEnd(QuarterOffset): _output_name = "BusinessQuarterEnd" _default_starting_month = 3 _from_name_starting_month = 12 - _prefix = "BQ" + _prefix = "BQE" _day_opt = "business_end" @@ -4568,7 +4568,7 @@ prefix_mapping = { BusinessDay, # 'B' BusinessMonthBegin, # 'BMS' BusinessMonthEnd, # 'BME' - BQuarterEnd, # 'BQ' + BQuarterEnd, # 'BQE' BQuarterBegin, # 'BQS' BusinessHour, # 'bh' CustomBusinessDay, # 'C' @@ -4752,7 +4752,7 @@ cpdef to_offset(freq, bint is_period=False): for n, (sep, stride, name) in enumerate(tups): if is_period is False and name in c_OFFSET_DEPR_FREQSTR: warnings.warn( - f"\'{name}\' will be deprecated, please use " + f"\'{name}\' is deprecated, please use " f"\'{c_OFFSET_DEPR_FREQSTR.get(name)}\' instead.", FutureWarning, stacklevel=find_stack_level(), diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 7918e43b48719..1674960f21b19 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -9200,11 +9200,11 @@ def resample( closed : {{'right', 'left'}}, default None Which side of bin interval is closed. The default is 'left' for all frequency offsets except for 'ME', 'YE', 'QE', 'BME', - 'BA', 'BQ', and 'W' which all have a default of 'right'. + 'BA', 'BQE', and 'W' which all have a default of 'right'. label : {{'right', 'left'}}, default None Which bin edge label to label bucket with. The default is 'left' for all frequency offsets except for 'ME', 'YE', 'QE', 'BME', - 'BA', 'BQ', and 'W' which all have a default of 'right'. + 'BA', 'BQE', and 'W' which all have a default of 'right'. convention : {{'start', 'end', 's', 'e'}}, default 'start' For `PeriodIndex` only, controls whether to use the start or end of `rule`. diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 14cd77ec8559b..6a95457526b5a 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -2130,7 +2130,7 @@ def __init__( else: freq = to_offset(freq) - end_types = {"ME", "YE", "QE", "BME", "BY", "BQ", "W"} + end_types = {"ME", "YE", "QE", "BME", "BY", "BQE", "W"} rule = freq.rule_code if rule in end_types or ("-" in rule and rule[: rule.find("-")] in end_types): if closed is None: @@ -2327,7 +2327,7 @@ def _adjust_bin_edges( # Some hacks for > daily data, see #1471, #1458, #1483 if self.freq.name in ("BME", "ME", "W") or self.freq.name.split("-")[0] in ( - "BQ", + "BQE", "BY", "QE", "YE", diff --git a/pandas/tests/arrays/test_datetimes.py b/pandas/tests/arrays/test_datetimes.py index 566117899cfc5..3fe53db1b714c 100644 --- a/pandas/tests/arrays/test_datetimes.py +++ b/pandas/tests/arrays/test_datetimes.py @@ -756,9 +756,7 @@ def test_iter_zoneinfo_fold(self, tz): ) def test_date_range_frequency_M_Q_Y_A_deprecated(self, freq, freq_depr): # GH#9586, GH#54275 - depr_msg = ( - f"'{freq_depr[1:]}' will be deprecated, please use '{freq[1:]}' instead." - ) + depr_msg = f"'{freq_depr[1:]}' is deprecated, please use '{freq[1:]}' instead." expected = pd.date_range("1/1/2000", periods=4, freq=freq) with tm.assert_produces_warning(FutureWarning, match=depr_msg): diff --git a/pandas/tests/frame/methods/test_asfreq.py b/pandas/tests/frame/methods/test_asfreq.py index 616ad5133e778..13ba002f2cdc8 100644 --- a/pandas/tests/frame/methods/test_asfreq.py +++ b/pandas/tests/frame/methods/test_asfreq.py @@ -240,6 +240,8 @@ def test_asfreq_2ME(self, freq, freq_half): ("2ME", "2M"), ("2QE", "2Q"), ("2QE-SEP", "2Q-SEP"), + ("1BQE", "1BQ"), + ("2BQE-SEP", "2BQ-SEP"), ("1YE", "1Y"), ("2YE-MAR", "2Y-MAR"), ("1YE", "1A"), @@ -247,10 +249,8 @@ def test_asfreq_2ME(self, freq, freq_half): ], ) def test_asfreq_frequency_M_Q_Y_A_deprecated(self, freq, freq_depr): - # GH#9586 - depr_msg = ( - f"'{freq_depr[1:]}' will be deprecated, please use '{freq[1:]}' instead." - ) + # GH#9586, #55978 + depr_msg = f"'{freq_depr[1:]}' is deprecated, please use '{freq[1:]}' instead." 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)}) diff --git a/pandas/tests/indexes/datetimes/methods/test_to_period.py b/pandas/tests/indexes/datetimes/methods/test_to_period.py index af04f827d4593..206524e77c2a5 100644 --- a/pandas/tests/indexes/datetimes/methods/test_to_period.py +++ b/pandas/tests/indexes/datetimes/methods/test_to_period.py @@ -50,7 +50,7 @@ def test_to_period_quarterly(self, month): result = stamps.to_period(freq) tm.assert_index_equal(rng, result) - @pytest.mark.parametrize("off", ["BQ", "QS", "BQS"]) + @pytest.mark.parametrize("off", ["BQE", "QS", "BQS"]) def test_to_period_quarterlyish(self, off): rng = date_range("01-Jan-2012", periods=8, freq=off) prng = rng.to_period() @@ -103,7 +103,7 @@ def test_dti_to_period_2monthish(self, freq_offset, freq_period): ) def test_to_period_frequency_M_Q_Y_A_deprecated(self, freq, freq_depr): # GH#9586 - msg = f"'{freq_depr[1:]}' will be deprecated, please use '{freq[1:]}' instead." + msg = f"'{freq_depr[1:]}' is deprecated, please use '{freq[1:]}' instead." rng = date_range("01-Jan-2012", periods=8, freq=freq) prng = rng.to_period() diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py index 3a6d1cacd0f81..cacf232c9b6d1 100644 --- a/pandas/tests/indexes/datetimes/test_date_range.py +++ b/pandas/tests/indexes/datetimes/test_date_range.py @@ -146,7 +146,7 @@ def test_date_range_float_periods(self): tm.assert_index_equal(rng, exp) def test_date_range_frequency_M_deprecated(self): - depr_msg = "'M' will be deprecated, please use 'ME' instead." + depr_msg = "'M' is deprecated, please use 'ME' instead." expected = date_range("1/1/2000", periods=4, freq="2ME") with tm.assert_produces_warning(FutureWarning, match=depr_msg): @@ -786,7 +786,7 @@ def test_frequencies_A_deprecated_Y_renamed(self, freq, freq_depr): # GH#9586, GH#54275 freq_msg = re.split("[0-9]*", freq, maxsplit=1)[1] freq_depr_msg = re.split("[0-9]*", freq_depr, maxsplit=1)[1] - msg = f"'{freq_depr_msg}' will be deprecated, please use '{freq_msg}' instead." + msg = f"'{freq_depr_msg}' is deprecated, please use '{freq_msg}' instead." expected = date_range("1/1/2000", periods=2, freq=freq) with tm.assert_produces_warning(FutureWarning, match=msg): @@ -1645,7 +1645,7 @@ def test_date_range_fy5253(self, unit): "freqstr,offset", [ ("QS", offsets.QuarterBegin(startingMonth=1)), - ("BQ", offsets.BQuarterEnd(startingMonth=12)), + ("BQE", offsets.BQuarterEnd(startingMonth=12)), ("W-SUN", offsets.Week(weekday=6)), ], ) diff --git a/pandas/tests/indexes/datetimes/test_datetime.py b/pandas/tests/indexes/datetimes/test_datetime.py index a1074a1744c7a..3c37aee3c3309 100644 --- a/pandas/tests/indexes/datetimes/test_datetime.py +++ b/pandas/tests/indexes/datetimes/test_datetime.py @@ -201,16 +201,24 @@ def test_AS_BA_BAS_deprecated(self, freq_depr, expected_values, expected_freq): tm.assert_index_equal(result, expected) - def test_BM_deprecated(self): + @pytest.mark.parametrize( + "freq, expected_values, freq_depr", + [ + ("2BME", ["2016-02-29", "2016-04-29", "2016-06-30"], "2BM"), + ("2BQE", ["2016-03-31"], "2BQ"), + ("1BQE-MAR", ["2016-03-31", "2016-06-30"], "1BQ-MAR"), + ], + ) + def test_BM_BQ_deprecated(self, freq, expected_values, freq_depr): # GH#52064 - msg = "'BM' is deprecated and will be removed in a future version." + msg = f"'{freq_depr[1:]}' is deprecated, please use '{freq[1:]}' instead." with tm.assert_produces_warning(FutureWarning, match=msg): - expected = date_range(start="2016-02-21", end="2016-08-21", freq="2BM") + expected = date_range(start="2016-02-21", end="2016-08-21", freq=freq_depr) result = DatetimeIndex( - ["2016-02-29", "2016-04-29", "2016-06-30"], + data=expected_values, dtype="datetime64[ns]", - freq="2BME", + freq=freq, ) tm.assert_index_equal(result, expected) diff --git a/pandas/tests/indexes/datetimes/test_scalar_compat.py b/pandas/tests/indexes/datetimes/test_scalar_compat.py index 6fbfe27acc0e0..c361c15c79758 100644 --- a/pandas/tests/indexes/datetimes/test_scalar_compat.py +++ b/pandas/tests/indexes/datetimes/test_scalar_compat.py @@ -308,7 +308,7 @@ def test_dti_fields(self, tz): tm.assert_index_equal(res, exp) def test_dti_is_year_quarter_start(self): - dti = date_range(freq="BQ-FEB", start=datetime(1998, 1, 1), periods=4) + dti = date_range(freq="BQE-FEB", start=datetime(1998, 1, 1), periods=4) assert sum(dti.is_quarter_start) == 0 assert sum(dti.is_quarter_end) == 4 diff --git a/pandas/tests/indexes/period/test_period.py b/pandas/tests/indexes/period/test_period.py index 6965aaf19f8fb..6411abe00a75a 100644 --- a/pandas/tests/indexes/period/test_period.py +++ b/pandas/tests/indexes/period/test_period.py @@ -299,7 +299,7 @@ def test_a_deprecated_from_time_series(self, freq_depr): series = Series(1, index=index) assert isinstance(series, Series) - @pytest.mark.parametrize("freq_depr", ["2ME", "2QE", "2YE"]) + @pytest.mark.parametrize("freq_depr", ["2ME", "2QE", "2BQE", "2YE"]) def test_period_index_frequency_error_message(self, freq_depr): # GH#9586 msg = f"for Period, please use '{freq_depr[1:-1]}' " diff --git a/pandas/tests/resample/test_datetime_index.py b/pandas/tests/resample/test_datetime_index.py index 9cf4e68a9c5ec..54d2fa0f50346 100644 --- a/pandas/tests/resample/test_datetime_index.py +++ b/pandas/tests/resample/test_datetime_index.py @@ -2058,7 +2058,7 @@ def test_resample_empty_series_with_tz(): ) def test_resample_M_Q_Y_A_deprecated(freq, freq_depr): # GH#9586 - depr_msg = f"'{freq_depr[1:]}' will be deprecated, please use '{freq[1:]}' instead." + depr_msg = f"'{freq_depr[1:]}' is deprecated, please use '{freq[1:]}' instead." s = Series(range(10), index=date_range("20130101", freq="d", periods=10)) expected = s.resample(freq).mean() @@ -2067,14 +2067,22 @@ def test_resample_M_Q_Y_A_deprecated(freq, freq_depr): tm.assert_series_equal(result, expected) -def test_resample_BM_deprecated(): +@pytest.mark.parametrize( + "freq, freq_depr", + [ + ("2BME", "2BM"), + ("2BQE", "2BQ"), + ("2BQE-MAR", "2BQ-MAR"), + ], +) +def test_resample_BM_BQ_deprecated(freq, freq_depr): # GH#52064 - depr_msg = "'BM' is deprecated and will be removed in a future version." + depr_msg = f"'{freq_depr[1:]}' is deprecated, please use '{freq[1:]}' instead." s = Series(range(10), index=date_range("20130101", freq="d", periods=10)) - expected = s.resample("2BME").mean() + expected = s.resample(freq).mean() with tm.assert_produces_warning(FutureWarning, match=depr_msg): - result = s.resample("2BM").mean() + result = s.resample(freq_depr).mean() tm.assert_series_equal(result, expected) diff --git a/pandas/tests/resample/test_period_index.py b/pandas/tests/resample/test_period_index.py index f3a7c94fc9eaa..3c5253a36c04b 100644 --- a/pandas/tests/resample/test_period_index.py +++ b/pandas/tests/resample/test_period_index.py @@ -924,10 +924,13 @@ def test_resample_t_l_deprecated(self): tm.assert_series_equal(result, expected) -@pytest.mark.parametrize("freq_depr", ["2ME", "2QE", "2QE-FEB", "2YE", "2YE-MAR"]) +@pytest.mark.parametrize( + "freq_depr", ["2ME", "2QE", "2QE-FEB", "2BQE", "2BQE-FEB", "2YE", "2YE-MAR"] +) def test_resample_frequency_ME_QE_error_message(series_and_frame, freq_depr): # GH#9586 - msg = f"for Period, please use '{freq_depr[1:2]}{freq_depr[3:]}' " + pos_e = freq_depr.index("E") + msg = f"for Period, please use '{freq_depr[1:pos_e]}{freq_depr[pos_e+1:]}' " f"instead of '{freq_depr[1:]}'" obj = series_and_frame diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 6d5b762b4f15a..f360d2d239384 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -838,7 +838,7 @@ def test_rule_code(self): "NOV", "DEC", ] - base_lst = ["YE", "YS", "BY", "BYS", "QE", "QS", "BQ", "BQS"] + base_lst = ["YE", "YS", "BY", "BYS", "QE", "QS", "BQE", "BQS"] for base in base_lst: for v in suffix_lst: alias = "-".join([base, v]) @@ -857,7 +857,7 @@ def test_freq_offsets(): class TestReprNames: def test_str_for_named_is_name(self): # look at all the amazing combinations! - month_prefixes = ["YE", "YS", "BY", "BYS", "QE", "BQ", "BQS", "QS"] + month_prefixes = ["YE", "YS", "BY", "BYS", "QE", "BQE", "BQS", "QS"] names = [ prefix + "-" + month for prefix in month_prefixes @@ -1122,7 +1122,7 @@ def test_is_yqm_start_end(): bm = to_offset("BME") qfeb = to_offset("QE-FEB") qsfeb = to_offset("QS-FEB") - bq = to_offset("BQ") + bq = to_offset("BQE") bqs_apr = to_offset("BQS-APR") as_nov = to_offset("YS-NOV") diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index 8460ef5826e34..d6ed3b1e90642 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -59,7 +59,7 @@ # -------------------------------------------------------------------- # Offset related functions -_need_suffix = ["QS", "BQ", "BQS", "YS", "BY", "BYS"] +_need_suffix = ["QS", "BQE", "BQS", "YS", "BY", "BYS"] for _prefix in _need_suffix: for _m in MONTHS: @@ -359,7 +359,7 @@ def _get_quarterly_rule(self) -> str | None: if pos_check is None: return None else: - return {"cs": "QS", "bs": "BQS", "ce": "QE", "be": "BQ"}.get(pos_check) + return {"cs": "QS", "bs": "BQS", "ce": "QE", "be": "BQE"}.get(pos_check) def _get_monthly_rule(self) -> str | None: if len(self.mdiffs) > 1: