Skip to content
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ Categorical
same type as if one used the :meth:`.str.` / :meth:`.dt.` on a :class:`Series` of that type. E.g. when accessing :meth:`Series.dt.tz_localize` on a
:class:`Categorical` with duplicate entries, the accessor was skipping duplicates (:issue:`27952`)
- Bug in :meth:`DataFrame.replace` and :meth:`Series.replace` that would give incorrect results on categorical data (:issue:`26988`)
- Bug where calling :meth:`Categorical.min` or :meth:`Categorical.max` on an empty Categorical would raise a numpy exception (:issue:`30227`)


Datetimelike
Expand Down
18 changes: 17 additions & 1 deletion pandas/core/arrays/categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from pandas.core.dtypes.dtypes import CategoricalDtype
from pandas.core.dtypes.generic import ABCIndexClass, ABCSeries
from pandas.core.dtypes.inference import is_hashable
from pandas.core.dtypes.missing import isna, notna
from pandas.core.dtypes.missing import isna, na_value_for_dtype, notna

from pandas._typing import ArrayLike, Dtype, Ordered
from pandas.core import ops
Expand Down Expand Up @@ -2116,6 +2116,10 @@ def min(self, skipna=True):

Only ordered `Categoricals` have a minimum!

.. versionchanged:: 1.0.0

Returns an NA value on empty arrays

Raises
------
TypeError
Expand All @@ -2126,6 +2130,10 @@ def min(self, skipna=True):
min : the minimum of this `Categorical`
"""
self.check_for_ordered("min")

if not len(self._codes):
return self.dtype.na_value

good = self._codes != -1
if not good.all():
if skipna:
Expand All @@ -2143,6 +2151,10 @@ def max(self, skipna=True):

Only ordered `Categoricals` have a maximum!

.. versionchanged:: 1.0.0

Returns an NA value on empty arrays

Raises
------
TypeError
Expand All @@ -2153,6 +2165,10 @@ def max(self, skipna=True):
max : the maximum of this `Categorical`
"""
self.check_for_ordered("max")

if not len(self._codes):
return self.dtype.na_value

good = self._codes != -1
if not good.all():
if skipna:
Expand Down
24 changes: 21 additions & 3 deletions pandas/tests/arrays/categorical/test_analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@

from pandas.compat import PYPY

from pandas import Categorical, Index, Series
from pandas import Categorical, Index, Series, date_range
from pandas.api.types import is_scalar
import pandas.util.testing as tm


class TestCategoricalAnalytics:
def test_min_max(self):

def test_min_max_not_ordered(self):
# unordered cats have no min/max
cat = Categorical(["a", "b", "c", "d"], ordered=False)
msg = "Categorical is not ordered for operation {}"
Expand All @@ -21,6 +20,7 @@ def test_min_max(self):
with pytest.raises(TypeError, match=msg.format("max")):
cat.max()

def test_min_max_ordered(self):
cat = Categorical(["a", "b", "c", "d"], ordered=True)
_min = cat.min()
_max = cat.max()
Expand All @@ -35,6 +35,24 @@ def test_min_max(self):
assert _min == "d"
assert _max == "a"

@pytest.mark.parametrize(
"categories,expected",
[
(list("ABC"), np.NaN),
([1, 2, 3], np.NaN),
(Series(date_range("2020-01-01", periods=3), dtype="category"), np.NaN),
],
)
def test_min_max_ordered_empty(self, categories, expected):
# GH 30227
cat = Categorical([], categories=list("ABC"), ordered=True)

result = cat.min()
assert result is expected

result = cat.max()
assert result is expected

@pytest.mark.parametrize("skipna", [True, False])
def test_min_max_with_nan(self, skipna):
# GH 25303
Expand Down