Skip to content

Commit e757e8a

Browse files
committed
DEPR: No NaNs in categories
1 parent 30f672c commit e757e8a

File tree

5 files changed

+129
-76
lines changed

5 files changed

+129
-76
lines changed

asv_bench/benchmarks/categoricals.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .pandas_vb_common import *
2-
2+
import string
33

44
class concat_categorical(object):
55
goal_time = 0.2
@@ -25,3 +25,21 @@ def time_value_counts(self):
2525

2626
def time_value_counts_dropna(self):
2727
self.ts.value_counts(dropna=True)
28+
29+
class categorical_constructor(object):
30+
goal_time = 0.2
31+
32+
def setup(self):
33+
n = 5
34+
N = 1e6
35+
self.categories = list(string.ascii_letters[:n])
36+
self.cat_idx = Index(self.categories)
37+
self.values = np.tile(self.categories, N)
38+
self.codes = np.tile(range(n), N)
39+
40+
def time_regular_constructor(self):
41+
Categorical(self.values, self.categories)
42+
43+
def time_fastpath(self):
44+
Categorical(self.codes, self.cat_idx, fastpath=True)
45+

doc/source/categorical.rst

+13-16
Original file line numberDiff line numberDiff line change
@@ -632,41 +632,35 @@ Missing Data
632632

633633
pandas primarily uses the value `np.nan` to represent missing data. It is by
634634
default not included in computations. See the :ref:`Missing Data section
635-
<missing_data>`
635+
<missing_data>`.
636636

637-
There are two ways a `np.nan` can be represented in categorical data: either the value is not
638-
available ("missing value") or `np.nan` is a valid category.
637+
Missing values should **not** be included in the Categorical's ``categories``,
638+
only in the ``values``.
639+
Instead, it is understood that NaN is different, and is always a possibility.
640+
When working with the Categorical's ``codes``, missing values will always have
641+
a code of ``-1``.
639642

640643
.. ipython:: python
641644
642645
s = pd.Series(["a","b",np.nan,"a"], dtype="category")
643646
# only two categories
644647
s
645-
s2 = pd.Series(["a","b","c","a"], dtype="category")
646-
s2.cat.categories = [1,2,np.nan]
647-
# three categories, np.nan included
648-
s2
648+
s.codes
649649
650-
.. note::
651-
As integer `Series` can't include NaN, the categories were converted to `object`.
652650
653-
.. note::
654-
Missing value methods like ``isnull`` and ``fillna`` will take both missing values as well as
655-
`np.nan` categories into account:
651+
Methods for working with missing data, e.g. :meth:`~Series.isnull`, :meth:`~Series.fillna`,
652+
:meth:`~Series.dropna`, all work normally:
656653

657654
.. ipython:: python
658655
659656
c = pd.Series(["a","b",np.nan], dtype="category")
660-
c.cat.set_categories(["a","b",np.nan], inplace=True)
661-
# will be inserted as a NA category:
662-
c[0] = np.nan
663657
s = pd.Series(c)
664658
s
665659
pd.isnull(s)
666660
s.fillna("a")
667661
668662
Differences to R's `factor`
669-
~~~~~~~~~~~~~~~~~~~~~~~~~~~
663+
---------------------------
670664

671665
The following differences to R's factor functions can be observed:
672666

@@ -677,6 +671,9 @@ The following differences to R's factor functions can be observed:
677671
* In contrast to R's `factor` function, using categorical data as the sole input to create a
678672
new categorical series will *not* remove unused categories but create a new categorical series
679673
which is equal to the passed in one!
674+
* R allows for missing values to be included in its `levels` (pandas' `categories`). Pandas
675+
does not allow `NaN` categories, but missing values can still be in the `values`.
676+
680677

681678
Gotchas
682679
-------

doc/source/whatsnew/v0.17.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,7 @@ Deprecations
652652
===================== =================================
653653

654654
- ``Categorical.name`` was deprecated to make ``Categorical`` more ``numpy.ndarray`` like. Use ``Series(cat, name="whatever")`` instead (:issue:`10482`).
655+
- Setting missing values (NaN) in a ``Categorical``'s ``categories`` will issue a warning (:issue:`10748`). You can still have missing values in the ``values``.
655656
- ``drop_duplicates`` and ``duplicated``'s ``take_last`` keyword was deprecated in favor of ``keep``. (:issue:`6511`, :issue:`8505`)
656657
- ``Series.nsmallest`` and ``nlargest``'s ``take_last`` keyword was deprecated in favor of ``keep``. (:issue:`10792`)
657658
- ``DataFrame.combineAdd`` and ``DataFrame.combineMult`` are deprecated. They

pandas/core/categorical.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -443,12 +443,18 @@ def _validate_categories(cls, categories):
443443
raise ValueError('Categorical categories must be unique')
444444
return categories
445445

446-
def _set_categories(self, categories):
446+
def _set_categories(self, categories, validate=True):
447447
""" Sets new categories """
448-
categories = self._validate_categories(categories)
449-
if not self._categories is None and len(categories) != len(self._categories):
450-
raise ValueError("new categories need to have the same number of items than the old "
451-
"categories!")
448+
if validate:
449+
categories = self._validate_categories(categories)
450+
if not self._categories is None and len(categories) != len(self._categories):
451+
raise ValueError("new categories need to have the same number of items than the old "
452+
"categories!")
453+
if np.any(isnull(categories)):
454+
# NaNs in cats deprecated in 0.17, remove in 0.18 or 0.19 GH 10748
455+
msg = ('\nSetting NaNs in `categories` is deprecated and '
456+
'will be removed in a future version of pandas.')
457+
warn(msg, FutureWarning, stacklevel=9)
452458
self._categories = categories
453459

454460
def _get_categories(self):
@@ -581,11 +587,11 @@ def set_categories(self, new_categories, ordered=None, rename=False, inplace=Fal
581587
if not cat._categories is None and len(new_categories) < len(cat._categories):
582588
# remove all _codes which are larger and set to -1/NaN
583589
self._codes[self._codes >= len(new_categories)] = -1
584-
cat._categories = new_categories
590+
cat._set_categories(new_categories, validate=False)
585591
else:
586592
values = cat.__array__()
587593
cat._codes = _get_codes_for_values(values, new_categories)
588-
cat._categories = new_categories
594+
cat._set_categories(new_categories, validate=False)
589595

590596
if ordered is None:
591597
ordered = self.ordered
@@ -708,7 +714,7 @@ def add_categories(self, new_categories, inplace=False):
708714
new_categories = list(self._categories) + list(new_categories)
709715
new_categories = self._validate_categories(new_categories)
710716
cat = self if inplace else self.copy()
711-
cat._categories = new_categories
717+
cat._set_categories(new_categories, validate=False)
712718
cat._codes = _coerce_indexer_dtype(cat._codes, new_categories)
713719
if not inplace:
714720
return cat
@@ -791,7 +797,7 @@ def remove_unused_categories(self, inplace=False):
791797
from pandas.core.index import _ensure_index
792798
new_categories = _ensure_index(new_categories)
793799
cat._codes = _get_codes_for_values(cat.__array__(), new_categories)
794-
cat._categories = new_categories
800+
cat._set_categories(new_categories, validate=False)
795801
if not inplace:
796802
return cat
797803

@@ -1171,7 +1177,7 @@ def order(self, inplace=False, ascending=True, na_position='last'):
11711177
Category.sort
11721178
"""
11731179
warn("order is deprecated, use sort_values(...)",
1174-
FutureWarning, stacklevel=2)
1180+
FutureWarning, stacklevel=3)
11751181
return self.sort_values(inplace=inplace, ascending=ascending, na_position=na_position)
11761182

11771183
def sort(self, inplace=True, ascending=True, na_position='last'):

0 commit comments

Comments
 (0)