1from __future__ import annotations
2
3from csv import QUOTE_NONNUMERIC
4from functools import partial
5import itertools
6import operator
7from shutil import get_terminal_size
8from typing import (
9 TYPE_CHECKING,
10 Literal,
11 Self,
12 cast,
13 overload,
14)
15import warnings
16
17import numpy as np
18
19from pandas._config import get_option
20
21from pandas._libs import (
22 NaT,
23 algos as libalgos,
24 lib,
25)
26from pandas._libs.arrays import NDArrayBacked
27from pandas.compat.numpy import function as nv
28from pandas.errors import Pandas4Warning
29from pandas.util._decorators import set_module
30from pandas.util._exceptions import find_stack_level
31from pandas.util._validators import validate_bool_kwarg
32
33from pandas.core.dtypes.cast import (
34 coerce_indexer_dtype,
35 find_common_type,
36)
37from pandas.core.dtypes.common import (
38 ensure_int64,
39 ensure_platform_int,
40 is_any_real_numeric_dtype,
41 is_bool_dtype,
42 is_dict_like,
43 is_hashable,
44 is_integer_dtype,
45 is_list_like,
46 is_scalar,
47 needs_i8_conversion,
48 pandas_dtype,
49)
50from pandas.core.dtypes.dtypes import (
51 ArrowDtype,
52 CategoricalDtype,
53 CategoricalDtypeType,
54 ExtensionDtype,
55)
56from pandas.core.dtypes.generic import (
57 ABCIndex,
58 ABCSeries,
59)
60from pandas.core.dtypes.missing import (
61 is_valid_na_for_dtype,
62 isna,
63)
64
65from pandas.core import (
66 algorithms,
67 arraylike,
68 ops,
69)
70from pandas.core.accessor import (
71 PandasDelegate,
72 delegate_names,
73)
74from pandas.core.algorithms import (
75 factorize,
76 take_nd,
77)
78from pandas.core.arrays._mixins import (
79 NDArrayBackedExtensionArray,
80 ravel_compat,
81)
82from pandas.core.base import (
83 ExtensionArray,
84 NoNewAttributesMixin,
85 PandasObject,
86)
87import pandas.core.common as com
88from pandas.core.construction import (
89 extract_array,
90 sanitize_array,
91)
92from pandas.core.ops.common import unpack_zerodim_and_defer
93from pandas.core.sorting import nargsort
94from pandas.core.strings.object_array import ObjectStringArrayMixin
95
96from pandas.io.formats import console
97
98if TYPE_CHECKING:
99 from collections.abc import (
100 Callable,
101 Hashable,
102 Iterator,
103 Sequence,
104 )
105
106 from pandas._typing import (
107 ArrayLike,
108 AstypeArg,
109 AxisInt,
110 Dtype,
111 NpDtype,
112 Ordered,
113 Shape,
114 SortKind,
115 npt,
116 )
117
118 from pandas import (
119 DataFrame,
120 Index,
121 Series,
122 )
123
124
125def _cat_compare_op(op):
126 opname = f"__{op.__name__}__"
127 fill_value = op is operator.ne
128
129 @unpack_zerodim_and_defer(opname)
130 def func(self, other):
131 hashable = is_hashable(other)
132 if is_list_like(other) and len(other) != len(self) and not hashable:
133 # in hashable case we may have a tuple that is itself a category
134 raise ValueError("Lengths must match.")
135
136 if not self.ordered:
137 if opname in ["__lt__", "__gt__", "__le__", "__ge__"]:
138 raise TypeError(
139 "Unordered Categoricals can only compare equality or not"
140 )
141 if isinstance(other, Categorical):
142 # Two Categoricals can only be compared if the categories are
143 # the same (maybe up to ordering, depending on ordered)
144
145 msg = "Categoricals can only be compared if 'categories' are the same."
146 if not self._categories_match_up_to_permutation(other):
147 raise TypeError(msg)
148
149 if not self.ordered and not self.categories.equals(other.categories):
150 # both unordered and different order
151 other_codes = recode_for_categories(
152 other.codes, other.categories, self.categories, copy=False
153 )
154 else:
155 other_codes = other._codes
156
157 ret = op(self._codes, other_codes)
158 mask = (self._codes == -1) | (other_codes == -1)
159 if mask.any():
160 ret[mask] = fill_value
161 return ret
162
163 if hashable:
164 if other in self.categories:
165 i = self._unbox_scalar(other)
166 ret = op(self._codes, i)
167
168 if opname not in {"__eq__", "__ge__", "__gt__"}:
169 # GH#29820 performance trick; get_loc will always give i>=0,
170 # so in the cases (__ne__, __le__, __lt__) the setting
171 # here is a no-op, so can be skipped.
172 mask = self._codes == -1
173 ret[mask] = fill_value
174 return ret
175 else:
176 return ops.invalid_comparison(self, other, op)
177 else:
178 # allow categorical vs object dtype array comparisons for equality
179 # these are only positional comparisons
180 if opname not in ["__eq__", "__ne__"]:
181 raise TypeError(
182 f"Cannot compare a Categorical for op {opname} with "
183 f"type {type(other)}.\nIf you want to compare values, "
184 "use 'np.asarray(cat) <op> other'."
185 )
186
187 if isinstance(other, ExtensionArray) and needs_i8_conversion(other.dtype):
188 # We would return NotImplemented here, but that messes up
189 # ExtensionIndex's wrapped methods
190 return op(other, self)
191 return getattr(np.array(self), opname)(np.array(other))
192
193 func.__name__ = opname
194
195 return func
196
197
198def contains(cat, key, container) -> bool:
199 """
200 Helper for membership check for ``key`` in ``cat``.
201
202 This is a helper method for :method:`__contains__`
203 and :class:`CategoricalIndex.__contains__`.
204
205 Returns True if ``key`` is in ``cat.categories`` and the
206 location of ``key`` in ``categories`` is in ``container``.
207
208 Parameters
209 ----------
210 cat : :class:`Categorical`or :class:`categoricalIndex`
211 key : a hashable object
212 The key to check membership for.
213 container : Container (e.g. list-like or mapping)
214 The container to check for membership in.
215
216 Returns
217 -------
218 is_in : bool
219 True if ``key`` is in ``self.categories`` and location of
220 ``key`` in ``categories`` is in ``container``, else False.
221
222 Notes
223 -----
224 This method does not check for NaN values. Do that separately
225 before calling this method.
226 """
227 hash(key)
228
229 # get location of key in categories.
230 # If a KeyError, the key isn't in categories, so logically
231 # can't be in container either.
232 try:
233 loc = cat.categories.get_loc(key)
234 except (KeyError, TypeError):
235 return False
236
237 # loc is the location of key in categories, but also the *value*
238 # for key in container. So, `key` may be in categories,
239 # but still not in `container`. Example ('b' in categories,
240 # but not in values):
241 # 'b' in Categorical(['a'], categories=['a', 'b']) # False
242 if is_scalar(loc):
243 return loc in container
244 else:
245 # if categories is an IntervalIndex, loc is an array.
246 return any(loc_ in container for loc_ in loc)
247
248
249@set_module("pandas")
250class Categorical(NDArrayBackedExtensionArray, PandasObject, ObjectStringArrayMixin):
251 """
252 Represent a categorical variable in classic R / S-plus fashion.
253
254 `Categoricals` can only take on a limited, and usually fixed, number
255 of possible values (`categories`). In contrast to statistical categorical
256 variables, a `Categorical` might have an order, but numerical operations
257 (additions, divisions, ...) are not possible.
258
259 All values of the `Categorical` are either in `categories` or `np.nan`.
260 Assigning values outside of `categories` will raise a `ValueError`. Order
261 is defined by the order of the `categories`, not lexical order of the
262 values.
263
264 Parameters
265 ----------
266 values : list-like
267 The values of the categorical. If categories are given, values not in
268 categories will be replaced with NaN.
269 categories : Index-like (unique), optional
270 The unique categories for this categorical. If not given, the
271 categories are assumed to be the unique values of `values` (sorted, if
272 possible, otherwise in the order in which they appear).
273 ordered : bool, default False
274 Whether or not this categorical is treated as an ordered categorical.
275 If True, the resulting categorical will be ordered.
276 An ordered categorical respects, when sorted, the order of its
277 `categories` attribute (which in turn is the `categories` argument, if
278 provided).
279 dtype : CategoricalDtype
280 An instance of ``CategoricalDtype`` to use for this categorical.
281 copy : bool, default True
282 Whether to copy if the codes are unchanged.
283
284 Attributes
285 ----------
286 categories : Index
287 The categories of this categorical.
288 codes : ndarray
289 The codes (integer positions, which point to the categories) of this
290 categorical, read only.
291 ordered : bool
292 Whether or not this Categorical is ordered.
293 dtype : CategoricalDtype
294 The instance of ``CategoricalDtype`` storing the ``categories``
295 and ``ordered``.
296
297 Methods
298 -------
299 from_codes
300 as_ordered
301 as_unordered
302 set_categories
303 rename_categories
304 reorder_categories
305 add_categories
306 remove_categories
307 remove_unused_categories
308 map
309 __array__
310
311 Raises
312 ------
313 ValueError
314 If the categories do not validate.
315 TypeError
316 If an explicit ``ordered=True`` is given but no `categories` and the
317 `values` are not sortable.
318
319 See Also
320 --------
321 CategoricalDtype : Type for categorical data.
322 CategoricalIndex : An Index with an underlying ``Categorical``.
323
324 Notes
325 -----
326 See the `user guide
327 <https://pandas.pydata.org/pandas-docs/stable/user_guide/categorical.html>`__
328 for more.
329
330 Examples
331 --------
332 >>> pd.Categorical([1, 2, 3, 1, 2, 3])
333 [1, 2, 3, 1, 2, 3]
334 Categories (3, int64): [1, 2, 3]
335
336 >>> pd.Categorical(["a", "b", "c", "a", "b", "c"])
337 ['a', 'b', 'c', 'a', 'b', 'c']
338 Categories (3, str): ['a', 'b', 'c']
339
340 Missing values are not included as a category.
341
342 >>> c = pd.Categorical([1, 2, 3, 1, 2, 3, np.nan])
343 >>> c
344 [1, 2, 3, 1, 2, 3, NaN]
345 Categories (3, int64): [1, 2, 3]
346
347 However, their presence is indicated in the `codes` attribute
348 by code `-1`.
349
350 >>> c.codes
351 array([ 0, 1, 2, 0, 1, 2, -1], dtype=int8)
352
353 Ordered `Categoricals` can be sorted according to the custom order
354 of the categories and can have a min and max value.
355
356 >>> c = pd.Categorical(
357 ... ["a", "b", "c", "a", "b", "c"], ordered=True, categories=["c", "b", "a"]
358 ... )
359 >>> c
360 ['a', 'b', 'c', 'a', 'b', 'c']
361 Categories (3, str): ['c' < 'b' < 'a']
362 >>> c.min()
363 'c'
364 """
365
366 # For comparisons, so that numpy uses our implementation if the compare
367 # ops, which raise
368 __array_priority__ = 1000
369 # tolist is not actually deprecated, just suppressed in the __dir__
370 _hidden_attrs = PandasObject._hidden_attrs | frozenset(["tolist"])
371 _typ = "categorical"
372
373 _dtype: CategoricalDtype
374
375 @classmethod
376 # error: Argument 2 of "_simple_new" is incompatible with supertype
377 # "NDArrayBacked"; supertype defines the argument type as
378 # "Union[dtype[Any], ExtensionDtype]"
379 def _simple_new( # type: ignore[override]
380 cls, codes: np.ndarray, dtype: CategoricalDtype
381 ) -> Self:
382 # NB: This is not _quite_ as simple as the "usual" _simple_new
383 codes = coerce_indexer_dtype(codes, dtype.categories)
384 dtype = CategoricalDtype(ordered=False).update_dtype(dtype)
385 return super()._simple_new(codes, dtype)
386
387 def __init__(
388 self,
389 values,
390 categories=None,
391 ordered=None,
392 dtype: Dtype | None = None,
393 copy: bool = True,
394 ) -> None:
395 dtype = CategoricalDtype._from_values_or_dtype(
396 values, categories, ordered, dtype
397 )
398 # At this point, dtype is always a CategoricalDtype, but
399 # we may have dtype.categories be None, and we need to
400 # infer categories in a factorization step further below
401
402 if not is_list_like(values):
403 # GH#38433
404 raise TypeError("Categorical input must be list-like")
405
406 # null_mask indicates missing values we want to exclude from inference.
407 # This means: only missing values in list-likes (not arrays/ndframes).
408 null_mask = np.array(False)
409
410 # sanitize input
411 vdtype = getattr(values, "dtype", None)
412 if isinstance(vdtype, CategoricalDtype):
413 if dtype.categories is None:
414 dtype = CategoricalDtype(values.categories, dtype.ordered)
415 elif isinstance(values, range):
416 from pandas.core.indexes.range import RangeIndex
417
418 values = RangeIndex(values)
419 elif not isinstance(values, (ABCIndex, ABCSeries, ExtensionArray)):
420 values = com.convert_to_list_like(values)
421 if isinstance(values, list) and len(values) == 0:
422 # By convention, empty lists result in object dtype:
423 values = np.array([], dtype=object)
424 elif isinstance(values, np.ndarray):
425 if values.ndim > 1:
426 # preempt sanitize_array from raising ValueError
427 raise NotImplementedError(
428 "> 1 ndim Categorical are not supported at this time"
429 )
430 values = sanitize_array(values, None)
431 else:
432 # i.e. must be a list
433 arr = sanitize_array(values, None)
434 null_mask = isna(arr)
435 if null_mask.any():
436 # We remove null values here, then below will re-insert
437 # them, grep "full_codes"
438 arr_list = [values[idx] for idx in np.where(~null_mask)[0]]
439
440 # GH#44900 Do not cast to float if we have only missing values
441 if arr_list or arr.dtype == "object":
442 sanitize_dtype = None
443 else:
444 sanitize_dtype = arr.dtype
445
446 arr = sanitize_array(arr_list, None, dtype=sanitize_dtype)
447 values = arr
448
449 if dtype.categories is None:
450 if isinstance(values.dtype, ArrowDtype) and issubclass(
451 values.dtype.type, CategoricalDtypeType
452 ):
453 from pandas import Index
454
455 if isinstance(values, Index):
456 arr = values._data._pa_array.combine_chunks()
457 else:
458 arr = extract_array(values)._pa_array.combine_chunks()
459 categories = arr.dictionary.to_pandas(types_mapper=ArrowDtype)
460 codes = arr.indices.to_numpy()
461 dtype = CategoricalDtype(categories, values.dtype.pyarrow_dtype.ordered)
462 else:
463 preserve_object = False
464 if isinstance(values, (ABCIndex, ABCSeries)) and values.dtype == object:
465 # GH#61778
466 preserve_object = True
467 if not isinstance(values, ABCIndex):
468 # in particular RangeIndex xref test_index_equal_range_categories
469 values = sanitize_array(values, None)
470 try:
471 codes, categories = factorize(values, sort=True)
472 except TypeError as err:
473 codes, categories = factorize(values, sort=False)
474 if dtype.ordered:
475 # raise, as we don't have a sortable data structure and so
476 # the user should give us one by specifying categories
477 raise TypeError(
478 "'values' is not ordered, please "
479 "explicitly specify the categories order "
480 "by passing in a categories argument."
481 ) from err
482
483 if preserve_object:
484 # GH#61778 wrap categories in an Index to prevent dtype
485 # inference in the CategoricalDtype constructor
486 from pandas import Index
487
488 categories = Index(categories, dtype=object, copy=False)
489
490 # if not preserve_obejct, we're inferring from values
491 dtype = CategoricalDtype(categories, dtype.ordered)
492
493 elif isinstance(values.dtype, CategoricalDtype):
494 old_codes = extract_array(values)._codes
495 codes = recode_for_categories(
496 old_codes,
497 values.dtype.categories,
498 dtype.categories,
499 copy=copy,
500 warn=True,
501 )
502
503 else:
504 codes = _get_codes_for_values(values, dtype.categories)
505
506 if null_mask.any():
507 # Reinsert -1 placeholders for previously removed missing values
508 full_codes = -np.ones(null_mask.shape, dtype=codes.dtype)
509 full_codes[~null_mask] = codes
510 codes = full_codes
511
512 dtype = CategoricalDtype(ordered=False).update_dtype(dtype)
513 arr = coerce_indexer_dtype(codes, dtype.categories)
514 super().__init__(arr, dtype)
515
516 @property
517 def dtype(self) -> CategoricalDtype:
518 """
519 The :class:`~pandas.api.types.CategoricalDtype` for this instance.
520
521 See Also
522 --------
523 astype : Cast argument to a specified dtype.
524 CategoricalDtype : Type for categorical data.
525
526 Examples
527 --------
528 >>> cat = pd.Categorical(["a", "b"], ordered=True)
529 >>> cat
530 ['a', 'b']
531 Categories (2, str): ['a' < 'b']
532 >>> cat.dtype
533 CategoricalDtype(categories=['a', 'b'], ordered=True, categories_dtype=str)
534 """
535 return self._dtype
536
537 @property
538 def _internal_fill_value(self) -> int:
539 # using the specific numpy integer instead of python int to get
540 # the correct dtype back from _quantile in the all-NA case
541 dtype = self._ndarray.dtype
542 return dtype.type(-1)
543
544 @classmethod
545 def _from_sequence(
546 cls, scalars, *, dtype: Dtype | None = None, copy: bool = False
547 ) -> Self:
548 return cls(scalars, dtype=dtype, copy=copy)
549
550 def _cast_pointwise_result(self, values) -> ArrayLike:
551 res = super()._cast_pointwise_result(values)
552 with warnings.catch_warnings():
553 warnings.filterwarnings(
554 "ignore",
555 "Constructing a Categorical with a dtype and values containing",
556 )
557 cat = type(self)._from_sequence(res, dtype=self.dtype)
558 if (cat.isna() == isna(res)).all():
559 # i.e. the conversion was non-lossy
560 return cat
561 return res
562
563 @overload
564 def astype(self, dtype: npt.DTypeLike, copy: bool = ...) -> np.ndarray: ...
565
566 @overload
567 def astype(self, dtype: ExtensionDtype, copy: bool = ...) -> ExtensionArray: ...
568
569 @overload
570 def astype(self, dtype: AstypeArg, copy: bool = ...) -> ArrayLike: ...
571
572 def astype(self, dtype: AstypeArg, copy: bool = True) -> ArrayLike:
573 """
574 Coerce this type to another dtype
575
576 Parameters
577 ----------
578 dtype : numpy dtype or pandas type
579 copy : bool, default True
580 By default, astype always returns a newly allocated object.
581 If copy is set to False and dtype is categorical, the original
582 object is returned.
583 """
584 dtype = pandas_dtype(dtype)
585 result: Categorical | np.ndarray
586 if self.dtype is dtype:
587 result = self.copy() if copy else self
588
589 elif isinstance(dtype, CategoricalDtype):
590 # GH 10696/18593/18630
591 dtype = self.dtype.update_dtype(dtype)
592 self = self.copy() if copy else self
593 result = self._set_dtype(dtype, copy=False)
594 wrong = result.isna() & ~self.isna()
595 if wrong.any():
596 warnings.warn(
597 "Constructing a Categorical with a dtype and values containing "
598 "non-null entries not in that dtype's categories is deprecated "
599 "and will raise in a future version.",
600 Pandas4Warning,
601 stacklevel=find_stack_level(),
602 )
603
604 elif isinstance(dtype, ExtensionDtype):
605 return super().astype(dtype, copy=copy)
606
607 elif dtype.kind in "iu" and self.isna().any():
608 raise ValueError("Cannot convert float NaN to integer")
609
610 elif len(self.codes) == 0 or len(self.categories) == 0:
611 # For NumPy 1.x compatibility we cannot use copy=None. And
612 # `copy=False` has the meaning of `copy=None` here:
613 if not copy:
614 result = np.asarray(self, dtype=dtype)
615 else:
616 result = np.array(self, dtype=dtype)
617
618 else:
619 # GH8628 (PERF): astype category codes instead of astyping array
620 new_cats = self.categories._values
621
622 try:
623 new_cats = new_cats.astype(dtype=dtype, copy=copy)
624 fill_value = self.categories._na_value
625 if not is_valid_na_for_dtype(fill_value, dtype):
626 fill_value = lib.item_from_zerodim(
627 np.array(self.categories._na_value).astype(dtype)
628 )
629 except (
630 TypeError, # downstream error msg for CategoricalIndex is misleading
631 ValueError,
632 ) as err:
633 msg = f"Cannot cast {self.categories.dtype} dtype to {dtype}"
634 raise ValueError(msg) from err
635
636 result = take_nd(
637 new_cats, ensure_platform_int(self._codes), fill_value=fill_value
638 )
639
640 return result
641
642 @classmethod
643 def _from_inferred_categories(
644 cls, inferred_categories, inferred_codes, dtype, true_values=None
645 ) -> Self:
646 """
647 Construct a Categorical from inferred values.
648
649 For inferred categories (`dtype` is None) the categories are sorted.
650 For explicit `dtype`, the `inferred_categories` are cast to the
651 appropriate type.
652
653 Parameters
654 ----------
655 inferred_categories : Index
656 inferred_codes : Index
657 dtype : CategoricalDtype or 'category'
658 true_values : list, optional
659 If none are provided, the default ones are
660 "True", "TRUE", and "true."
661
662 Returns
663 -------
664 Categorical
665 """
666 from pandas import (
667 Index,
668 to_datetime,
669 to_numeric,
670 to_timedelta,
671 )
672
673 cats = Index(inferred_categories, copy=False)
674 known_categories = (
675 isinstance(dtype, CategoricalDtype) and dtype.categories is not None
676 )
677
678 if known_categories:
679 # Convert to a specialized type with `dtype` if specified.
680 if is_any_real_numeric_dtype(dtype.categories.dtype):
681 cats = to_numeric(inferred_categories, errors="coerce")
682 elif lib.is_np_dtype(dtype.categories.dtype, "M"):
683 cats = to_datetime(inferred_categories, errors="coerce")
684 elif lib.is_np_dtype(dtype.categories.dtype, "m"):
685 cats = to_timedelta(inferred_categories, errors="coerce")
686 elif is_bool_dtype(dtype.categories.dtype):
687 if true_values is None:
688 true_values = ["True", "TRUE", "true"]
689
690 # error: Incompatible types in assignment (expression has type
691 # "ndarray", variable has type "Index")
692 cats = cats.isin(true_values) # type: ignore[assignment]
693
694 if known_categories:
695 # Recode from observation order to dtype.categories order.
696 categories = dtype.categories
697 codes = recode_for_categories(
698 inferred_codes, cats, categories, copy=False, warn=True
699 )
700 elif not cats.is_monotonic_increasing:
701 # Sort categories and recode for unknown categories.
702 unsorted = cats.copy()
703 categories = cats.sort_values()
704
705 codes = recode_for_categories(
706 inferred_codes, unsorted, categories, copy=False, warn=True
707 )
708 dtype = CategoricalDtype(categories, ordered=False)
709 else:
710 dtype = CategoricalDtype(cats, ordered=False)
711 codes = inferred_codes
712
713 return cls._simple_new(codes, dtype=dtype)
714
715 @classmethod
716 def from_codes(
717 cls,
718 codes,
719 categories=None,
720 ordered=None,
721 dtype: Dtype | None = None,
722 validate: bool = True,
723 ) -> Self:
724 """
725 Make a Categorical type from codes and categories or dtype.
726
727 This constructor is useful if you already have codes and
728 categories/dtype and so do not need the (computation intensive)
729 factorization step, which is usually done on the constructor.
730
731 If your data does not follow this convention, please use the normal
732 constructor.
733
734 Parameters
735 ----------
736 codes : array-like of int
737 An integer array, where each integer points to a category in
738 categories or dtype.categories, or else is -1 for NaN.
739 categories : index-like, optional
740 The categories for the categorical. Items need to be unique.
741 If the categories are not given here, then they must be provided
742 in `dtype`.
743 ordered : bool, optional
744 Whether or not this categorical is treated as an ordered
745 categorical. If not given here or in `dtype`, the resulting
746 categorical will be unordered.
747 dtype : CategoricalDtype or "category", optional
748 If :class:`CategoricalDtype`, cannot be used together with
749 `categories` or `ordered`.
750 validate : bool, default True
751 If True, validate that the codes are valid for the dtype.
752 If False, don't validate that the codes are valid. Be careful about skipping
753 validation, as invalid codes can lead to severe problems, such as segfaults.
754
755 .. versionadded:: 2.1.0
756
757 Returns
758 -------
759 Categorical
760
761 See Also
762 --------
763 codes : The category codes of the categorical.
764 CategoricalIndex : An Index with an underlying ``Categorical``.
765
766 Examples
767 --------
768 >>> dtype = pd.CategoricalDtype(["a", "b"], ordered=True)
769 >>> pd.Categorical.from_codes(codes=[0, 1, 0, 1], dtype=dtype)
770 ['a', 'b', 'a', 'b']
771 Categories (2, str): ['a' < 'b']
772 """
773 dtype = CategoricalDtype._from_values_or_dtype(
774 categories=categories, ordered=ordered, dtype=dtype
775 )
776 if dtype.categories is None:
777 msg = (
778 "The categories must be provided in 'categories' or "
779 "'dtype'. Both were None."
780 )
781 raise ValueError(msg)
782
783 if validate:
784 # beware: non-valid codes may segfault
785 codes = cls._validate_codes_for_dtype(codes, dtype=dtype)
786
787 return cls._simple_new(codes, dtype=dtype)
788
789 # ------------------------------------------------------------------
790 # Categories/Codes/Ordered
791
792 @property
793 def categories(self) -> Index:
794 """
795 The categories of this categorical.
796
797 Setting assigns new values to each category (effectively a rename of
798 each individual category).
799
800 The assigned value has to be a list-like object. All items must be
801 unique and the number of items in the new categories must be the same
802 as the number of items in the old categories.
803
804 Raises
805 ------
806 ValueError
807 If the new categories do not validate as categories or if the
808 number of new categories is unequal the number of old categories
809
810 See Also
811 --------
812 rename_categories : Rename categories.
813 reorder_categories : Reorder categories.
814 add_categories : Add new categories.
815 remove_categories : Remove the specified categories.
816 remove_unused_categories : Remove categories which are not used.
817 set_categories : Set the categories to the specified ones.
818
819 Examples
820 --------
821 For :class:`pandas.Series`:
822
823 >>> ser = pd.Series(["a", "b", "c", "a"], dtype="category")
824 >>> ser.cat.categories
825 Index(['a', 'b', 'c'], dtype='str')
826
827 >>> raw_cat = pd.Categorical([None, "b", "c", None], categories=["b", "c", "d"])
828 >>> ser = pd.Series(raw_cat)
829 >>> ser.cat.categories
830 Index(['b', 'c', 'd'], dtype='str')
831
832 For :class:`pandas.Categorical`:
833
834 >>> cat = pd.Categorical(["a", "b"], ordered=True)
835 >>> cat.categories
836 Index(['a', 'b'], dtype='str')
837
838 For :class:`pandas.CategoricalIndex`:
839
840 >>> ci = pd.CategoricalIndex(["a", "c", "b", "a", "c", "b"])
841 >>> ci.categories
842 Index(['a', 'b', 'c'], dtype='str')
843
844 >>> ci = pd.CategoricalIndex(["a", "c"], categories=["c", "b", "a"])
845 >>> ci.categories
846 Index(['c', 'b', 'a'], dtype='str')
847 """
848 return self.dtype.categories
849
850 @property
851 def ordered(self) -> Ordered:
852 """
853 Whether the categories have an ordered relationship.
854
855 See Also
856 --------
857 set_ordered : Set the ordered attribute.
858 as_ordered : Set the Categorical to be ordered.
859 as_unordered : Set the Categorical to be unordered.
860
861 Examples
862 --------
863 For :class:`pandas.Series`:
864
865 >>> ser = pd.Series(["a", "b", "c", "a"], dtype="category")
866 >>> ser.cat.ordered
867 False
868
869 >>> raw_cat = pd.Categorical(["a", "b", "c", "a"], ordered=True)
870 >>> ser = pd.Series(raw_cat)
871 >>> ser.cat.ordered
872 True
873
874 For :class:`pandas.Categorical`:
875
876 >>> cat = pd.Categorical(["a", "b"], ordered=True)
877 >>> cat.ordered
878 True
879
880 >>> cat = pd.Categorical(["a", "b"], ordered=False)
881 >>> cat.ordered
882 False
883
884 For :class:`pandas.CategoricalIndex`:
885
886 >>> ci = pd.CategoricalIndex(["a", "b"], ordered=True)
887 >>> ci.ordered
888 True
889
890 >>> ci = pd.CategoricalIndex(["a", "b"], ordered=False)
891 >>> ci.ordered
892 False
893 """
894 return self.dtype.ordered
895
896 @property
897 def codes(self) -> np.ndarray:
898 """
899 The category codes of this categorical index.
900
901 Codes are an array of integers which are the positions of the actual
902 values in the categories array.
903
904 There is no setter, use the other categorical methods and the normal item
905 setter to change values in the categorical.
906
907 Returns
908 -------
909 ndarray[int]
910 A non-writable view of the ``codes`` array.
911
912 See Also
913 --------
914 Categorical.from_codes : Make a Categorical from codes.
915 CategoricalIndex : An Index with an underlying ``Categorical``.
916
917 Examples
918 --------
919 For :class:`pandas.Categorical`:
920
921 >>> cat = pd.Categorical(["a", "b"], ordered=True)
922 >>> cat.codes
923 array([0, 1], dtype=int8)
924
925 For :class:`pandas.CategoricalIndex`:
926
927 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a", "b", "c"])
928 >>> ci.codes
929 array([0, 1, 2, 0, 1, 2], dtype=int8)
930
931 >>> ci = pd.CategoricalIndex(["a", "c"], categories=["c", "b", "a"])
932 >>> ci.codes
933 array([2, 0], dtype=int8)
934 """
935 v = self._codes.view()
936 v.flags.writeable = False
937 return v
938
939 def _set_categories(self, categories, fastpath: bool = False) -> None:
940 """
941 Sets new categories inplace
942
943 Parameters
944 ----------
945 fastpath : bool, default False
946 Don't perform validation of the categories for uniqueness or nulls
947
948 Examples
949 --------
950 >>> c = pd.Categorical(["a", "b"])
951 >>> c
952 ['a', 'b']
953 Categories (2, str): ['a', 'b']
954
955 >>> c._set_categories(pd.Index(["a", "c"]))
956 >>> c
957 ['a', 'c']
958 Categories (2, str): ['a', 'c']
959 """
960 if fastpath:
961 new_dtype = CategoricalDtype._from_fastpath(categories, self.ordered)
962 else:
963 new_dtype = CategoricalDtype(categories, ordered=self.ordered)
964 if (
965 not fastpath
966 and self.dtype.categories is not None
967 and len(new_dtype.categories) != len(self.dtype.categories)
968 ):
969 raise ValueError(
970 "new categories need to have the same number of "
971 "items as the old categories!"
972 )
973
974 super().__init__(self._ndarray, new_dtype)
975
976 def _set_dtype(self, dtype: CategoricalDtype, *, copy: bool) -> Self:
977 """
978 Internal method for directly updating the CategoricalDtype
979
980 Parameters
981 ----------
982 dtype : CategoricalDtype
983
984 Notes
985 -----
986 We don't do any validation here. It's assumed that the dtype is
987 a (valid) instance of `CategoricalDtype`.
988 """
989 codes = recode_for_categories(
990 self.codes, self.categories, dtype.categories, copy=copy
991 )
992 return type(self)._simple_new(codes, dtype=dtype)
993
994 def set_ordered(self, value: bool) -> Self:
995 """
996 Set the ordered attribute to the boolean value.
997
998 Parameters
999 ----------
1000 value : bool
1001 Set whether this categorical is ordered (True) or not (False).
1002 """
1003 new_dtype = CategoricalDtype(self.categories, ordered=value)
1004 cat = self.copy()
1005 NDArrayBacked.__init__(cat, cat._ndarray, new_dtype)
1006 return cat
1007
1008 def as_ordered(self) -> Self:
1009 """
1010 Set the Categorical to be ordered.
1011
1012 Returns
1013 -------
1014 Categorical
1015 Ordered Categorical.
1016
1017 See Also
1018 --------
1019 as_unordered : Set the Categorical to be unordered.
1020
1021 Examples
1022 --------
1023 For :class:`pandas.Series`:
1024
1025 >>> ser = pd.Series(["a", "b", "c", "a"], dtype="category")
1026 >>> ser.cat.ordered
1027 False
1028 >>> ser = ser.cat.as_ordered()
1029 >>> ser.cat.ordered
1030 True
1031
1032 For :class:`pandas.CategoricalIndex`:
1033
1034 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"])
1035 >>> ci.ordered
1036 False
1037 >>> ci = ci.as_ordered()
1038 >>> ci.ordered
1039 True
1040 """
1041 return self.set_ordered(True)
1042
1043 def as_unordered(self) -> Self:
1044 """
1045 Set the Categorical to be unordered.
1046
1047 Returns
1048 -------
1049 Categorical
1050 Unordered Categorical.
1051
1052 See Also
1053 --------
1054 as_ordered : Set the Categorical to be ordered.
1055
1056 Examples
1057 --------
1058 For :class:`pandas.Series`:
1059
1060 >>> raw_cat = pd.Categorical(["a", "b", "c", "a"], ordered=True)
1061 >>> ser = pd.Series(raw_cat)
1062 >>> ser.cat.ordered
1063 True
1064 >>> ser = ser.cat.as_unordered()
1065 >>> ser.cat.ordered
1066 False
1067
1068 For :class:`pandas.CategoricalIndex`:
1069
1070 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"], ordered=True)
1071 >>> ci.ordered
1072 True
1073 >>> ci = ci.as_unordered()
1074 >>> ci.ordered
1075 False
1076 """
1077 return self.set_ordered(False)
1078
1079 def set_categories(
1080 self, new_categories, ordered=None, rename: bool = False
1081 ) -> Self:
1082 """
1083 Set the categories to the specified new categories.
1084
1085 ``new_categories`` can include new categories (which will result in
1086 unused categories) or remove old categories (which results in values
1087 set to ``NaN``). If ``rename=True``, the categories will simply be renamed
1088 (less or more items than in old categories will result in values set to
1089 ``NaN`` or in unused categories respectively).
1090
1091 This method can be used to perform more than one action of adding,
1092 removing, and reordering simultaneously and is therefore faster than
1093 performing the individual steps via the more specialised methods.
1094
1095 On the other hand this methods does not do checks (e.g., whether the
1096 old categories are included in the new categories on a reorder), which
1097 can result in surprising changes, for example when using special string
1098 dtypes, which do not consider a S1 string equal to a single char
1099 python string.
1100
1101 Parameters
1102 ----------
1103 new_categories : Index-like
1104 The categories in new order.
1105 ordered : bool, default None
1106 Whether or not the categorical is treated as an ordered categorical.
1107 If not given, do not change the ordered information.
1108 rename : bool, default False
1109 Whether or not the new_categories should be considered as a rename
1110 of the old categories or as reordered categories.
1111
1112 Returns
1113 -------
1114 Categorical
1115 New categories to be used, with optional ordering changes.
1116
1117 Raises
1118 ------
1119 ValueError
1120 If new_categories does not validate as categories
1121
1122 See Also
1123 --------
1124 rename_categories : Rename categories.
1125 reorder_categories : Reorder categories.
1126 add_categories : Add new categories.
1127 remove_categories : Remove the specified categories.
1128 remove_unused_categories : Remove categories which are not used.
1129
1130 Examples
1131 --------
1132 For :class:`pandas.Series`:
1133
1134 >>> raw_cat = pd.Categorical(
1135 ... ["a", "b", "c", None], categories=["a", "b", "c"], ordered=True
1136 ... )
1137 >>> ser = pd.Series(raw_cat)
1138 >>> ser
1139 0 a
1140 1 b
1141 2 c
1142 3 NaN
1143 dtype: category
1144 Categories (3, str): ['a' < 'b' < 'c']
1145
1146 >>> ser.cat.set_categories(["A", "B", "C"], rename=True)
1147 0 A
1148 1 B
1149 2 C
1150 3 NaN
1151 dtype: category
1152 Categories (3, str): ['A' < 'B' < 'C']
1153
1154 For :class:`pandas.CategoricalIndex`:
1155
1156 >>> ci = pd.CategoricalIndex(
1157 ... ["a", "b", "c", None], categories=["a", "b", "c"], ordered=True
1158 ... )
1159 >>> ci
1160 CategoricalIndex(['a', 'b', 'c', nan], categories=['a', 'b', 'c'],
1161 ordered=True, dtype='category')
1162
1163 >>> ci.set_categories(["A", "b", "c"])
1164 CategoricalIndex([nan, 'b', 'c', nan], categories=['A', 'b', 'c'],
1165 ordered=True, dtype='category')
1166 >>> ci.set_categories(["A", "b", "c"], rename=True)
1167 CategoricalIndex(['A', 'b', 'c', nan], categories=['A', 'b', 'c'],
1168 ordered=True, dtype='category')
1169 """
1170
1171 if ordered is None:
1172 ordered = self.dtype.ordered
1173 new_dtype = CategoricalDtype(new_categories, ordered=ordered)
1174
1175 cat = self.copy()
1176 if rename:
1177 if cat.dtype.categories is not None and len(new_dtype.categories) < len(
1178 cat.dtype.categories
1179 ):
1180 # remove all _codes which are larger and set to -1/NaN
1181 cat._codes[cat._codes >= len(new_dtype.categories)] = -1
1182 codes = cat._codes
1183 else:
1184 codes = recode_for_categories(
1185 cat.codes, cat.categories, new_dtype.categories, copy=False, warn=False
1186 )
1187 NDArrayBacked.__init__(cat, codes, new_dtype)
1188 return cat
1189
1190 def rename_categories(self, new_categories) -> Self:
1191 """
1192 Rename categories.
1193
1194 This method is commonly used to re-label or adjust the
1195 category names in categorical data without changing the
1196 underlying data. It is useful in situations where you want
1197 to modify the labels used for clarity, consistency,
1198 or readability.
1199
1200 Parameters
1201 ----------
1202 new_categories : list-like, dict-like or callable
1203
1204 New categories which will replace old categories.
1205
1206 * list-like: all items must be unique and the number of items in
1207 the new categories must match the existing number of categories.
1208
1209 * dict-like: specifies a mapping from
1210 old categories to new. Categories not contained in the mapping
1211 are passed through and extra categories in the mapping are
1212 ignored.
1213
1214 * callable : a callable that is called on all items in the old
1215 categories and whose return values comprise the new categories.
1216
1217 Returns
1218 -------
1219 Categorical
1220 Categorical with renamed categories.
1221
1222 Raises
1223 ------
1224 ValueError
1225 If new categories are list-like and do not have the same number of
1226 items than the current categories or do not validate as categories
1227
1228 See Also
1229 --------
1230 reorder_categories : Reorder categories.
1231 add_categories : Add new categories.
1232 remove_categories : Remove the specified categories.
1233 remove_unused_categories : Remove categories which are not used.
1234 set_categories : Set the categories to the specified ones.
1235
1236 Examples
1237 --------
1238 >>> c = pd.Categorical(["a", "a", "b"])
1239 >>> c.rename_categories([0, 1])
1240 [0, 0, 1]
1241 Categories (2, int64): [0, 1]
1242
1243 For dict-like ``new_categories``, extra keys are ignored and
1244 categories not in the dictionary are passed through
1245
1246 >>> c.rename_categories({"a": "A", "c": "C"})
1247 ['A', 'A', 'b']
1248 Categories (2, str): ['A', 'b']
1249
1250 You may also provide a callable to create the new categories
1251
1252 >>> c.rename_categories(lambda x: x.upper())
1253 ['A', 'A', 'B']
1254 Categories (2, str): ['A', 'B']
1255 """
1256
1257 if is_dict_like(new_categories):
1258 new_categories = [
1259 new_categories.get(item, item) for item in self.categories
1260 ]
1261 elif callable(new_categories):
1262 new_categories = [new_categories(item) for item in self.categories]
1263
1264 cat = self.copy()
1265 cat._set_categories(new_categories)
1266 return cat
1267
1268 def reorder_categories(self, new_categories, ordered=None) -> Self:
1269 """
1270 Reorder categories as specified in new_categories.
1271
1272 ``new_categories`` need to include all old categories and no new category
1273 items.
1274
1275 Parameters
1276 ----------
1277 new_categories : Index-like
1278 The categories in new order.
1279 ordered : bool, optional
1280 Whether or not the categorical is treated as an ordered categorical.
1281 If not given, do not change the ordered information.
1282
1283 Returns
1284 -------
1285 Categorical
1286 Categorical with reordered categories.
1287
1288 Raises
1289 ------
1290 ValueError
1291 If the new categories do not contain all old category items or any
1292 new ones
1293
1294 See Also
1295 --------
1296 rename_categories : Rename categories.
1297 add_categories : Add new categories.
1298 remove_categories : Remove the specified categories.
1299 remove_unused_categories : Remove categories which are not used.
1300 set_categories : Set the categories to the specified ones.
1301
1302 Examples
1303 --------
1304 For :class:`pandas.Series`:
1305
1306 >>> ser = pd.Series(["a", "b", "c", "a"], dtype="category")
1307 >>> ser = ser.cat.reorder_categories(["c", "b", "a"], ordered=True)
1308 >>> ser
1309 0 a
1310 1 b
1311 2 c
1312 3 a
1313 dtype: category
1314 Categories (3, str): ['c' < 'b' < 'a']
1315
1316 >>> ser.sort_values()
1317 2 c
1318 1 b
1319 0 a
1320 3 a
1321 dtype: category
1322 Categories (3, str): ['c' < 'b' < 'a']
1323
1324 For :class:`pandas.CategoricalIndex`:
1325
1326 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"])
1327 >>> ci
1328 CategoricalIndex(['a', 'b', 'c', 'a'], categories=['a', 'b', 'c'],
1329 ordered=False, dtype='category')
1330 >>> ci.reorder_categories(["c", "b", "a"], ordered=True)
1331 CategoricalIndex(['a', 'b', 'c', 'a'], categories=['c', 'b', 'a'],
1332 ordered=True, dtype='category')
1333 """
1334 if (
1335 len(self.categories) != len(new_categories)
1336 or not self.categories.difference(new_categories).empty
1337 ):
1338 raise ValueError(
1339 "items in new_categories are not the same as in old categories"
1340 )
1341 return self.set_categories(new_categories, ordered=ordered)
1342
1343 def add_categories(self, new_categories) -> Self:
1344 """
1345 Add new categories.
1346
1347 `new_categories` will be included at the last/highest place in the
1348 categories and will be unused directly after this call.
1349
1350 Parameters
1351 ----------
1352 new_categories : category or list-like of category
1353 The new categories to be included.
1354
1355 Returns
1356 -------
1357 Categorical
1358 Categorical with new categories added.
1359
1360 Raises
1361 ------
1362 ValueError
1363 If the new categories include old categories or do not validate as
1364 categories
1365
1366 See Also
1367 --------
1368 rename_categories : Rename categories.
1369 reorder_categories : Reorder categories.
1370 remove_categories : Remove the specified categories.
1371 remove_unused_categories : Remove categories which are not used.
1372 set_categories : Set the categories to the specified ones.
1373
1374 Examples
1375 --------
1376 >>> c = pd.Categorical(["c", "b", "c"])
1377 >>> c
1378 ['c', 'b', 'c']
1379 Categories (2, str): ['b', 'c']
1380
1381 >>> c.add_categories(["d", "a"])
1382 ['c', 'b', 'c']
1383 Categories (4, str): ['b', 'c', 'd', 'a']
1384 """
1385
1386 if not is_list_like(new_categories):
1387 new_categories = [new_categories]
1388 already_included = set(new_categories) & set(self.dtype.categories)
1389 if len(already_included) != 0:
1390 raise ValueError(
1391 f"new categories must not include old categories: {already_included}"
1392 )
1393
1394 if hasattr(new_categories, "dtype"):
1395 from pandas import Series
1396
1397 dtype = find_common_type(
1398 [self.dtype.categories.dtype, new_categories.dtype]
1399 )
1400 new_categories = Series(
1401 list(self.dtype.categories) + list(new_categories), dtype=dtype
1402 )
1403 else:
1404 new_categories = list(self.dtype.categories) + list(new_categories)
1405
1406 new_dtype = CategoricalDtype(new_categories, self.ordered)
1407 cat = self.copy()
1408 codes = coerce_indexer_dtype(cat._ndarray, new_dtype.categories)
1409 NDArrayBacked.__init__(cat, codes, new_dtype)
1410 return cat
1411
1412 def remove_categories(self, removals) -> Self:
1413 """
1414 Remove the specified categories.
1415
1416 The ``removals`` argument must be a subset of the current categories.
1417 Any values that were part of the removed categories will be set to NaN.
1418
1419 Parameters
1420 ----------
1421 removals : category or list of categories
1422 The categories which should be removed.
1423
1424 Returns
1425 -------
1426 Categorical
1427 Categorical with removed categories.
1428
1429 Raises
1430 ------
1431 ValueError
1432 If the removals are not contained in the categories
1433
1434 See Also
1435 --------
1436 rename_categories : Rename categories.
1437 reorder_categories : Reorder categories.
1438 add_categories : Add new categories.
1439 remove_unused_categories : Remove categories which are not used.
1440 set_categories : Set the categories to the specified ones.
1441
1442 Examples
1443 --------
1444 >>> c = pd.Categorical(["a", "c", "b", "c", "d"])
1445 >>> c
1446 ['a', 'c', 'b', 'c', 'd']
1447 Categories (4, str): ['a', 'b', 'c', 'd']
1448
1449 >>> c.remove_categories(["d", "a"])
1450 [NaN, 'c', 'b', 'c', NaN]
1451 Categories (2, str): ['b', 'c']
1452 """
1453 from pandas import Index
1454
1455 if not is_list_like(removals):
1456 removals = [removals]
1457
1458 removals = Index(removals).unique().dropna()
1459 new_categories = (
1460 self.dtype.categories.difference(removals, sort=False)
1461 if self.dtype.ordered is True
1462 else self.dtype.categories.difference(removals)
1463 )
1464 not_included = removals.difference(self.dtype.categories)
1465
1466 if len(not_included) != 0:
1467 not_included = set(not_included)
1468 raise ValueError(f"removals must all be in old categories: {not_included}")
1469
1470 return self.set_categories(new_categories, ordered=self.ordered, rename=False)
1471
1472 def remove_unused_categories(self) -> Self:
1473 """
1474 Remove categories which are not used.
1475
1476 This method is useful when working with datasets
1477 that undergo dynamic changes where categories may no longer be
1478 relevant, allowing to maintain a clean, efficient data structure.
1479
1480 Returns
1481 -------
1482 Categorical
1483 Categorical with unused categories dropped.
1484
1485 See Also
1486 --------
1487 rename_categories : Rename categories.
1488 reorder_categories : Reorder categories.
1489 add_categories : Add new categories.
1490 remove_categories : Remove the specified categories.
1491 set_categories : Set the categories to the specified ones.
1492
1493 Examples
1494 --------
1495 >>> c = pd.Categorical(["a", "c", "b", "c", "d"])
1496 >>> c
1497 ['a', 'c', 'b', 'c', 'd']
1498 Categories (4, str): ['a', 'b', 'c', 'd']
1499
1500 >>> c[2] = "a"
1501 >>> c[4] = "c"
1502 >>> c
1503 ['a', 'c', 'a', 'c', 'c']
1504 Categories (4, str): ['a', 'b', 'c', 'd']
1505
1506 >>> c.remove_unused_categories()
1507 ['a', 'c', 'a', 'c', 'c']
1508 Categories (2, str): ['a', 'c']
1509 """
1510 idx, inv = np.unique(self._codes, return_inverse=True)
1511
1512 if idx.size != 0 and idx[0] == -1: # na sentinel
1513 idx, inv = idx[1:], inv - 1
1514
1515 new_categories = self.dtype.categories.take(idx)
1516 new_dtype = CategoricalDtype._from_fastpath(
1517 new_categories, ordered=self.ordered
1518 )
1519 new_codes = coerce_indexer_dtype(inv, new_dtype.categories)
1520
1521 cat = self.copy()
1522 NDArrayBacked.__init__(cat, new_codes, new_dtype)
1523 return cat
1524
1525 # ------------------------------------------------------------------
1526
1527 def map(
1528 self,
1529 mapper,
1530 na_action: Literal["ignore"] | None = None,
1531 ):
1532 """
1533 Map categories using an input mapping or function.
1534
1535 Maps the categories to new categories. If the mapping correspondence is
1536 one-to-one the result is a :class:`~pandas.Categorical` which has the
1537 same order property as the original, otherwise a :class:`~pandas.Index`
1538 is returned. NaN values are unaffected.
1539
1540 If a `dict` or :class:`~pandas.Series` is used any unmapped category is
1541 mapped to `NaN`. Note that if this happens an :class:`~pandas.Index`
1542 will be returned.
1543
1544 Parameters
1545 ----------
1546 mapper : function, dict, or Series
1547 Mapping correspondence.
1548 na_action : {None, 'ignore'}, default None
1549 If 'ignore', propagate NaN values, without passing them to the
1550 mapping correspondence.
1551
1552 Returns
1553 -------
1554 pandas.Categorical or pandas.Index
1555 Mapped categorical.
1556
1557 See Also
1558 --------
1559 CategoricalIndex.map : Apply a mapping correspondence on a
1560 :class:`~pandas.CategoricalIndex`.
1561 Index.map : Apply a mapping correspondence on an
1562 :class:`~pandas.Index`.
1563 Series.map : Apply a mapping correspondence on a
1564 :class:`~pandas.Series`.
1565 Series.apply : Apply more complex functions on a
1566 :class:`~pandas.Series`.
1567
1568 Examples
1569 --------
1570 >>> cat = pd.Categorical(["a", "b", "c"])
1571 >>> cat
1572 ['a', 'b', 'c']
1573 Categories (3, str): ['a', 'b', 'c']
1574 >>> cat.map(lambda x: x.upper(), na_action=None)
1575 ['A', 'B', 'C']
1576 Categories (3, str): ['A', 'B', 'C']
1577 >>> cat.map({"a": "first", "b": "second", "c": "third"}, na_action=None)
1578 ['first', 'second', 'third']
1579 Categories (3, str): ['first', 'second', 'third']
1580
1581 If the mapping is one-to-one the ordering of the categories is
1582 preserved:
1583
1584 >>> cat = pd.Categorical(["a", "b", "c"], ordered=True)
1585 >>> cat
1586 ['a', 'b', 'c']
1587 Categories (3, str): ['a' < 'b' < 'c']
1588 >>> cat.map({"a": 3, "b": 2, "c": 1}, na_action=None)
1589 [3, 2, 1]
1590 Categories (3, int64): [3 < 2 < 1]
1591
1592 If the mapping is not one-to-one an :class:`~pandas.Index` is returned:
1593
1594 >>> cat.map({"a": "first", "b": "second", "c": "first"}, na_action=None)
1595 Index(['first', 'second', 'first'], dtype='str')
1596
1597 If a `dict` is used, all unmapped categories are mapped to `NaN` and
1598 the result is an :class:`~pandas.Index`:
1599
1600 >>> cat.map({"a": "first", "b": "second"}, na_action=None)
1601 Index(['first', 'second', nan], dtype='str')
1602
1603 The mapping function is applied to categories, not to each value. It is
1604 therefore only called once per unique category, and the result reused for
1605 all occurrences:
1606
1607 >>> cat = pd.Categorical(["a", "a", "b"])
1608 >>> calls = []
1609 >>> def f(x):
1610 ... calls.append(x)
1611 ... return x.upper()
1612 >>> result = cat.map(f)
1613 >>> result
1614 ['A', 'A', 'B']
1615 Categories (2, str): ['A', 'B']
1616 >>> calls
1617 ['a', 'b']
1618 """
1619 assert callable(mapper) or is_dict_like(mapper)
1620
1621 new_categories = self.categories.map(mapper)
1622
1623 has_nans = np.any(self._codes == -1)
1624
1625 na_val = np.nan
1626 if na_action is None and has_nans:
1627 na_val = mapper(np.nan) if callable(mapper) else mapper.get(np.nan, np.nan)
1628
1629 if new_categories.is_unique and not new_categories.hasnans and na_val is np.nan:
1630 new_dtype = CategoricalDtype(new_categories, ordered=self.ordered)
1631 return self.from_codes(self._codes.copy(), dtype=new_dtype, validate=False)
1632
1633 if has_nans:
1634 new_categories = new_categories.insert(len(new_categories), na_val)
1635
1636 return np.take(new_categories, self._codes)
1637
1638 __eq__ = _cat_compare_op(operator.eq)
1639 __ne__ = _cat_compare_op(operator.ne)
1640 __lt__ = _cat_compare_op(operator.lt)
1641 __gt__ = _cat_compare_op(operator.gt)
1642 __le__ = _cat_compare_op(operator.le)
1643 __ge__ = _cat_compare_op(operator.ge)
1644
1645 # -------------------------------------------------------------
1646 # Validators; ideally these can be de-duplicated
1647
1648 def _validate_setitem_value(self, value):
1649 if not is_hashable(value):
1650 # wrap scalars and hashable-listlikes in list
1651 return self._validate_listlike(value)
1652 else:
1653 return self._validate_scalar(value)
1654
1655 def _validate_scalar(self, fill_value):
1656 """
1657 Convert a user-facing fill_value to a representation to use with our
1658 underlying ndarray, raising TypeError if this is not possible.
1659
1660 Parameters
1661 ----------
1662 fill_value : object
1663
1664 Returns
1665 -------
1666 fill_value : int
1667
1668 Raises
1669 ------
1670 TypeError
1671 """
1672
1673 if is_valid_na_for_dtype(fill_value, self.categories.dtype):
1674 fill_value = -1
1675 elif fill_value in self.categories:
1676 fill_value = self._unbox_scalar(fill_value)
1677 else:
1678 raise TypeError(
1679 "Cannot setitem on a Categorical with a new "
1680 f"category ({fill_value}), set the categories first"
1681 ) from None
1682 return fill_value
1683
1684 @classmethod
1685 def _validate_codes_for_dtype(cls, codes, *, dtype: CategoricalDtype) -> np.ndarray:
1686 if isinstance(codes, ExtensionArray) and is_integer_dtype(codes.dtype):
1687 # Avoid the implicit conversion of Int to object
1688 if isna(codes).any():
1689 raise ValueError("codes cannot contain NA values")
1690 codes = codes.to_numpy(dtype=np.int64)
1691 else:
1692 codes = np.asarray(codes)
1693 if len(codes) and codes.dtype.kind not in "iu":
1694 raise ValueError("codes need to be array-like integers")
1695
1696 if len(codes) and (codes.max() >= len(dtype.categories) or codes.min() < -1):
1697 raise ValueError("codes need to be between -1 and len(categories)-1")
1698 return codes
1699
1700 # -------------------------------------------------------------
1701
1702 @ravel_compat
1703 def __array__(
1704 self, dtype: NpDtype | None = None, copy: bool | None = None
1705 ) -> np.ndarray:
1706 """
1707 The numpy array interface.
1708
1709 Users should not call this directly. Rather, it is invoked by
1710 :func:`numpy.array` and :func:`numpy.asarray`.
1711
1712 Parameters
1713 ----------
1714 dtype : np.dtype or None
1715 Specifies the dtype for the array.
1716
1717 copy : bool or None, optional
1718 See :func:`numpy.asarray`.
1719
1720 Returns
1721 -------
1722 numpy.array
1723 A numpy array of either the specified dtype or,
1724 if dtype==None (default), the same dtype as
1725 categorical.categories.dtype.
1726
1727 See Also
1728 --------
1729 numpy.asarray : Convert input to numpy.ndarray.
1730
1731 Examples
1732 --------
1733
1734 >>> cat = pd.Categorical(["a", "b"], ordered=True)
1735
1736 The following calls ``cat.__array__``
1737
1738 >>> np.asarray(cat)
1739 array(['a', 'b'], dtype=object)
1740 """
1741 if copy is False:
1742 raise ValueError(
1743 "Unable to avoid copy while creating an array as requested."
1744 )
1745
1746 ret = take_nd(self.categories._values, self._codes)
1747 # When we're a Categorical[ExtensionArray], like Interval,
1748 # we need to ensure __array__ gets all the way to an
1749 # ndarray.
1750
1751 # `take_nd` should already make a copy, so don't force again.
1752 return np.asarray(ret, dtype=dtype)
1753
1754 def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs):
1755 # for binary ops, use our custom dunder methods
1756 result = arraylike.maybe_dispatch_ufunc_to_dunder_op(
1757 self, ufunc, method, *inputs, **kwargs
1758 )
1759 if result is not NotImplemented:
1760 return result
1761
1762 if "out" in kwargs:
1763 # e.g. test_numpy_ufuncs_out
1764 return arraylike.dispatch_ufunc_with_out(
1765 self, ufunc, method, *inputs, **kwargs
1766 )
1767
1768 if method == "reduce":
1769 # e.g. TestCategoricalAnalytics::test_min_max_ordered
1770 result = arraylike.dispatch_reduction_ufunc(
1771 self, ufunc, method, *inputs, **kwargs
1772 )
1773 if result is not NotImplemented:
1774 return result
1775
1776 # for all other cases, raise for now (similarly as what happens in
1777 # Series.__array_prepare__)
1778 raise TypeError(
1779 f"Object with dtype {self.dtype} cannot perform "
1780 f"the numpy op {ufunc.__name__}"
1781 )
1782
1783 def __setstate__(self, state) -> None:
1784 """Necessary for making this object picklable"""
1785 if not isinstance(state, dict):
1786 return super().__setstate__(state)
1787
1788 if "_dtype" not in state:
1789 state["_dtype"] = CategoricalDtype(state["_categories"], state["_ordered"])
1790
1791 if "_codes" in state and "_ndarray" not in state:
1792 # backward compat, changed what is property vs attribute
1793 state["_ndarray"] = state.pop("_codes")
1794
1795 super().__setstate__(state)
1796
1797 @property
1798 def nbytes(self) -> int:
1799 return self._codes.nbytes + self.dtype.categories.values.nbytes
1800
1801 def memory_usage(self, deep: bool = False) -> int:
1802 """
1803 Memory usage of my values
1804
1805 Parameters
1806 ----------
1807 deep : bool
1808 Introspect the data deeply, interrogate
1809 `object` dtypes for system-level memory consumption
1810
1811 Returns
1812 -------
1813 bytes used
1814
1815 Notes
1816 -----
1817 Memory usage does not include memory consumed by elements that
1818 are not components of the array if deep=False
1819
1820 See Also
1821 --------
1822 numpy.ndarray.nbytes
1823 """
1824 return self._codes.nbytes + self.dtype.categories.memory_usage(deep=deep)
1825
1826 def isna(self) -> npt.NDArray[np.bool_]:
1827 """
1828 Detect missing values
1829
1830 Missing values (-1 in .codes) are detected.
1831
1832 Returns
1833 -------
1834 np.ndarray[bool] of whether my values are null
1835
1836 See Also
1837 --------
1838 isna : Top-level isna.
1839 isnull : Alias of isna.
1840 Categorical.notna : Boolean inverse of Categorical.isna.
1841
1842 """
1843 return self._codes == -1
1844
1845 isnull = isna
1846
1847 def notna(self) -> npt.NDArray[np.bool_]:
1848 """
1849 Inverse of isna
1850
1851 Both missing values (-1 in .codes) and NA as a category are detected as
1852 null.
1853
1854 Returns
1855 -------
1856 np.ndarray[bool] of whether my values are not null
1857
1858 See Also
1859 --------
1860 notna : Top-level notna.
1861 notnull : Alias of notna.
1862 Categorical.isna : Boolean inverse of Categorical.notna.
1863
1864 """
1865 return ~self.isna()
1866
1867 notnull = notna
1868
1869 def value_counts(self, dropna: bool = True) -> Series:
1870 """
1871 Return a Series containing counts of each category.
1872
1873 Every category will have an entry, even those with a count of 0.
1874
1875 Parameters
1876 ----------
1877 dropna : bool, default True
1878 Don't include counts of NaN.
1879
1880 Returns
1881 -------
1882 counts : Series
1883
1884 See Also
1885 --------
1886 Series.value_counts
1887 """
1888 from pandas import (
1889 CategoricalIndex,
1890 Series,
1891 )
1892
1893 code, cat = self._codes, self.categories
1894 ncat, mask = (len(cat), code >= 0)
1895 ix, clean = np.arange(ncat), mask.all()
1896
1897 if dropna or clean:
1898 obs = code if clean else code[mask]
1899 count = np.bincount(obs, minlength=ncat or 0)
1900 else:
1901 count = np.bincount(np.where(mask, code, ncat))
1902 ix = np.append(ix, -1)
1903
1904 ix = coerce_indexer_dtype(ix, self.dtype.categories)
1905 ix_categorical = self._from_backing_data(ix)
1906
1907 return Series(
1908 count,
1909 index=CategoricalIndex(ix_categorical),
1910 dtype="int64",
1911 name="count",
1912 copy=False,
1913 )
1914
1915 # error: Argument 2 of "_empty" is incompatible with supertype
1916 # "NDArrayBackedExtensionArray"; supertype defines the argument type as
1917 # "ExtensionDtype"
1918 @classmethod
1919 def _empty( # type: ignore[override]
1920 cls, shape: Shape, dtype: CategoricalDtype
1921 ) -> Self:
1922 """
1923 Analogous to np.empty(shape, dtype=dtype)
1924
1925 Parameters
1926 ----------
1927 shape : tuple[int]
1928 dtype : CategoricalDtype
1929 """
1930 arr = cls._from_sequence([], dtype=dtype)
1931
1932 # We have to use np.zeros instead of np.empty otherwise the resulting
1933 # ndarray may contain codes not supported by this dtype, in which
1934 # case repr(result) could segfault.
1935 backing = np.zeros(shape, dtype=arr._ndarray.dtype)
1936
1937 return arr._from_backing_data(backing)
1938
1939 def _internal_get_values(self) -> ArrayLike:
1940 """
1941 Return the values.
1942
1943 For internal compatibility with pandas formatting.
1944
1945 Returns
1946 -------
1947 np.ndarray or ExtensionArray
1948 A numpy array or ExtensionArray of the same dtype as
1949 categorical.categories.dtype.
1950 """
1951 # if we are a datetime and period index, return Index to keep metadata
1952 if needs_i8_conversion(self.categories.dtype):
1953 return self.categories.take(self._codes, fill_value=NaT)._values
1954 elif is_integer_dtype(self.categories.dtype) and -1 in self._codes:
1955 return (
1956 self.categories.astype("object")
1957 .take(self._codes, fill_value=np.nan)
1958 ._values
1959 )
1960 return np.array(self)
1961
1962 def check_for_ordered(self, op) -> None:
1963 """assert that we are ordered"""
1964 if not self.ordered:
1965 raise TypeError(
1966 f"Categorical is not ordered for operation {op}\n"
1967 "you can use .as_ordered() to change the "
1968 "Categorical to an ordered one\n"
1969 )
1970
1971 def argsort(
1972 self, *, ascending: bool = True, kind: SortKind = "quicksort", **kwargs
1973 ) -> npt.NDArray[np.intp]:
1974 """
1975 Return the indices that would sort the Categorical.
1976
1977 Missing values are sorted at the end.
1978
1979 Parameters
1980 ----------
1981 ascending : bool, default True
1982 Whether the indices should result in an ascending
1983 or descending sort.
1984 kind : {'quicksort', 'mergesort', 'heapsort', 'stable'}, optional
1985 Sorting algorithm.
1986 **kwargs:
1987 passed through to :func:`numpy.argsort`.
1988
1989 Returns
1990 -------
1991 np.ndarray[np.intp]
1992
1993 See Also
1994 --------
1995 numpy.ndarray.argsort
1996
1997 Notes
1998 -----
1999 While an ordering is applied to the category values, arg-sorting
2000 in this context refers more to organizing and grouping together
2001 based on matching category values. Thus, this function can be
2002 called on an unordered Categorical instance unlike the functions
2003 'Categorical.min' and 'Categorical.max'.
2004
2005 Examples
2006 --------
2007 >>> pd.Categorical(["b", "b", "a", "c"]).argsort()
2008 array([2, 0, 1, 3])
2009
2010 >>> cat = pd.Categorical(
2011 ... ["b", "b", "a", "c"], categories=["c", "b", "a"], ordered=True
2012 ... )
2013 >>> cat.argsort()
2014 array([3, 0, 1, 2])
2015
2016 Missing values are placed at the end
2017
2018 >>> cat = pd.Categorical([2, None, 1])
2019 >>> cat.argsort()
2020 array([2, 0, 1])
2021 """
2022 return super().argsort(ascending=ascending, kind=kind, **kwargs)
2023
2024 @overload
2025 def sort_values(
2026 self,
2027 *,
2028 inplace: Literal[False] = ...,
2029 ascending: bool = ...,
2030 na_position: str = ...,
2031 ) -> Self: ...
2032
2033 @overload
2034 def sort_values(
2035 self, *, inplace: Literal[True], ascending: bool = ..., na_position: str = ...
2036 ) -> None: ...
2037
2038 def sort_values(
2039 self,
2040 *,
2041 inplace: bool = False,
2042 ascending: bool = True,
2043 na_position: str = "last",
2044 ) -> Self | None:
2045 """
2046 Sort the Categorical by category value returning a new
2047 Categorical by default.
2048
2049 While an ordering is applied to the category values, sorting in this
2050 context refers more to organizing and grouping together based on
2051 matching category values. Thus, this function can be called on an
2052 unordered Categorical instance unlike the functions 'Categorical.min'
2053 and 'Categorical.max'.
2054
2055 Parameters
2056 ----------
2057 inplace : bool, default False
2058 Do operation in place.
2059 ascending : bool, default True
2060 Order ascending. Passing False orders descending. The
2061 ordering parameter provides the method by which the
2062 category values are organized.
2063 na_position : {'first', 'last'} (optional, default='last')
2064 'first' puts NaNs at the beginning
2065 'last' puts NaNs at the end
2066
2067 Returns
2068 -------
2069 Categorical or None
2070
2071 See Also
2072 --------
2073 Categorical.sort
2074 Series.sort_values
2075
2076 Examples
2077 --------
2078 >>> c = pd.Categorical([1, 2, 2, 1, 5])
2079 >>> c
2080 [1, 2, 2, 1, 5]
2081 Categories (3, int64): [1, 2, 5]
2082 >>> c.sort_values()
2083 [1, 1, 2, 2, 5]
2084 Categories (3, int64): [1, 2, 5]
2085 >>> c.sort_values(ascending=False)
2086 [5, 2, 2, 1, 1]
2087 Categories (3, int64): [1, 2, 5]
2088
2089 >>> c = pd.Categorical([1, 2, 2, 1, 5])
2090
2091 'sort_values' behaviour with NaNs. Note that 'na_position'
2092 is independent of the 'ascending' parameter:
2093
2094 >>> c = pd.Categorical([np.nan, 2, 2, np.nan, 5])
2095 >>> c
2096 [NaN, 2, 2, NaN, 5]
2097 Categories (2, int64): [2, 5]
2098 >>> c.sort_values()
2099 [2, 2, 5, NaN, NaN]
2100 Categories (2, int64): [2, 5]
2101 >>> c.sort_values(ascending=False)
2102 [5, 2, 2, NaN, NaN]
2103 Categories (2, int64): [2, 5]
2104 >>> c.sort_values(na_position="first")
2105 [NaN, NaN, 2, 2, 5]
2106 Categories (2, int64): [2, 5]
2107 >>> c.sort_values(ascending=False, na_position="first")
2108 [NaN, NaN, 5, 2, 2]
2109 Categories (2, int64): [2, 5]
2110 """
2111 inplace = validate_bool_kwarg(inplace, "inplace")
2112 if na_position not in ["last", "first"]:
2113 raise ValueError(f"invalid na_position: {na_position!r}")
2114
2115 sorted_idx = nargsort(self, ascending=ascending, na_position=na_position)
2116
2117 if not inplace:
2118 codes = self._codes[sorted_idx]
2119 return self._from_backing_data(codes)
2120 self._codes[:] = self._codes[sorted_idx]
2121 return None
2122
2123 def _rank(
2124 self,
2125 *,
2126 axis: AxisInt = 0,
2127 method: str = "average",
2128 na_option: str = "keep",
2129 ascending: bool = True,
2130 pct: bool = False,
2131 ):
2132 """
2133 See Series.rank.__doc__.
2134 """
2135 if axis != 0:
2136 raise NotImplementedError
2137 vff = self._values_for_rank()
2138 return algorithms.rank(
2139 vff,
2140 axis=axis,
2141 method=method,
2142 na_option=na_option,
2143 ascending=ascending,
2144 pct=pct,
2145 )
2146
2147 def _values_for_rank(self) -> np.ndarray:
2148 """
2149 For correctly ranking ordered categorical data. See GH#15420
2150
2151 Ordered categorical data should be ranked on the basis of
2152 codes with -1 translated to NaN.
2153
2154 Returns
2155 -------
2156 numpy.array
2157
2158 """
2159 from pandas import Series
2160
2161 if self.ordered:
2162 values = self.codes
2163 mask = values == -1
2164 if mask.any():
2165 values = values.astype("float64")
2166 values[mask] = np.nan
2167 elif is_any_real_numeric_dtype(self.categories.dtype):
2168 values = np.array(self)
2169 else:
2170 # reorder the categories (so rank can use the float codes)
2171 # instead of passing an object array to rank
2172 values = np.array(
2173 self.rename_categories(
2174 Series(self.categories, copy=False).rank().values
2175 )
2176 )
2177 return values
2178
2179 def _hash_pandas_object(
2180 self, *, encoding: str, hash_key: str, categorize: bool
2181 ) -> npt.NDArray[np.uint64]:
2182 """
2183 Hash a Categorical by hashing its categories, and then mapping the codes
2184 to the hashes.
2185
2186 Parameters
2187 ----------
2188 encoding : str
2189 hash_key : str
2190 categorize : bool
2191 Ignored for Categorical.
2192
2193 Returns
2194 -------
2195 np.ndarray[uint64]
2196 """
2197 # Note we ignore categorize, as we are already Categorical.
2198 from pandas.core.util.hashing import hash_array
2199
2200 # Convert ExtensionArrays to ndarrays
2201 values = np.asarray(self.categories._values)
2202 hashed = hash_array(values, encoding, hash_key, categorize=False)
2203
2204 # we have uint64, as we don't directly support missing values
2205 # we don't want to use take_nd which will coerce to float
2206 # instead, directly construct the result with a
2207 # max(np.uint64) as the missing value indicator
2208 #
2209 # TODO: GH#15362
2210
2211 mask = self.isna()
2212 if len(hashed):
2213 result = hashed.take(self._codes)
2214 else:
2215 result = np.zeros(len(mask), dtype="uint64")
2216
2217 if mask.any():
2218 result[mask] = lib.u8max
2219
2220 return result
2221
2222 # ------------------------------------------------------------------
2223 # NDArrayBackedExtensionArray compat
2224
2225 @property
2226 def _codes(self) -> np.ndarray:
2227 return self._ndarray
2228
2229 def _box_func(self, i: int):
2230 if i == -1:
2231 return np.nan
2232 return self.categories[i]
2233
2234 def _unbox_scalar(self, key) -> int:
2235 # searchsorted is very performance sensitive. By converting codes
2236 # to same dtype as self.codes, we get much faster performance.
2237 code = self.categories.get_loc(key)
2238 code = self._ndarray.dtype.type(code)
2239 return code
2240
2241 # ------------------------------------------------------------------
2242
2243 def __iter__(self) -> Iterator:
2244 """
2245 Returns an Iterator over the values of this Categorical.
2246 """
2247 if self.ndim == 1:
2248 return iter(self._internal_get_values().tolist())
2249 else:
2250 return (self[n] for n in range(len(self)))
2251
2252 def __contains__(self, key) -> bool:
2253 """
2254 Returns True if `key` is in this Categorical.
2255 """
2256 # if key is a NaN, check if any NaN is in self.
2257 if is_valid_na_for_dtype(key, self.categories.dtype):
2258 return bool(self.isna().any())
2259
2260 return contains(self, key, container=self._codes)
2261
2262 # ------------------------------------------------------------------
2263 # Rendering Methods
2264
2265 # error: Return type "None" of "_formatter" incompatible with return
2266 # type "Callable[[Any], str | None]" in supertype "ExtensionArray"
2267 def _formatter(self, boxed: bool = False) -> None: # type: ignore[override]
2268 # Returning None here will cause format_array to do inference.
2269 return None
2270
2271 def _repr_categories(self) -> list[str]:
2272 """
2273 return the base repr for the categories
2274 """
2275 max_categories = (
2276 10
2277 if get_option("display.max_categories") == 0
2278 else get_option("display.max_categories")
2279 )
2280 from pandas.io.formats import format as fmt
2281
2282 formatter = None
2283 if self.categories.dtype == "str" or self.categories.dtype == "string": # noqa: PLR1714 (repeated-equality-comparison)
2284 # the extension array formatter defaults to boxed=True in format_array
2285 # override here to boxed=False to be consistent with QUOTE_NONNUMERIC
2286 formatter = cast(ExtensionArray, self.categories._values)._formatter(
2287 boxed=False
2288 )
2289
2290 format_array = partial(
2291 fmt.format_array, formatter=formatter, quoting=QUOTE_NONNUMERIC
2292 )
2293 if len(self.categories) > max_categories:
2294 num = max_categories // 2
2295 head = format_array(self.categories[:num]._values)
2296 tail = format_array(self.categories[-num:]._values)
2297 category_strs = [*head, "...", *tail]
2298 else:
2299 category_strs = format_array(self.categories._values)
2300
2301 # Strip all leading spaces, which format_array adds for columns...
2302 category_strs = [x.strip() for x in category_strs]
2303 return category_strs
2304
2305 def _get_repr_footer(self) -> str:
2306 """
2307 Returns a string representation of the footer.
2308 """
2309 category_strs = self._repr_categories()
2310 dtype = str(self.categories.dtype)
2311 levheader = f"Categories ({len(self.categories)}, {dtype}): "
2312 width, _ = get_terminal_size()
2313 max_width = get_option("display.width") or width
2314 if console.in_ipython_frontend():
2315 # 0 = no breaks
2316 max_width = 0
2317 levstring = ""
2318 start = True
2319 cur_col_len = len(levheader) # header
2320 sep_len, sep = (3, " < ") if self.ordered else (2, ", ")
2321 linesep = f"{sep.rstrip()}\n" # remove whitespace
2322 for val in category_strs:
2323 if max_width != 0 and cur_col_len + sep_len + len(val) > max_width:
2324 levstring += linesep + (" " * (len(levheader) + 1))
2325 cur_col_len = len(levheader) + 1 # header + a whitespace
2326 elif not start:
2327 levstring += sep
2328 cur_col_len += len(val)
2329 levstring += val
2330 start = False
2331 # replace to simple save space by
2332 return f"{levheader}[{levstring.replace(' < ... < ', ' ... ')}]"
2333
2334 def _get_values_repr(self) -> str:
2335 from pandas.io.formats import format as fmt
2336
2337 assert len(self) > 0
2338
2339 vals = self._internal_get_values()
2340 fmt_values = fmt.format_array(
2341 vals,
2342 None,
2343 float_format=None,
2344 na_rep="NaN",
2345 quoting=QUOTE_NONNUMERIC,
2346 )
2347
2348 fmt_values = [i.strip() for i in fmt_values]
2349 joined = ", ".join(fmt_values)
2350 result = "[" + joined + "]"
2351 return result
2352
2353 def __repr__(self) -> str:
2354 """
2355 String representation.
2356 """
2357 footer = self._get_repr_footer()
2358 length = len(self)
2359 max_len = 10
2360 if length > max_len:
2361 # In long cases we do not display all entries, so we add Length
2362 # information to the __repr__.
2363 num = max_len // 2
2364 head = self[:num]._get_values_repr()
2365 tail = self[-(max_len - num) :]._get_values_repr()
2366 body = f"{head[:-1]}, ..., {tail[1:]}"
2367 length_info = f"Length: {len(self)}"
2368 result = f"{body}\n{length_info}\n{footer}"
2369 elif length > 0:
2370 body = self._get_values_repr()
2371 result = f"{body}\n{footer}"
2372 else:
2373 # In the empty case we use a comma instead of newline to get
2374 # a more compact __repr__
2375 body = "[]"
2376 result = f"{body}, {footer}"
2377
2378 return result
2379
2380 # ------------------------------------------------------------------
2381
2382 def _validate_listlike(self, value):
2383 # NB: here we assume scalar-like tuples have already been excluded
2384 value = extract_array(value, extract_numpy=True)
2385
2386 # require identical categories set
2387 if isinstance(value, Categorical):
2388 if self.dtype != value.dtype:
2389 raise TypeError(
2390 "Cannot set a Categorical with another, "
2391 "without identical categories"
2392 )
2393 # dtype equality implies categories_match_up_to_permutation
2394 value = self._encode_with_my_categories(value)
2395 return value._codes
2396
2397 from pandas import Index
2398
2399 # tupleize_cols=False for e.g. test_fillna_iterable_category GH#41914
2400 to_add = Index._with_infer(value, tupleize_cols=False, copy=False).difference(
2401 self.categories
2402 )
2403
2404 # no assignments of values not in categories, but it's always ok to set
2405 # something to np.nan
2406 if len(to_add) and not isna(to_add).all():
2407 raise TypeError(
2408 "Cannot setitem on a Categorical with a new "
2409 "category, set the categories first"
2410 )
2411
2412 codes = self.categories.get_indexer(value)
2413 return codes.astype(self._ndarray.dtype, copy=False)
2414
2415 def _reverse_indexer(self) -> dict[Hashable, npt.NDArray[np.intp]]:
2416 """
2417 Compute the inverse of a categorical, returning
2418 a dict of categories -> indexers.
2419
2420 *This is an internal function*
2421
2422 Returns
2423 -------
2424 Dict[Hashable, np.ndarray[np.intp]]
2425 dict of categories -> indexers
2426
2427 Examples
2428 --------
2429 >>> c = pd.Categorical(list("aabca"))
2430 >>> c
2431 ['a', 'a', 'b', 'c', 'a']
2432 Categories (3, str): ['a', 'b', 'c']
2433 >>> c.categories
2434 Index(['a', 'b', 'c'], dtype='str')
2435 >>> c.codes
2436 array([0, 0, 1, 2, 0], dtype=int8)
2437 >>> c._reverse_indexer()
2438 {'a': array([0, 1, 4]), 'b': array([2]), 'c': array([3])}
2439
2440 """
2441 categories = self.categories
2442 r, counts = libalgos.groupsort_indexer(
2443 ensure_platform_int(self.codes), categories.size
2444 )
2445 counts = ensure_int64(counts).cumsum()
2446 _result = (r[start:end] for start, end in itertools.pairwise(counts))
2447 return dict(zip(categories, _result, strict=True))
2448
2449 # ------------------------------------------------------------------
2450 # Reductions
2451
2452 def _reduce(
2453 self, name: str, *, skipna: bool = True, keepdims: bool = False, **kwargs
2454 ):
2455 result = super()._reduce(name, skipna=skipna, keepdims=keepdims, **kwargs)
2456 if name in ["argmax", "argmin"]:
2457 # don't wrap in Categorical!
2458 return result
2459 if keepdims:
2460 return type(self)(result, dtype=self.dtype)
2461 else:
2462 return result
2463
2464 def min(self, *, skipna: bool = True, **kwargs):
2465 """
2466 The minimum value of the object.
2467
2468 Only ordered `Categoricals` have a minimum!
2469
2470 Raises
2471 ------
2472 TypeError
2473 If the `Categorical` is not `ordered`.
2474
2475 Returns
2476 -------
2477 min : the minimum of this `Categorical`, NA value if empty
2478 """
2479 nv.validate_minmax_axis(kwargs.get("axis", 0))
2480 nv.validate_min((), kwargs)
2481 self.check_for_ordered("min")
2482
2483 if not len(self._codes):
2484 return self.dtype.na_value
2485
2486 good = self._codes != -1
2487 if not good.all():
2488 if skipna and good.any():
2489 pointer = self._codes[good].min()
2490 else:
2491 return np.nan
2492 else:
2493 pointer = self._codes.min()
2494 return self._wrap_reduction_result(None, pointer)
2495
2496 def max(self, *, skipna: bool = True, **kwargs):
2497 """
2498 The maximum value of the object.
2499
2500 Only ordered `Categoricals` have a maximum!
2501
2502 Raises
2503 ------
2504 TypeError
2505 If the `Categorical` is not `ordered`.
2506
2507 Returns
2508 -------
2509 max : the maximum of this `Categorical`, NA if array is empty
2510 """
2511 nv.validate_minmax_axis(kwargs.get("axis", 0))
2512 nv.validate_max((), kwargs)
2513 self.check_for_ordered("max")
2514
2515 if not len(self._codes):
2516 return self.dtype.na_value
2517
2518 good = self._codes != -1
2519 if not good.all():
2520 if skipna and good.any():
2521 pointer = self._codes[good].max()
2522 else:
2523 return np.nan
2524 else:
2525 pointer = self._codes.max()
2526 return self._wrap_reduction_result(None, pointer)
2527
2528 def _mode(self, dropna: bool = True) -> Categorical:
2529 codes = self._codes
2530 mask = None
2531 if dropna:
2532 mask = self.isna()
2533
2534 res_codes, _ = algorithms.mode(codes, mask=mask)
2535 res_codes = cast(np.ndarray, res_codes)
2536 assert res_codes.dtype == codes.dtype
2537 res = self._from_backing_data(res_codes)
2538 return res
2539
2540 # ------------------------------------------------------------------
2541 # ExtensionArray Interface
2542
2543 def unique(self) -> Self:
2544 """
2545 Return the ``Categorical`` which ``categories`` and ``codes`` are
2546 unique.
2547
2548 Returns
2549 -------
2550 Categorical
2551
2552 See Also
2553 --------
2554 pandas.unique
2555 CategoricalIndex.unique
2556 Series.unique : Return unique values of Series object.
2557
2558 Examples
2559 --------
2560 >>> pd.Categorical(list("baabc")).unique()
2561 ['b', 'a', 'c']
2562 Categories (3, str): ['a', 'b', 'c']
2563 >>> pd.Categorical(list("baab"), categories=list("abc"), ordered=True).unique()
2564 ['b', 'a']
2565 Categories (3, str): ['a' < 'b' < 'c']
2566 """
2567 return super().unique()
2568
2569 def equals(self, other: object) -> bool:
2570 """
2571 Returns True if categorical arrays are equal.
2572
2573 Parameters
2574 ----------
2575 other : `Categorical`
2576
2577 Returns
2578 -------
2579 bool
2580 """
2581 if not isinstance(other, Categorical):
2582 return False
2583 elif self._categories_match_up_to_permutation(other):
2584 other = self._encode_with_my_categories(other)
2585 return np.array_equal(self._codes, other._codes)
2586 return False
2587
2588 def _accumulate(self, name: str, skipna: bool = True, **kwargs) -> Self:
2589 func: Callable
2590 if name == "cummin":
2591 func = np.minimum.accumulate
2592 elif name == "cummax":
2593 func = np.maximum.accumulate
2594 else:
2595 raise TypeError(f"Accumulation {name} not supported for {type(self)}")
2596 self.check_for_ordered(name)
2597
2598 codes = self.codes.copy()
2599 mask = self.isna()
2600 if func == np.minimum.accumulate:
2601 codes[mask] = np.iinfo(codes.dtype.type).max
2602 # no need to change codes for maximum because codes[mask] is already -1
2603 if not skipna:
2604 mask = np.maximum.accumulate(mask)
2605
2606 codes = func(codes)
2607 codes[mask] = -1
2608 return self._simple_new(codes, dtype=self._dtype)
2609
2610 @classmethod
2611 def _concat_same_type(cls, to_concat: Sequence[Self], axis: AxisInt = 0) -> Self:
2612 from pandas.core.dtypes.concat import union_categoricals
2613
2614 first = to_concat[0]
2615 if axis >= first.ndim:
2616 raise ValueError(
2617 f"axis {axis} is out of bounds for array of dimension {first.ndim}"
2618 )
2619
2620 if axis == 1:
2621 # Flatten, concatenate then reshape
2622 if not all(x.ndim == 2 for x in to_concat):
2623 raise ValueError
2624
2625 # pass correctly-shaped to union_categoricals
2626 tc_flat = []
2627 for obj in to_concat:
2628 tc_flat.extend([obj[:, i] for i in range(obj.shape[1])])
2629
2630 res_flat = cls._concat_same_type(tc_flat, axis=0)
2631
2632 result = res_flat.reshape(len(first), -1, order="F")
2633 return result
2634
2635 # error: Incompatible types in assignment (expression has type "Categorical",
2636 # variable has type "Self")
2637 result = union_categoricals(to_concat) # type: ignore[assignment]
2638 return result
2639
2640 # ------------------------------------------------------------------
2641
2642 def _encode_with_my_categories(self, other: Categorical) -> Categorical:
2643 """
2644 Re-encode another categorical using this Categorical's categories.
2645
2646 Notes
2647 -----
2648 This assumes we have already checked
2649 self._categories_match_up_to_permutation(other).
2650 """
2651 # Indexing on codes is more efficient if categories are the same,
2652 # so we can apply some optimizations based on the degree of
2653 # dtype-matching.
2654 codes = recode_for_categories(
2655 other.codes, other.categories, self.categories, copy=False
2656 )
2657 return self._from_backing_data(codes)
2658
2659 def _categories_match_up_to_permutation(self, other: Categorical) -> bool:
2660 """
2661 Returns True if categoricals are the same dtype
2662 same categories, and same ordered
2663
2664 Parameters
2665 ----------
2666 other : Categorical
2667
2668 Returns
2669 -------
2670 bool
2671 """
2672 return hash(self.dtype) == hash(other.dtype)
2673
2674 def describe(self) -> DataFrame:
2675 """
2676 Describes this Categorical
2677
2678 Returns
2679 -------
2680 description: `DataFrame`
2681 A dataframe with frequency and counts by category.
2682 """
2683 counts = self.value_counts(dropna=False)
2684 freqs = counts / counts.sum()
2685
2686 from pandas import Index
2687 from pandas.core.reshape.concat import concat
2688
2689 result = concat([counts, freqs], ignore_index=True, axis=1)
2690 result.columns = Index(["counts", "freqs"])
2691 result.index.name = "categories"
2692
2693 return result
2694
2695 def isin(self, values: ArrayLike) -> npt.NDArray[np.bool_]:
2696 """
2697 Check whether `values` are contained in Categorical.
2698
2699 Return a boolean NumPy Array showing whether each element in
2700 the Categorical matches an element in the passed sequence of
2701 `values` exactly.
2702
2703 Parameters
2704 ----------
2705 values : np.ndarray or ExtensionArray
2706 The sequence of values to test. Passing in a single string will
2707 raise a ``TypeError``. Instead, turn a single string into a
2708 list of one element.
2709
2710 Returns
2711 -------
2712 np.ndarray[bool]
2713
2714 Raises
2715 ------
2716 TypeError
2717 * If `values` is not a set or list-like
2718
2719 See Also
2720 --------
2721 pandas.Series.isin : Equivalent method on Series.
2722
2723 Examples
2724 --------
2725 >>> s = pd.Categorical(["llama", "cow", "llama", "beetle", "llama", "hippo"])
2726 >>> s.isin(["cow", "llama"])
2727 array([ True, True, True, False, True, False])
2728
2729 Passing a single string as ``s.isin('llama')`` will raise an error. Use
2730 a list of one element instead:
2731
2732 >>> s.isin(["llama"])
2733 array([ True, False, True, False, True, False])
2734 """
2735 null_mask = np.asarray(isna(values))
2736 code_values = self.categories.get_indexer_for(values)
2737 code_values = code_values[null_mask | (code_values >= 0)]
2738 return algorithms.isin(self.codes, code_values)
2739
2740 # ------------------------------------------------------------------------
2741 # String methods interface
2742 def _str_map(
2743 self, f, na_value=lib.no_default, dtype=np.dtype("object"), convert: bool = True
2744 ):
2745 # Optimization to apply the callable `f` to the categories once
2746 # and rebuild the result by `take`ing from the result with the codes.
2747 # Returns the same type as the object-dtype implementation though.
2748 categories = self.categories
2749 codes = self.codes
2750 if categories.dtype == "string":
2751 result = categories.array._str_map(f, na_value, dtype) # type: ignore[attr-defined]
2752 if (
2753 categories.dtype.na_value is np.nan # type: ignore[union-attr]
2754 and is_bool_dtype(dtype)
2755 and (na_value is lib.no_default or isna(na_value))
2756 ):
2757 # NaN propagates as False for functions with boolean return type
2758 na_value = False
2759 else:
2760 from pandas.core.arrays import NumpyExtensionArray
2761
2762 result = NumpyExtensionArray(categories.to_numpy())._str_map(
2763 f, na_value, dtype
2764 )
2765 return take_nd(result, codes, fill_value=na_value)
2766
2767 def _str_get_dummies(self, sep: str = "|", dtype: NpDtype | None = None):
2768 # sep may not be in categories. Just bail on this.
2769 from pandas.core.arrays import NumpyExtensionArray
2770
2771 return NumpyExtensionArray(self.to_numpy(str, na_value="NaN"))._str_get_dummies(
2772 sep, dtype
2773 )
2774
2775 # ------------------------------------------------------------------------
2776 # GroupBy Methods
2777
2778 def _groupby_op(
2779 self,
2780 *,
2781 how: str,
2782 has_dropped_na: bool,
2783 min_count: int,
2784 ngroups: int,
2785 ids: npt.NDArray[np.intp],
2786 **kwargs,
2787 ):
2788 from pandas.core.groupby.ops import WrappedCythonOp
2789
2790 kind = WrappedCythonOp.get_kind_from_how(how)
2791 op = WrappedCythonOp(how=how, kind=kind, has_dropped_na=has_dropped_na)
2792
2793 dtype = self.dtype
2794 if how in ["sum", "prod", "cumsum", "cumprod", "skew", "kurt"]:
2795 raise TypeError(f"{dtype} type does not support {how} operations")
2796 if how in ["min", "max", "rank", "idxmin", "idxmax"] and not dtype.ordered:
2797 # raise TypeError instead of NotImplementedError to ensure we
2798 # don't go down a group-by-group path, since in the empty-groups
2799 # case that would fail to raise
2800 raise TypeError(f"Cannot perform {how} with non-ordered Categorical")
2801 if how not in [
2802 "rank",
2803 "any",
2804 "all",
2805 "first",
2806 "last",
2807 "min",
2808 "max",
2809 "idxmin",
2810 "idxmax",
2811 ]:
2812 if kind == "transform":
2813 raise TypeError(f"{dtype} type does not support {how} operations")
2814 raise TypeError(f"{dtype} dtype does not support aggregation '{how}'")
2815
2816 result_mask = None
2817 mask = self.isna()
2818 if how == "rank":
2819 assert self.ordered # checked earlier
2820 npvalues = self._ndarray
2821 elif how in ["first", "last", "min", "max", "idxmin", "idxmax"]:
2822 npvalues = self._ndarray
2823 result_mask = np.zeros(ngroups, dtype=bool)
2824 else:
2825 # any/all
2826 npvalues = self.astype(bool)
2827
2828 res_values = op._cython_op_ndim_compat(
2829 npvalues,
2830 min_count=min_count,
2831 ngroups=ngroups,
2832 comp_ids=ids,
2833 mask=mask,
2834 result_mask=result_mask,
2835 **kwargs,
2836 )
2837
2838 if how in op.cast_blocklist:
2839 return res_values
2840 elif how in ["first", "last", "min", "max"]:
2841 res_values[result_mask == 1] = -1
2842 return self._from_backing_data(res_values)
2843
2844
2845# The Series.cat accessor
2846
2847
2848@delegate_names(
2849 delegate=Categorical, accessors=["categories", "ordered"], typ="property"
2850)
2851@delegate_names(
2852 delegate=Categorical,
2853 accessors=[
2854 "rename_categories",
2855 "reorder_categories",
2856 "add_categories",
2857 "remove_categories",
2858 "remove_unused_categories",
2859 "set_categories",
2860 "as_ordered",
2861 "as_unordered",
2862 ],
2863 typ="method",
2864)
2865class CategoricalAccessor(PandasDelegate, PandasObject, NoNewAttributesMixin):
2866 """
2867 Accessor object for categorical properties of the Series values.
2868
2869 Parameters
2870 ----------
2871 data : Series or CategoricalIndex
2872 The object to which the categorical accessor is attached.
2873
2874 See Also
2875 --------
2876 Series.dt : Accessor object for datetimelike properties of the Series values.
2877 Series.sparse : Accessor for sparse matrix data types.
2878
2879 Examples
2880 --------
2881 >>> s = pd.Series(list("abbccc")).astype("category")
2882 >>> s
2883 0 a
2884 1 b
2885 2 b
2886 3 c
2887 4 c
2888 5 c
2889 dtype: category
2890 Categories (3, str): ['a', 'b', 'c']
2891
2892 >>> s.cat.categories
2893 Index(['a', 'b', 'c'], dtype='str')
2894
2895 >>> s.cat.rename_categories(list("cba"))
2896 0 c
2897 1 b
2898 2 b
2899 3 a
2900 4 a
2901 5 a
2902 dtype: category
2903 Categories (3, str): ['c', 'b', 'a']
2904
2905 >>> s.cat.reorder_categories(list("cba"))
2906 0 a
2907 1 b
2908 2 b
2909 3 c
2910 4 c
2911 5 c
2912 dtype: category
2913 Categories (3, str): ['c', 'b', 'a']
2914
2915 >>> s.cat.add_categories(["d", "e"])
2916 0 a
2917 1 b
2918 2 b
2919 3 c
2920 4 c
2921 5 c
2922 dtype: category
2923 Categories (5, str): ['a', 'b', 'c', 'd', 'e']
2924
2925 >>> s.cat.remove_categories(["a", "c"])
2926 0 NaN
2927 1 b
2928 2 b
2929 3 NaN
2930 4 NaN
2931 5 NaN
2932 dtype: category
2933 Categories (1, str): ['b']
2934
2935 >>> s1 = s.cat.add_categories(["d", "e"])
2936 >>> s1.cat.remove_unused_categories()
2937 0 a
2938 1 b
2939 2 b
2940 3 c
2941 4 c
2942 5 c
2943 dtype: category
2944 Categories (3, str): ['a', 'b', 'c']
2945
2946 >>> s.cat.set_categories(list("abcde"))
2947 0 a
2948 1 b
2949 2 b
2950 3 c
2951 4 c
2952 5 c
2953 dtype: category
2954 Categories (5, str): ['a', 'b', 'c', 'd', 'e']
2955
2956 >>> s.cat.as_ordered()
2957 0 a
2958 1 b
2959 2 b
2960 3 c
2961 4 c
2962 5 c
2963 dtype: category
2964 Categories (3, str): ['a' < 'b' < 'c']
2965
2966 >>> s.cat.as_unordered()
2967 0 a
2968 1 b
2969 2 b
2970 3 c
2971 4 c
2972 5 c
2973 dtype: category
2974 Categories (3, str): ['a', 'b', 'c']
2975 """
2976
2977 def __init__(self, data) -> None:
2978 self._validate(data)
2979 self._parent = data.values
2980 self._index = data.index
2981 self._name = data.name
2982 self._freeze()
2983
2984 @staticmethod
2985 def _validate(data) -> None:
2986 if not isinstance(data.dtype, CategoricalDtype):
2987 raise AttributeError("Can only use .cat accessor with a 'category' dtype")
2988
2989 def _delegate_property_get(self, name: str):
2990 return getattr(self._parent, name)
2991
2992 def _delegate_property_set(self, name: str, new_values) -> None:
2993 setattr(self._parent, name, new_values)
2994
2995 @property
2996 def codes(self) -> Series:
2997 """
2998 Return Series of codes as well as the index.
2999
3000 See Also
3001 --------
3002 Series.cat.categories : Return the categories of this categorical.
3003 Series.cat.as_ordered : Set the Categorical to be ordered.
3004 Series.cat.as_unordered : Set the Categorical to be unordered.
3005
3006 Examples
3007 --------
3008 >>> raw_cate = pd.Categorical(["a", "b", None, "a"], categories=["a", "b"])
3009 >>> ser = pd.Series(raw_cate)
3010 >>> ser.cat.codes
3011 0 0
3012 1 1
3013 2 -1
3014 3 0
3015 dtype: int8
3016 """
3017 from pandas import Series
3018
3019 return Series(self._parent.codes, index=self._index)
3020
3021 def _delegate_method(self, name: str, *args, **kwargs):
3022 from pandas import Series
3023
3024 method = getattr(self._parent, name)
3025 res = method(*args, **kwargs)
3026 if res is not None:
3027 return Series(res, index=self._index, name=self._name)
3028
3029
3030# utility routines
3031
3032
3033def _get_codes_for_values(
3034 values: Index | Series | ExtensionArray | np.ndarray,
3035 categories: Index,
3036) -> np.ndarray:
3037 """
3038 utility routine to turn values into codes given the specified categories
3039
3040 If `values` is known to be a Categorical, use recode_for_categories instead.
3041 """
3042 codes = categories.get_indexer_for(values)
3043 wrong = (codes == -1) & ~isna(values)
3044 if wrong.any():
3045 warnings.warn(
3046 "Constructing a Categorical with a dtype and values containing "
3047 "non-null entries not in that dtype's categories is deprecated "
3048 "and will raise in a future version.",
3049 Pandas4Warning,
3050 stacklevel=find_stack_level(),
3051 )
3052 return coerce_indexer_dtype(codes, categories)
3053
3054
3055def recode_for_categories(
3056 codes: np.ndarray,
3057 old_categories,
3058 new_categories,
3059 *,
3060 copy: bool = True,
3061 warn: bool = False,
3062) -> np.ndarray:
3063 """
3064 Convert a set of codes for to a new set of categories
3065
3066 Parameters
3067 ----------
3068 codes : np.ndarray
3069 old_categories, new_categories : Index
3070 copy: bool, default True
3071 Whether to copy if the codes are unchanged.
3072 warn : bool, default False
3073 Whether to warn on silent-NA mapping.
3074
3075 Returns
3076 -------
3077 new_codes : np.ndarray[np.int64]
3078
3079 Examples
3080 --------
3081 >>> old_cat = pd.Index(["b", "a", "c"])
3082 >>> new_cat = pd.Index(["a", "b"])
3083 >>> codes = np.array([0, 1, 1, 2])
3084 >>> recode_for_categories(codes, old_cat, new_cat, copy=True)
3085 array([ 1, 0, 0, -1], dtype=int8)
3086 """
3087 if len(old_categories) == 0:
3088 # All null anyway, so just retain the nulls
3089 if copy:
3090 return codes.copy()
3091 return codes
3092 elif new_categories.equals(old_categories):
3093 # Same categories, so no need to actually recode
3094 if copy:
3095 return codes.copy()
3096 return codes
3097
3098 codes_in_old_cats = new_categories.get_indexer_for(old_categories)
3099 if warn:
3100 wrong = codes_in_old_cats == -1
3101 if wrong.any():
3102 warnings.warn(
3103 "Constructing a Categorical with a dtype and values containing "
3104 "non-null entries not in that dtype's categories is deprecated "
3105 "and will raise in a future version.",
3106 Pandas4Warning,
3107 stacklevel=find_stack_level(),
3108 )
3109 indexer = coerce_indexer_dtype(codes_in_old_cats, new_categories)
3110 new_codes = take_nd(indexer, codes, fill_value=-1)
3111 return new_codes
3112
3113
3114def factorize_from_iterable(values) -> tuple[np.ndarray, Index]:
3115 """
3116 Factorize an input `values` into `categories` and `codes`. Preserves
3117 categorical dtype in `categories`.
3118
3119 Parameters
3120 ----------
3121 values : list-like
3122
3123 Returns
3124 -------
3125 codes : ndarray
3126 categories : Index
3127 If `values` has a categorical dtype, then `categories` is
3128 a CategoricalIndex keeping the categories and order of `values`.
3129 """
3130 from pandas import CategoricalIndex
3131
3132 if not is_list_like(values):
3133 raise TypeError("Input must be list-like")
3134
3135 categories: Index
3136
3137 vdtype = getattr(values, "dtype", None)
3138 if isinstance(vdtype, CategoricalDtype):
3139 values = extract_array(values)
3140 # The Categorical we want to build has the same categories
3141 # as values but its codes are by def [0, ..., len(n_categories) - 1]
3142 cat_codes = np.arange(len(values.categories), dtype=values.codes.dtype)
3143 cat = Categorical.from_codes(cat_codes, dtype=values.dtype, validate=False)
3144
3145 categories = CategoricalIndex(cat)
3146 codes = values.codes
3147 else:
3148 # The value of ordered is irrelevant since we don't use cat as such,
3149 # but only the resulting categories, the order of which is independent
3150 # from ordered. Set ordered to False as default. See GH #15457
3151 cat = Categorical(values, ordered=False)
3152 categories = cat.categories
3153 codes = cat.codes
3154 return codes, categories
3155
3156
3157def factorize_from_iterables(iterables) -> tuple[list[np.ndarray], list[Index]]:
3158 """
3159 A higher-level wrapper over `factorize_from_iterable`.
3160
3161 Parameters
3162 ----------
3163 iterables : list-like of list-likes
3164
3165 Returns
3166 -------
3167 codes : list of ndarrays
3168 categories : list of Indexes
3169
3170 Notes
3171 -----
3172 See `factorize_from_iterable` for more info.
3173 """
3174 if len(iterables) == 0:
3175 # For consistency, it should return two empty lists.
3176 return [], []
3177
3178 codes, categories = zip(
3179 *(factorize_from_iterable(it) for it in iterables),
3180 strict=True,
3181 )
3182 return list(codes), list(categories)