Repository: pandas-dev/pandas
Pull request #56050 (2023-11-18 18:12:54)

DEPR offsets: rename 'SM' to 'SME'
xref #52064

Deprecated the alias denoting semi month end frequency ’SM’ for offsets in favour of ‘SME'

natmokval: @MarcoGorelli, could you please take a look at this PR? I think ci failures are unrelated to my changes.

Patch:
diff --git a/doc/source/user_guide/timeseries.rst b/doc/source/user_guide/timeseries.rst
index 22e3921bbf..ca57e68d76 100644
--- a/doc/source/user_guide/timeseries.rst
+++ b/doc/source/user_guide/timeseries.rst
@@ -882,7 +882,7 @@ into ``freq`` keyword arguments. The available date offsets and associated frequ
     :class:`~pandas.tseries.offsets.BMonthBegin` or :class:`~pandas.tseries.offsets.BusinessMonthBegin`, ``'BMS'``, "business month begin"
     :class:`~pandas.tseries.offsets.CBMonthEnd` or :class:`~pandas.tseries.offsets.CustomBusinessMonthEnd`, ``'CBME'``, "custom business month end"
     :class:`~pandas.tseries.offsets.CBMonthBegin` or :class:`~pandas.tseries.offsets.CustomBusinessMonthBegin`, ``'CBMS'``, "custom business month begin"
-    :class:`~pandas.tseries.offsets.SemiMonthEnd`, ``'SM'``, "15th (or other day_of_month) and calendar month end"
+    :class:`~pandas.tseries.offsets.SemiMonthEnd`, ``'SME'``, "15th (or other day_of_month) and calendar month end"
     :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"
@@ -1241,7 +1241,7 @@ frequencies. We will refer to these aliases as *offset aliases*.
     "D", "calendar day frequency"
     "W", "weekly frequency"
     "ME", "month end frequency"
-    "SM", "semi-month end frequency (15th and end of month)"
+    "SME", "semi-month end frequency (15th and end of month)"
     "BME", "business month end frequency"
     "CBME", "custom business month end frequency"
     "MS", "month start frequency"
diff --git a/doc/source/whatsnew/v0.19.0.rst b/doc/source/whatsnew/v0.19.0.rst
index 32b3ced4f9..1ae7111137 100644
--- a/doc/source/whatsnew/v0.19.0.rst
+++ b/doc/source/whatsnew/v0.19.0.rst
@@ -329,11 +329,13 @@ These provide date offsets anchored (by default) to the 15th and end of month, a
 
 **SemiMonthEnd**:
 
-.. ipython:: python
+.. code-block:: python
 
-   pd.Timestamp("2016-01-01") + SemiMonthEnd()
+   In [46]: pd.Timestamp("2016-01-01") + SemiMonthEnd()
+   Out[46]: Timestamp('2016-01-15 00:00:00')
 
-   pd.date_range("2015-01-01", freq="SM", periods=4)
+   In [47]: pd.date_range("2015-01-01", freq="SM", periods=4)
+   Out[47]: DatetimeIndex(['2015-01-15', '2015-01-31', '2015-02-15', '2015-02-28'], dtype='datetime64[ns]', freq='SM-15')
 
 **SemiMonthBegin**:
 
@@ -345,11 +347,13 @@ These provide date offsets anchored (by default) to the 15th and end of month, a
 
 Using the anchoring suffix, you can also specify the day of month to use instead of the 15th.
 
-.. ipython:: python
+.. code-block:: python
 
-   pd.date_range("2015-01-01", freq="SMS-16", periods=4)
+   In [50]: pd.date_range("2015-01-01", freq="SMS-16", periods=4)
+   Out[50]: DatetimeIndex(['2015-01-01', '2015-01-16', '2015-02-01', '2015-02-16'], dtype='datetime64[ns]', freq='SMS-16')
 
-   pd.date_range("2015-01-01", freq="SM-14", periods=4)
+   In [51]: pd.date_range("2015-01-01", freq="SM-14", periods=4)
+   Out[51]: DatetimeIndex(['2015-01-14', '2015-01-31', '2015-02-14', '2015-02-28'], dtype='datetime64[ns]', freq='SM-14')
 
 .. _whatsnew_0190.enhancements.index:
 
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index 16cfc3efc1..56a6d4581d 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -233,13 +233,17 @@ Other API changes
 Deprecations
 ~~~~~~~~~~~~
 
-Deprecate aliases ``M``, ``Q``, and ``Y`` in favour of ``ME``, ``QE``, and ``YE`` for offsets
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Deprecate aliases ``M``, ``SM``, ``BM``, ``CBM``, ``Q``, ``BQ``, and ``Y`` in favour of ``ME``, ``SME``, ``BME``, ``CBME``, ``QE``, ``BQE``, and ``YE`` for offsets
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 Deprecated the following frequency aliases (:issue:`9586`):
 
 - ``M`` (month end) has been renamed ``ME`` for offsets
+- ``SM`` (semi month end) has been renamed ``SME`` for offsets
+- ``BM`` (business month end) has been renamed ``BME`` for offsets
+- ``CBM`` (custom business month end) has been renamed ``CBME`` for offsets
 - ``Q`` (quarter end) has been renamed ``QE`` for offsets
+- ``BQ`` (business quarter end) has been renamed ``BQE`` for offsets
 - ``Y`` (year end) has been renamed ``YE`` for offsets
 
 For example:
@@ -291,7 +295,7 @@ Other Deprecations
 - 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 ``BM``, ``CBM``, and ``SM`` denoting frequencies in :class:`BusinessMonthEnd`, :class:`CustomBusinessMonthEnd, :class:`SemiMonthEnd` (: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`)
 - Deprecated strings ``H``, ``T``, ``S``, ``L``, ``U``, and ``N`` denoting units in :class:`Timedelta` (:issue:`52536`)
diff --git a/pandas/_libs/tslibs/dtypes.pyx b/pandas/_libs/tslibs/dtypes.pyx
index 75558ea738..ffb1afe872 100644
--- a/pandas/_libs/tslibs/dtypes.pyx
+++ b/pandas/_libs/tslibs/dtypes.pyx
@@ -175,6 +175,7 @@ OFFSET_TO_PERIOD_FREQSTR: dict = {
     "WEEKDAY": "D",
     "EOM": "M",
     "BME": "M",
+    "SME": "M",
     "BQS": "Q",
     "QS": "Q",
     "BQE": "Q",
@@ -275,6 +276,7 @@ cdef dict c_OFFSET_DEPR_FREQSTR = {
     "A-NOV": "YE-NOV",
     "BM": "BME",
     "CBM": "CBME",
+    "SM": "SME",
     "BQ": "BQE",
     "BQ-DEC": "BQE-DEC",
     "BQ-JAN": "BQE-JAN",
@@ -358,8 +360,6 @@ cdef dict c_DEPR_ABBREVS = {
     "BAS-SEP": "BYS-SEP",
     "BAS-OCT": "BYS-OCT",
     "BAS-NOV": "BYS-NOV",
-    "BM": "BME",
-    "CBM": "CBME",
     "H": "h",
     "BH": "bh",
     "CBH": "cbh",
diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx
index 69156ca561..b79b8b2311 100644
--- a/pandas/_libs/tslibs/offsets.pyx
+++ b/pandas/_libs/tslibs/offsets.pyx
@@ -3129,7 +3129,7 @@ cdef class SemiMonthEnd(SemiMonthOffset):
     >>> pd.offsets.SemiMonthEnd().rollforward(ts)
     Timestamp('2022-01-15 00:00:00')
     """
-    _prefix = "SM"
+    _prefix = "SME"
     _min_day_of_month = 1
 
     def is_on_offset(self, dt: datetime) -> bool:
@@ -4549,7 +4549,7 @@ prefix_mapping = {
         MonthEnd,  # 'ME'
         MonthBegin,  # 'MS'
         Nano,  # 'ns'
-        SemiMonthEnd,  # 'SM'
+        SemiMonthEnd,  # 'SME'
         SemiMonthBegin,  # 'SMS'
         Week,  # 'W'
         Second,  # 's'


Test patch:
diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py
index 4043faada4..80b5880257 100644
--- a/pandas/tests/indexes/datetimes/test_date_range.py
+++ b/pandas/tests/indexes/datetimes/test_date_range.py
@@ -145,12 +145,20 @@ class TestDateRanges:
         exp = date_range("1/1/2000", periods=10)
         tm.assert_index_equal(rng, exp)
 
-    def test_date_range_frequency_M_deprecated(self):
-        depr_msg = "'M' is deprecated, please use 'ME' instead."
+    @pytest.mark.parametrize(
+        "freq,freq_depr",
+        [
+            ("2ME", "2M"),
+            ("2SME", "2SM"),
+        ],
+    )
+    def test_date_range_frequency_M_SM_deprecated(self, freq, freq_depr):
+        # GH#52064
+        depr_msg = f"'{freq_depr[1:]}' is deprecated, please use '{freq[1:]}' instead."
 
-        expected = date_range("1/1/2000", periods=4, freq="2ME")
+        expected = date_range("1/1/2000", periods=4, freq=freq)
         with tm.assert_produces_warning(FutureWarning, match=depr_msg):
-            result = date_range("1/1/2000", periods=4, freq="2M")
+            result = date_range("1/1/2000", periods=4, freq=freq_depr)
         tm.assert_index_equal(result, expected)
 
     def test_date_range_tuple_freq_raises(self):
@@ -1624,8 +1632,8 @@ class TestDateRangeNonTickFreq:
             datetime(2008, 12, 31),
         ]
         # ensure generating a range with DatetimeIndex gives same result
-        result = date_range(start=dates[0], end=dates[-1], freq="SM", unit=unit)
-        exp = DatetimeIndex(dates, dtype=f"M8[{unit}]", freq="SM")
+        result = date_range(start=dates[0], end=dates[-1], freq="SME", unit=unit)
+        exp = DatetimeIndex(dates, dtype=f"M8[{unit}]", freq="SME")
         tm.assert_index_equal(result, exp)
 
     def test_date_range_week_of_month(self, unit):
diff --git a/pandas/tests/indexes/period/test_period.py b/pandas/tests/indexes/period/test_period.py
index fa85b7ea45..7b7baab112 100644
--- a/pandas/tests/indexes/period/test_period.py
+++ b/pandas/tests/indexes/period/test_period.py
@@ -281,11 +281,19 @@ class TestPeriodIndex:
         exp = Index([x.ordinal for x in index])
         tm.assert_index_equal(result, exp)
 
-    def test_period_index_frequency_ME_error_message(self):
-        msg = "for Period, please use 'M' instead of 'ME'"
+    @pytest.mark.parametrize(
+        "freq,freq_depr",
+        [
+            ("2M", "2ME"),
+            ("2SM", "2SME"),
+        ],
+    )
+    def test_period_index_frequency_ME_SME_error_message(self, freq, freq_depr):
+        # GH#52064
+        msg = f"for Period, please use '{freq[1:]}' instead of '{freq_depr[1:]}'"
 
         with pytest.raises(ValueError, match=msg):
-            PeriodIndex(["2020-01-01", "2020-01-02"], freq="2ME")
+            PeriodIndex(["2020-01-01", "2020-01-02"], freq=freq_depr)
 
     def test_H_deprecated_from_time_series(self):
         # GH#52536
diff --git a/pandas/tests/tslibs/test_to_offset.py b/pandas/tests/tslibs/test_to_offset.py
index 3fb205f21a..ef68408305 100644
--- a/pandas/tests/tslibs/test_to_offset.py
+++ b/pandas/tests/tslibs/test_to_offset.py
@@ -26,8 +26,8 @@ from pandas._libs.tslibs import (
         ("1s0.25ms", offsets.Micro(1000250)),
         ("1s0.25ms", offsets.Micro(1000250)),
         ("2800ns", offsets.Nano(2800)),
-        ("2SM", offsets.SemiMonthEnd(2)),
-        ("2SM-16", offsets.SemiMonthEnd(2, day_of_month=16)),
+        ("2SME", offsets.SemiMonthEnd(2)),
+        ("2SME-16", offsets.SemiMonthEnd(2, day_of_month=16)),
         ("2SMS-14", offsets.SemiMonthBegin(2, day_of_month=14)),
         ("2SMS-15", offsets.SemiMonthBegin(2)),
     ],
@@ -38,7 +38,7 @@ def test_to_offset(freq_input, expected):
 
 
 @pytest.mark.parametrize(
-    "freqstr,expected", [("-1s", -1), ("-2SM", -2), ("-1SMS", -1), ("-5min10s", -310)]
+    "freqstr,expected", [("-1s", -1), ("-2SME", -2), ("-1SMS", -1), ("-5min10s", -310)]
 )
 def test_to_offset_negative(freqstr, expected):
     result = to_offset(freqstr)
@@ -66,12 +66,12 @@ def test_to_offset_negative(freqstr, expected):
         "+d",
         "-m",
         # Invalid shortcut anchors.
-        "SM-0",
-        "SM-28",
-        "SM-29",
-        "SM-FOO",
+        "SME-0",
+        "SME-28",
+        "SME-29",
+        "SME-FOO",
         "BSM",
-        "SM--1",
+        "SME--1",
         "SMS-1",
         "SMS-28",
         "SMS-30",
@@ -161,10 +161,10 @@ def test_to_offset_pd_timedelta(kwargs, expected):
         ("QE", offsets.QuarterEnd(startingMonth=12)),
         ("QE-DEC", offsets.QuarterEnd(startingMonth=12)),
         ("QE-MAY", offsets.QuarterEnd(startingMonth=5)),
-        ("SM", offsets.SemiMonthEnd(day_of_month=15)),
-        ("SM-15", offsets.SemiMonthEnd(day_of_month=15)),
-        ("SM-1", offsets.SemiMonthEnd(day_of_month=1)),
-        ("SM-27", offsets.SemiMonthEnd(day_of_month=27)),
+        ("SME", offsets.SemiMonthEnd(day_of_month=15)),
+        ("SME-15", offsets.SemiMonthEnd(day_of_month=15)),
+        ("SME-1", offsets.SemiMonthEnd(day_of_month=1)),
+        ("SME-27", offsets.SemiMonthEnd(day_of_month=27)),
         ("SMS-2", offsets.SemiMonthBegin(day_of_month=2)),
         ("SMS-27", offsets.SemiMonthBegin(day_of_month=27)),
     ],