Repository: pandas-dev/pandas (python)

Issue:
DEPR: is_decimal, is_interval
These are aliases for isinstance checks, don't seem necessary.


Pull request:
DEPR: is_period, is_interval
- [x] closes #55264 (Replace xxxx with the GitHub issue number)
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


Patch:
diff --git a/asv_bench/benchmarks/groupby.py b/asv_bench/benchmarks/groupby.py
index 5e3eb7c7d4..202ba6e981 100644
--- a/asv_bench/benchmarks/groupby.py
+++ b/asv_bench/benchmarks/groupby.py
@@ -802,6 +802,51 @@ class Categories:
         self.df_extra_cat.groupby("a", observed=observed, sort=False)["b"].count()
 
 
+class MultipleCategories:
+    def setup(self):
+        N = 10**3
+        arr = np.random.random(N)
+        data = {
+            "a1": Categorical(np.random.randint(10000, size=N)),
+            "a2": Categorical(np.random.randint(10000, size=N)),
+            "b": arr,
+        }
+        self.df = DataFrame(data)
+        data = {
+            "a1": Categorical(np.random.randint(10000, size=N), ordered=True),
+            "a2": Categorical(np.random.randint(10000, size=N), ordered=True),
+            "b": arr,
+        }
+        self.df_ordered = DataFrame(data)
+        data = {
+            "a1": Categorical(np.random.randint(100, size=N), categories=np.arange(N)),
+            "a2": Categorical(np.random.randint(100, size=N), categories=np.arange(N)),
+            "b": arr,
+        }
+        self.df_extra_cat = DataFrame(data)
+
+    def time_groupby_sort(self):
+        self.df.groupby(["a1", "a2"], observed=False)["b"].count()
+
+    def time_groupby_nosort(self):
+        self.df.groupby(["a1", "a2"], observed=False, sort=False)["b"].count()
+
+    def time_groupby_ordered_sort(self):
+        self.df_ordered.groupby(["a1", "a2"], observed=False)["b"].count()
+
+    def time_groupby_ordered_nosort(self):
+        self.df_ordered.groupby(["a1", "a2"], observed=False, sort=False)["b"].count()
+
+    def time_groupby_extra_cat_sort(self):
+        self.df_extra_cat.groupby(["a1", "a2"], observed=False)["b"].count()
+
+    def time_groupby_extra_cat_nosort(self):
+        self.df_extra_cat.groupby(["a1", "a2"], observed=False, sort=False)["b"].count()
+
+    def time_groupby_transform(self):
+        self.df_extra_cat.groupby(["a1", "a2"], observed=False)["b"].cumsum()
+
+
 class Datelike:
     # GH 14338
     params = ["period_range", "date_range", "date_range_tz"]
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index 3d34955a3b..a33322aeba 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -262,6 +262,7 @@ For example:
 Other Deprecations
 ^^^^^^^^^^^^^^^^^^
 - Changed :meth:`Timedelta.resolution_string` to return ``h``, ``min``, ``s``, ``ms``, ``us``, and ``ns`` instead of ``H``, ``T``, ``S``, ``L``, ``U``, and ``N``, for compatibility with respective deprecations in frequency aliases (:issue:`52536`)
+- Deprecated :func:`pandas.api.types.is_interval` and :func:`pandas.api.types.is_period`, use ``isinstance(obj, pd.Interval)`` and ``isinstance(obj, pd.Period)`` instead (:issue:`55264`)
 - 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:`Index.format`, use ``index.astype(str)`` or ``index.map(formatter)`` instead (:issue:`55413`)
@@ -446,9 +447,9 @@ Groupby/resample/rolling
 - Bug in :meth:`DataFrame.resample` not respecting ``closed`` and ``label`` arguments for :class:`~pandas.tseries.offsets.BusinessDay` (:issue:`55282`)
 - Bug in :meth:`DataFrame.resample` where bin edges were not correct for :class:`~pandas.tseries.offsets.BusinessDay` (:issue:`55281`)
 - Bug in :meth:`DataFrame.resample` where bin edges were not correct for :class:`~pandas.tseries.offsets.MonthBegin` (:issue:`55271`)
-- Bug in :meth:`DataFrameGroupBy.value_counts` and :meth:`SeriesGroupBy.value_count` could result in incorrect sorting if the columns of the DataFrame or name of the Series are integers (:issue:`56007`)
-- Bug in :meth:`DataFrameGroupBy.value_counts` and :meth:`SeriesGroupBy.value_count` would not respect ``sort=False`` in :meth:`DataFrame.groupby` and :meth:`Series.groupby` (:issue:`56007`)
-- Bug in :meth:`DataFrameGroupBy.value_counts` and :meth:`SeriesGroupBy.value_count` would sort by proportions rather than frequencies when ``sort=True`` and ``normalize=True`` (:issue:`56007`)
+- Bug in :meth:`DataFrameGroupBy.value_counts` and :meth:`SeriesGroupBy.value_count` could result in incorrect sorting if the columns of the DataFrame or name of the Series are integers (:issue:`55951`)
+- Bug in :meth:`DataFrameGroupBy.value_counts` and :meth:`SeriesGroupBy.value_count` would not respect ``sort=False`` in :meth:`DataFrame.groupby` and :meth:`Series.groupby` (:issue:`55951`)
+- Bug in :meth:`DataFrameGroupBy.value_counts` and :meth:`SeriesGroupBy.value_count` would sort by proportions rather than frequencies when ``sort=True`` and ``normalize=True`` (:issue:`55951`)
 
 Reshaping
 ^^^^^^^^^
diff --git a/pandas/_libs/lib.pyx b/pandas/_libs/lib.pyx
index 0b147ab2fd..8493f8bd06 100644
--- a/pandas/_libs/lib.pyx
+++ b/pandas/_libs/lib.pyx
@@ -65,6 +65,7 @@ from numpy cimport (
 )
 
 cnp.import_array()
+from pandas._libs.interval import Interval
 
 
 cdef extern from "pandas/parser/pd_parser.h":
@@ -228,7 +229,7 @@ def is_scalar(val: object) -> bool:
     # Note: PyNumber_Check check includes Decimal, Fraction, numbers.Number
     return (PyNumber_Check(val)
             or is_period_object(val)
-            or is_interval(val)
+            or isinstance(val, Interval)
             or is_offset_object(val))
 
 
@@ -1161,6 +1162,17 @@ cpdef bint is_decimal(object obj):
 
 
 cpdef bint is_interval(object obj):
+    import warnings
+
+    from pandas.util._exceptions import find_stack_level
+
+    warnings.warn(
+        # GH#55264
+        "is_interval is deprecated and will be removed in a future version. "
+        "Use isinstance(obj, pd.Interval) instead.",
+        FutureWarning,
+        stacklevel=find_stack_level(),
+    )
     return getattr(obj, "_typ", "_typ") == "interval"
 
 
@@ -1172,6 +1184,17 @@ def is_period(val: object) -> bool:
     -------
     bool
     """
+    import warnings
+
+    from pandas.util._exceptions import find_stack_level
+
+    warnings.warn(
+        # GH#55264
+        "is_period is deprecated and will be removed in a future version. "
+        "Use isinstance(obj, pd.Period) instead.",
+        FutureWarning,
+        stacklevel=find_stack_level(),
+    )
     return is_period_object(val)
 
 
@@ -1694,7 +1717,7 @@ def infer_dtype(value: object, skipna: bool = True) -> str:
         if is_period_array(values, skipna=skipna):
             return "period"
 
-    elif is_interval(val):
+    elif isinstance(val, Interval):
         if is_interval_array(values):
             return "interval"
 
@@ -2169,7 +2192,7 @@ cpdef bint is_interval_array(ndarray values):
     for i in range(n):
         val = values[i]
 
-        if is_interval(val):
+        if isinstance(val, Interval):
             if closed is None:
                 closed = val.closed
                 numeric = (
@@ -2630,7 +2653,7 @@ def maybe_convert_objects(ndarray[object] objects,
             except (ValueError, TypeError):
                 seen.object_ = True
                 break
-        elif is_interval(val):
+        elif isinstance(val, Interval):
             if convert_non_numeric:
                 seen.interval_ = True
                 break
diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx
index 699b8749e7..69156ca561 100644
--- a/pandas/_libs/tslibs/offsets.pyx
+++ b/pandas/_libs/tslibs/offsets.pyx
@@ -101,12 +101,6 @@ cdef bint is_tick_object(object obj):
     return isinstance(obj, Tick)
 
 
-cdef datetime _as_datetime(datetime obj):
-    if isinstance(obj, _Timestamp):
-        return obj.to_pydatetime()
-    return obj
-
-
 cdef bint _is_normalized(datetime dt):
     if dt.hour != 0 or dt.minute != 0 or dt.second != 0 or dt.microsecond != 0:
         # Regardless of whether dt is datetime vs Timestamp
@@ -1322,7 +1316,7 @@ cdef class RelativeDeltaOffset(BaseOffset):
         if self._use_relativedelta:
             if isinstance(other, _Timestamp):
                 other_nanos = other.nanosecond
-            other = _as_datetime(other)
+                other = other.to_pydatetime(warn=False)
 
         if len(self.kwds) > 0:
             tzinfo = getattr(other, "tzinfo", None)
diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py
index 9079e33e08..0aa2078f6a 100644
--- a/pandas/core/arrays/datetimes.py
+++ b/pandas/core/arrays/datetimes.py
@@ -2790,13 +2790,7 @@ def _generate_range(
                 break
 
             # faster than cur + offset
-            with warnings.catch_warnings():
-                warnings.filterwarnings(
-                    "ignore",
-                    "Discarding nonzero nanoseconds in conversion",
-                    category=UserWarning,
-                )
-                next_date = offset._apply(cur)
+            next_date = offset._apply(cur)
             next_date = next_date.as_unit(unit)
             if next_date <= cur:
                 raise ValueError(f"Offset {offset} did not increment date")
@@ -2811,13 +2805,7 @@ def _generate_range(
                 break
 
             # faster than cur + offset
-            with warnings.catch_warnings():
-                warnings.filterwarnings(
-                    "ignore",
-                    "Discarding nonzero nanoseconds in conversion",
-                    category=UserWarning,
-                )
-                next_date = offset._apply(cur)
+            next_date = offset._apply(cur)
             next_date = next_date.as_unit(unit)
             if next_date >= cur:
                 raise ValueError(f"Offset {offset} did not decrement date")
diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py
index 3cac25600b..0ad652bd5a 100644
--- a/pandas/core/dtypes/cast.py
+++ b/pandas/core/dtypes/cast.py
@@ -20,7 +20,11 @@ import numpy as np
 
 from pandas._config import using_pyarrow_string_dtype
 
-from pandas._libs import lib
+from pandas._libs import (
+    Interval,
+    Period,
+    lib,
+)
 from pandas._libs.missing import (
     NA,
     NAType,
@@ -850,9 +854,9 @@ def infer_dtype_from_scalar(val) -> tuple[DtypeObj, Any]:
     elif is_complex(val):
         dtype = np.dtype(np.complex128)
 
-    if lib.is_period(val):
+    if isinstance(val, Period):
         dtype = PeriodDtype(freq=val.freq)
-    elif lib.is_interval(val):
+    elif isinstance(val, Interval):
         subtype = infer_dtype_from_scalar(val.left)[0]
         dtype = IntervalDtype(subtype=subtype, closed=val.closed)
 
diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py
index c17badccfb..2d530b64e1 100644
--- a/pandas/core/groupby/groupby.py
+++ b/pandas/core/groupby/groupby.py
@@ -2834,7 +2834,7 @@ class GroupBy(BaseGroupBy[NDFrameT]):
         if self.sort:
             # Sort by the groupings
             names = result_series.index.names
-            # GH#56007 - Temporarily replace names in case they are integers
+            # GH#55951 - Temporarily replace names in case they are integers
             result_series.index.names = range(len(names))
             index_level = list(range(len(self.grouper.groupings)))
             result_series = result_series.sort_index(
diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py
index f73e2b5512..0982b376a2 100644
--- a/pandas/core/indexes/interval.py
+++ b/pandas/core/indexes/interval.py
@@ -22,6 +22,7 @@ from pandas._libs.interval import (
 )
 from pandas._libs.tslibs import (
     BaseOffset,
+    Period,
     Timedelta,
     Timestamp,
     to_offset,
@@ -561,7 +562,7 @@ class IntervalIndex(ExtensionIndex):
         if scalar:
             # Timestamp/Timedelta
             key_dtype, key_i8 = infer_dtype_from_scalar(key)
-            if lib.is_period(key):
+            if isinstance(key, Period):
                 key_i8 = key.ordinal
             elif isinstance(key_i8, Timestamp):
                 key_i8 = key_i8._value


Test patch:
diff --git a/pandas/tests/dtypes/test_inference.py b/pandas/tests/dtypes/test_inference.py
index df7c787d2b..32c8def669 100644
--- a/pandas/tests/dtypes/test_inference.py
+++ b/pandas/tests/dtypes/test_inference.py
@@ -1622,11 +1622,23 @@ class TestTypeInference:
         tm.assert_numpy_array_equal(out, expected)
 
     def test_is_period(self):
-        assert lib.is_period(Period("2011-01", freq="M"))
-        assert not lib.is_period(PeriodIndex(["2011-01"], freq="M"))
-        assert not lib.is_period(Timestamp("2011-01"))
-        assert not lib.is_period(1)
-        assert not lib.is_period(np.nan)
+        # GH#55264
+        msg = "is_period is deprecated and will be removed in a future version"
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            assert lib.is_period(Period("2011-01", freq="M"))
+            assert not lib.is_period(PeriodIndex(["2011-01"], freq="M"))
+            assert not lib.is_period(Timestamp("2011-01"))
+            assert not lib.is_period(1)
+            assert not lib.is_period(np.nan)
+
+    def test_is_interval(self):
+        # GH#55264
+        msg = "is_interval is deprecated and will be removed in a future version"
+        item = Interval(1, 2)
+        with tm.assert_produces_warning(FutureWarning, match=msg):
+            assert lib.is_interval(item)
+            assert not lib.is_interval(pd.IntervalIndex([item]))
+            assert not lib.is_interval(pd.IntervalIndex([item])._engine)
 
     def test_categorical(self):
         # GH 8974
diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py
index f360d2d239..bc20e840b7 100644
--- a/pandas/tests/tseries/offsets/test_offsets.py
+++ b/pandas/tests/tseries/offsets/test_offsets.py
@@ -229,18 +229,9 @@ class TestCommon:
         assert result == expected
 
         # see gh-14101
-        exp_warning = None
         ts = Timestamp(dt) + Nano(5)
-
-        if (
-            type(offset_s).__name__ == "DateOffset"
-            and (funcname in ["apply", "_apply"] or normalize)
-            and ts.nanosecond > 0
-        ):
-            exp_warning = UserWarning
-
         # test nanosecond is preserved
-        with tm.assert_produces_warning(exp_warning):
+        with tm.assert_produces_warning(None):
             result = func(ts)
 
         assert isinstance(result, Timestamp)
@@ -274,18 +265,9 @@ class TestCommon:
             assert result == expected_localize
 
             # see gh-14101
-            exp_warning = None
             ts = Timestamp(dt, tz=tz) + Nano(5)
-
-            if (
-                type(offset_s).__name__ == "DateOffset"
-                and (funcname in ["apply", "_apply"] or normalize)
-                and ts.nanosecond > 0
-            ):
-                exp_warning = UserWarning
-
             # test nanosecond is preserved
-            with tm.assert_produces_warning(exp_warning):
+            with tm.assert_produces_warning(None):
                 result = func(ts)
             assert isinstance(result, Timestamp)
             if normalize is False:
@@ -1010,6 +992,14 @@ def test_dateoffset_add_sub_timestamp_with_nano():
     result = offset + ts
     assert result == expected
 
+    offset2 = DateOffset(minutes=2, nanoseconds=9, hour=1)
+    assert offset2._use_relativedelta
+    with tm.assert_produces_warning(None):
+        # no warning about Discarding nonzero nanoseconds
+        result2 = ts + offset2
+    expected2 = Timestamp("1970-01-01 01:02:00.000000013")
+    assert result2 == expected2
+
 
 @pytest.mark.parametrize(
     "attribute",