Repository: pandas-dev/pandas
Pull request #57815 (2024-03-11 20:24:05)

CLN: remove deprecated classes 'NumericBlock' and 'ObjectBlock'
xref #52817

removed deprecated classes `NumericBlock` and `ObjectBlock`



Patch:
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 3915539093..e43f6fdf9c 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -289,6 +289,7 @@ Bug fixes
 - Fixed bug in :meth:`DataFrame.join` inconsistently setting result index name (:issue:`55815`)
 - Fixed bug in :meth:`DataFrame.to_string` that raised ``StopIteration`` with nested DataFrames. (:issue:`16098`)
 - Fixed bug in :meth:`DataFrame.update` bool dtype being converted to object (:issue:`55509`)
+- Fixed bug in :meth:`DataFrameGroupBy.apply` that was returning a completely empty DataFrame when all return values of ``func`` were ``None`` instead of returning an empty DataFrame with the original columns and dtypes. (:issue:`57775`)
 - Fixed bug in :meth:`Series.diff` allowing non-integer values for the ``periods`` argument. (:issue:`56607`)
 - Fixed bug in :meth:`Series.rank` that doesn't preserve missing values for nullable integers when ``na_option='keep'``. (:issue:`56976`)
 
@@ -393,6 +394,7 @@ Other
 ^^^^^
 - Bug in :class:`DataFrame` when passing a ``dict`` with a NA scalar and ``columns`` that would always return ``np.nan`` (:issue:`57205`)
 - Bug in :func:`tseries.api.guess_datetime_format` would fail to infer time format when "%Y" == "%H%M" (:issue:`57452`)
+- Bug in :func:`unique` on :class:`Index` not always returning :class:`Index` (:issue:`57043`)
 - Bug in :meth:`DataFrame.sort_index` when passing ``axis="columns"`` and ``ignore_index=True`` and ``ascending=False`` not returning a :class:`RangeIndex` columns (:issue:`57293`)
 - Bug in :meth:`DataFrame.where` where using a non-bool type array in the function would return a ``ValueError`` instead of a ``TypeError`` (:issue:`56330`)
 - Bug in Dataframe Interchange Protocol implementation was returning incorrect results for data buffers' associated dtype, for string and datetime columns (:issue:`54781`)
diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py
index 774bbbe246..344314d829 100644
--- a/pandas/core/algorithms.py
+++ b/pandas/core/algorithms.py
@@ -439,6 +439,10 @@ def unique_with_mask(values, mask: npt.NDArray[np.bool_] | None = None):
         # Dispatch to extension dtype's unique.
         return values.unique()
 
+    if isinstance(values, ABCIndex):
+        # Dispatch to Index's unique.
+        return values.unique()
+
     original = values
     hashtable, values = _get_hashtable_algo(values)
 
diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py
index 64f55c1df4..3b20b854b3 100644
--- a/pandas/core/groupby/generic.py
+++ b/pandas/core/groupby/generic.py
@@ -1642,8 +1642,11 @@ class DataFrameGroupBy(GroupBy[DataFrame]):
         first_not_none = next(com.not_none(*values), None)
 
         if first_not_none is None:
-            # GH9684 - All values are None, return an empty frame.
-            return self.obj._constructor()
+            # GH9684 - All values are None, return an empty frame
+            # GH57775 - Ensure that columns and dtypes from original frame are kept.
+            result = self.obj._constructor(columns=data.columns)
+            result = result.astype(data.dtypes)
+            return result
         elif isinstance(first_not_none, DataFrame):
             return self._concat_objects(
                 values,
diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py
index 40d4cabb35..5023a4b8bd 100644
--- a/pandas/core/groupby/groupby.py
+++ b/pandas/core/groupby/groupby.py
@@ -1636,6 +1636,14 @@ class GroupBy(BaseGroupBy[NDFrameT]):
         a    5
         b    2
         dtype: int64
+
+        Example 4: The function passed to ``apply`` returns ``None`` for one of the
+        group. This group is filtered from the result:
+
+        >>> g1.apply(lambda x: None if x.iloc[0, 0] == 3 else x, include_groups=False)
+           B  C
+        0  1  4
+        1  2  6
         """
         if isinstance(func, str):
             if hasattr(self, func):
diff --git a/pandas/core/internals/__init__.py b/pandas/core/internals/__init__.py
index fb14c5ad82..31234fb1f1 100644
--- a/pandas/core/internals/__init__.py
+++ b/pandas/core/internals/__init__.py
@@ -35,8 +35,6 @@ def __getattr__(name: str):
         return create_block_manager_from_blocks
 
     if name in [
-        "NumericBlock",
-        "ObjectBlock",
         "Block",
         "ExtensionBlock",
         "DatetimeTZBlock",
@@ -49,11 +47,7 @@ def __getattr__(name: str):
             # on hard-coding stacklevel
             stacklevel=2,
         )
-        if name == "NumericBlock":
-            from pandas.core.internals.blocks import NumericBlock
-
-            return NumericBlock
-        elif name == "DatetimeTZBlock":
+        if name == "DatetimeTZBlock":
             from pandas.core.internals.blocks import DatetimeTZBlock
 
             return DatetimeTZBlock
@@ -61,13 +55,9 @@ def __getattr__(name: str):
             from pandas.core.internals.blocks import ExtensionBlock
 
             return ExtensionBlock
-        elif name == "Block":
+        else:
             from pandas.core.internals.blocks import Block
 
             return Block
-        else:
-            from pandas.core.internals.blocks import ObjectBlock
-
-            return ObjectBlock
 
     raise AttributeError(f"module 'pandas.core.internals' has no attribute '{name}'")
diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py
index 80c8a1e8ef..aa2c94da6c 100644
--- a/pandas/core/internals/blocks.py
+++ b/pandas/core/internals/blocks.py
@@ -2148,18 +2148,6 @@ class NumpyBlock(Block):
         return kind in "fciub"
 
 
-class NumericBlock(NumpyBlock):
-    # this Block type is kept for backwards-compatibility
-    # TODO(3.0): delete and remove deprecation in __init__.py.
-    __slots__ = ()
-
-
-class ObjectBlock(NumpyBlock):
-    # this Block type is kept for backwards-compatibility
-    # TODO(3.0): delete and remove deprecation in __init__.py.
-    __slots__ = ()
-
-
 class NDArrayBackedExtensionBlock(EABackedBlock):
     """
     Block backed by an NDArrayBackedExtensionArray
diff --git a/pandas/io/sas/sas7bdat.py b/pandas/io/sas/sas7bdat.py
index 49287ddf5f..6a392a0f02 100644
--- a/pandas/io/sas/sas7bdat.py
+++ b/pandas/io/sas/sas7bdat.py
@@ -17,10 +17,7 @@ Reference for binary data compression:
 from __future__ import annotations
 
 from collections import abc
-from datetime import (
-    datetime,
-    timedelta,
-)
+from datetime import datetime
 import sys
 from typing import TYPE_CHECKING
 
@@ -44,7 +41,6 @@ import pandas as pd
 from pandas import (
     DataFrame,
     Timestamp,
-    isna,
 )
 
 from pandas.io.common import get_handle
@@ -55,7 +51,6 @@ if TYPE_CHECKING:
     from pandas._typing import (
         CompressionOptions,
         FilePath,
-        NaTType,
         ReadBuffer,
     )
 
@@ -64,20 +59,6 @@ _unix_origin = Timestamp("1970-01-01")
 _sas_origin = Timestamp("1960-01-01")
 
 
-def _parse_datetime(sas_datetime: float, unit: str) -> datetime | NaTType:
-    if isna(sas_datetime):
-        return pd.NaT
-
-    if unit == "s":
-        return datetime(1960, 1, 1) + timedelta(seconds=sas_datetime)
-
-    elif unit == "d":
-        return datetime(1960, 1, 1) + timedelta(days=sas_datetime)
-
-    else:
-        raise ValueError("unit must be 'd' or 's'")
-
-
 def _convert_datetimes(sas_datetimes: pd.Series, unit: str) -> pd.Series:
     """
     Convert to Timestamp if possible, otherwise to datetime.datetime.
@@ -370,11 +351,6 @@ class SAS7BDATReader(ReaderBase, abc.Iterator):
             raise ValueError("The cached page is too small.")
         return self._cached_page[offset : offset + length]
 
-    def _read_and_convert_header_text(self, offset: int, length: int) -> str | bytes:
-        return self._convert_header_text(
-            self._read_bytes(offset, length).rstrip(b"\x00 ")
-        )
-
     def _parse_metadata(self) -> None:
         done = False
         while not done:
diff --git a/pandas/io/stata.py b/pandas/io/stata.py
index 4f8afd99cb..fe8b4896d0 100644
--- a/pandas/io/stata.py
+++ b/pandas/io/stata.py
@@ -1121,11 +1121,8 @@ class StataReader(StataParser, abc.Iterator):
 
         # State variables for the file
         self._close_file: Callable[[], None] | None = None
-        self._missing_values = False
-        self._can_read_value_labels = False
         self._column_selector_set = False
         self._value_labels_read = False
-        self._data_read = False
         self._dtype: np.dtype | None = None
         self._lines_read = 0
 
@@ -1650,8 +1647,6 @@ the string values returned are correct."""
         # StopIteration.  If reading the whole thing return an empty
         # data frame.
         if (self._nobs == 0) and nrows == 0:
-            self._can_read_value_labels = True
-            self._data_read = True
             data = DataFrame(columns=self._varlist)
             # Apply dtypes correctly
             for i, col in enumerate(data.columns):
@@ -1664,7 +1659,6 @@ the string values returned are correct."""
             return data
 
         if (self._format_version >= 117) and (not self._value_labels_read):
-            self._can_read_value_labels = True
             self._read_strls()
 
         # Read data
@@ -1687,9 +1681,7 @@ the string values returned are correct."""
         )
 
         self._lines_read += read_lines
-        if self._lines_read == self._nobs:
-            self._can_read_value_labels = True
-            self._data_read = True
+
         # if necessary, swap the byte order to native here
         if self._byteorder != self._native_byteorder:
             raw_data = raw_data.byteswap().view(raw_data.dtype.newbyteorder())
diff --git a/pandas/plotting/_matplotlib/converter.py b/pandas/plotting/_matplotlib/converter.py
index 0eb3318ac9..e2121526c1 100644
--- a/pandas/plotting/_matplotlib/converter.py
+++ b/pandas/plotting/_matplotlib/converter.py
@@ -4,7 +4,6 @@ import contextlib
 import datetime as pydt
 from datetime import (
     datetime,
-    timedelta,
     tzinfo,
 )
 import functools
@@ -460,28 +459,6 @@ class MilliSecondLocator(mdates.DateLocator):
         return self.nonsingular(vmin, vmax)
 
 
-def _from_ordinal(x, tz: tzinfo | None = None) -> datetime:
-    ix = int(x)
-    dt = datetime.fromordinal(ix)
-    remainder = float(x) - ix
-    hour, remainder = divmod(24 * remainder, 1)
-    minute, remainder = divmod(60 * remainder, 1)
-    second, remainder = divmod(60 * remainder, 1)
-    microsecond = int(1_000_000 * remainder)
-    if microsecond < 10:
-        microsecond = 0  # compensate for rounding errors
-    dt = datetime(
-        dt.year, dt.month, dt.day, int(hour), int(minute), int(second), microsecond
-    )
-    if tz is not None:
-        dt = dt.astimezone(tz)
-
-    if microsecond > 999990:  # compensate for rounding errors
-        dt += timedelta(microseconds=1_000_000 - microsecond)
-
-    return dt
-
-
 # Fixed frequency dynamic tick locators and formatters
 
 # -------------------------------------------------------------------------


Test patch:
diff --git a/pandas/tests/groupby/test_apply.py b/pandas/tests/groupby/test_apply.py
index dcb73bdba2..9bd2c22788 100644
--- a/pandas/tests/groupby/test_apply.py
+++ b/pandas/tests/groupby/test_apply.py
@@ -838,7 +838,8 @@ def test_groupby_apply_all_none():
     msg = "DataFrameGroupBy.apply operated on the grouping columns"
     with tm.assert_produces_warning(DeprecationWarning, match=msg):
         result = test_df.groupby("groups").apply(test_func)
-    expected = DataFrame()
+    expected = DataFrame(columns=test_df.columns)
+    expected = expected.astype(test_df.dtypes)
     tm.assert_frame_equal(result, expected)
 
 
diff --git a/pandas/tests/internals/test_api.py b/pandas/tests/internals/test_api.py
index 7c1d3ff774..5bff1b7be3 100644
--- a/pandas/tests/internals/test_api.py
+++ b/pandas/tests/internals/test_api.py
@@ -40,8 +40,6 @@ def test_namespace():
 @pytest.mark.parametrize(
     "name",
     [
-        "NumericBlock",
-        "ObjectBlock",
         "Block",
         "ExtensionBlock",
         "DatetimeTZBlock",
@@ -53,11 +51,6 @@ def test_deprecations(name):
     with tm.assert_produces_warning(DeprecationWarning, match=msg):
         getattr(internals, name)
 
-    if name not in ["NumericBlock", "ObjectBlock"]:
-        # NumericBlock and ObjectBlock are not in the internals.api namespace
-        with tm.assert_produces_warning(DeprecationWarning, match=msg):
-            getattr(api, name)
-
 
 def test_make_block_2d_with_dti():
     # GH#41168
diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py
index 2eb44ef477..7164b7a046 100644
--- a/pandas/tests/plotting/test_datetimelike.py
+++ b/pandas/tests/plotting/test_datetimelike.py
@@ -295,27 +295,6 @@ class TestTSPlot:
         ser = Series(np.random.default_rng(2).standard_normal(len(dr)), index=dr)
         _check_plot_works(ser.plot)
 
-    @pytest.mark.xfail(reason="Api changed in 3.6.0")
-    def test_uhf(self):
-        import pandas.plotting._matplotlib.converter as conv
-
-        idx = date_range("2012-6-22 21:59:51.960928", freq="ms", periods=500)
-        df = DataFrame(
-            np.random.default_rng(2).standard_normal((len(idx), 2)), index=idx
-        )
-
-        _, ax = mpl.pyplot.subplots()
-        df.plot(ax=ax)
-        axis = ax.get_xaxis()
-
-        tlocs = axis.get_ticklocs()
-        tlabels = axis.get_ticklabels()
-        for loc, label in zip(tlocs, tlabels):
-            xp = conv._from_ordinal(loc).strftime("%H:%M:%S.%f")
-            rs = str(label.get_text())
-            if len(rs):
-                assert xp == rs
-
     def test_irreg_hf(self):
         idx = date_range("2012-6-22 21:59:51", freq="s", periods=10)
         df = DataFrame(
diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py
index 057a5a6273..365ec452a7 100644
--- a/pandas/tests/test_algos.py
+++ b/pandas/tests/test_algos.py
@@ -16,7 +16,10 @@ from pandas.core.dtypes.common import (
     is_integer_dtype,
     is_object_dtype,
 )
-from pandas.core.dtypes.dtypes import CategoricalDtype
+from pandas.core.dtypes.dtypes import (
+    CategoricalDtype,
+    DatetimeTZDtype,
+)
 
 import pandas as pd
 from pandas import (
@@ -570,19 +573,20 @@ class TestUnique:
         for i in range(1000):
             len(algos.unique(lst))
 
-    def test_on_index_object(self):
-        mindex = MultiIndex.from_arrays(
-            [np.arange(5).repeat(5), np.tile(np.arange(5), 5)]
-        )
-        expected = mindex.values
-        expected.sort()
-
-        mindex = mindex.repeat(2)
+    def test_index_returned(self, index):
+        # GH#57043
+        index = index.repeat(2)
+        result = algos.unique(index)
 
-        result = pd.unique(mindex)
-        result.sort()
-
-        tm.assert_almost_equal(result, expected)
+        # dict.fromkeys preserves the order
+        unique_values = list(dict.fromkeys(index.values))
+        if isinstance(index, MultiIndex):
+            expected = MultiIndex.from_tuples(unique_values, names=index.names)
+        else:
+            expected = Index(unique_values, dtype=index.dtype)
+            if isinstance(index.dtype, DatetimeTZDtype):
+                expected = expected.normalize()
+        tm.assert_index_equal(result, expected, exact=True)
 
     def test_dtype_preservation(self, any_numpy_dtype):
         # GH 15442
@@ -623,7 +627,7 @@ class TestUnique:
 
     def test_datetime64_dtype_array_returned(self):
         # GH 9431
-        expected = np.array(
+        dt_arr = np.array(
             [
                 "2015-01-03T00:00:00.000000000",
                 "2015-01-01T00:00:00.000000000",
@@ -639,18 +643,18 @@ class TestUnique:
             ]
         )
         result = algos.unique(dt_index)
-        tm.assert_numpy_array_equal(result, expected)
-        assert result.dtype == expected.dtype
+        expected = to_datetime(dt_arr)
+        tm.assert_index_equal(result, expected, exact=True)
 
         s = Series(dt_index)
         result = algos.unique(s)
-        tm.assert_numpy_array_equal(result, expected)
-        assert result.dtype == expected.dtype
+        tm.assert_numpy_array_equal(result, dt_arr)
+        assert result.dtype == dt_arr.dtype
 
         arr = s.values
         result = algos.unique(arr)
-        tm.assert_numpy_array_equal(result, expected)
-        assert result.dtype == expected.dtype
+        tm.assert_numpy_array_equal(result, dt_arr)
+        assert result.dtype == dt_arr.dtype
 
     def test_datetime_non_ns(self):
         a = np.array(["2000", "2000", "2001"], dtype="datetime64[s]")
@@ -666,22 +670,23 @@ class TestUnique:
 
     def test_timedelta64_dtype_array_returned(self):
         # GH 9431
-        expected = np.array([31200, 45678, 10000], dtype="m8[ns]")
+        td_arr = np.array([31200, 45678, 10000], dtype="m8[ns]")
 
         td_index = to_timedelta([31200, 45678, 31200, 10000, 45678])
         result = algos.unique(td_index)
-        tm.assert_numpy_array_equal(result, expected)
+        expected = to_timedelta(td_arr)
+        tm.assert_index_equal(result, expected)
         assert result.dtype == expected.dtype
 
         s = Series(td_index)
         result = algos.unique(s)
-        tm.assert_numpy_array_equal(result, expected)
-        assert result.dtype == expected.dtype
+        tm.assert_numpy_array_equal(result, td_arr)
+        assert result.dtype == td_arr.dtype
 
         arr = s.values
         result = algos.unique(arr)
-        tm.assert_numpy_array_equal(result, expected)
-        assert result.dtype == expected.dtype
+        tm.assert_numpy_array_equal(result, td_arr)
+        assert result.dtype == td_arr.dtype
 
     def test_uint64_overflow(self):
         s = Series([1, 2, 2**63, 2**63], dtype=np.uint64)