Repository: pandas-dev/pandas
Pull request #57819 (2024-03-12 06:09:12)

CLN: Remove unused private code in sas module
- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
- [ ] [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).
- [x] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] 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/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/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())


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/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)