1from __future__ import annotations
2
3from typing import (
4 TYPE_CHECKING,
5 Any,
6 Literal,
7 Self,
8 cast,
9)
10
11import numpy as np
12
13from pandas._libs import index as libindex
14from pandas.util._decorators import (
15 cache_readonly,
16 set_module,
17)
18
19from pandas.core.dtypes.common import is_scalar
20from pandas.core.dtypes.dtypes import CategoricalDtype
21from pandas.core.dtypes.missing import (
22 is_valid_na_for_dtype,
23)
24
25from pandas.core.arrays.categorical import (
26 Categorical,
27 contains,
28)
29from pandas.core.construction import extract_array
30from pandas.core.indexes.base import (
31 Index,
32 maybe_extract_name,
33)
34from pandas.core.indexes.extension import NDArrayBackedExtensionIndex
35
36if TYPE_CHECKING:
37 from collections.abc import (
38 Callable,
39 Hashable,
40 )
41
42 from pandas._typing import (
43 ArrayLike,
44 Axes,
45 Dtype,
46 DtypeObj,
47 Level,
48 ReindexMethod,
49 npt,
50 )
51
52 from pandas import Series
53 from pandas.core.indexes.multi import MultiIndex
54
55
56@set_module("pandas")
57class CategoricalIndex(NDArrayBackedExtensionIndex):
58 """
59 Index based on an underlying :class:`Categorical`.
60
61 CategoricalIndex, like Categorical, can only take on a limited,
62 and usually fixed, number of possible values (`categories`). Also,
63 like Categorical, it might have an order, but numerical operations
64 (additions, divisions, ...) are not possible.
65
66 Parameters
67 ----------
68 data : array-like (1-dimensional)
69 The values of the categorical. If `categories` are given, values not in
70 `categories` will be replaced with NaN.
71 categories : index-like, optional
72 The categories for the categorical. Items need to be unique.
73 If the categories are not given here (and also not in `dtype`), they
74 will be inferred from the `data`.
75 ordered : bool, optional
76 Whether or not this categorical is treated as an ordered
77 categorical. If not given here or in `dtype`, the resulting
78 categorical will be unordered.
79 dtype : CategoricalDtype or "category", optional
80 If :class:`CategoricalDtype`, cannot be used together with
81 `categories` or `ordered`.
82 copy : bool, default False
83 Make a copy of input ndarray.
84 name : object, optional
85 Name to be stored in the index.
86
87 Attributes
88 ----------
89 codes
90 categories
91 ordered
92
93 Methods
94 -------
95 rename_categories
96 reorder_categories
97 add_categories
98 remove_categories
99 remove_unused_categories
100 set_categories
101 as_ordered
102 as_unordered
103 map
104
105 Raises
106 ------
107 ValueError
108 If the categories do not validate.
109 TypeError
110 If an explicit ``ordered=True`` is given but no `categories` and the
111 `values` are not sortable.
112
113 See Also
114 --------
115 Index : The base pandas Index type.
116 Categorical : A categorical array.
117 CategoricalDtype : Type for categorical data.
118
119 Notes
120 -----
121 See the `user guide
122 <https://pandas.pydata.org/pandas-docs/stable/user_guide/advanced.html#categoricalindex>`__
123 for more.
124
125 Examples
126 --------
127 >>> pd.CategoricalIndex(["a", "b", "c", "a", "b", "c"])
128 CategoricalIndex(['a', 'b', 'c', 'a', 'b', 'c'],
129 categories=['a', 'b', 'c'], ordered=False, dtype='category')
130
131 ``CategoricalIndex`` can also be instantiated from a ``Categorical``:
132
133 >>> c = pd.Categorical(["a", "b", "c", "a", "b", "c"])
134 >>> pd.CategoricalIndex(c)
135 CategoricalIndex(['a', 'b', 'c', 'a', 'b', 'c'],
136 categories=['a', 'b', 'c'], ordered=False, dtype='category')
137
138 Ordered ``CategoricalIndex`` can have a min and max value.
139
140 >>> ci = pd.CategoricalIndex(
141 ... ["a", "b", "c", "a", "b", "c"], ordered=True, categories=["c", "b", "a"]
142 ... )
143 >>> ci
144 CategoricalIndex(['a', 'b', 'c', 'a', 'b', 'c'],
145 categories=['c', 'b', 'a'], ordered=True, dtype='category')
146 >>> ci.min()
147 'c'
148 """
149
150 _typ = "categoricalindex"
151 _data_cls = Categorical
152
153 @property
154 def _can_hold_strings(self) -> bool:
155 return self.categories._can_hold_strings
156
157 @cache_readonly
158 def _should_fallback_to_positional(self) -> bool:
159 return self.categories._should_fallback_to_positional
160
161 _data: Categorical
162 _values: Categorical
163
164 # --------------------------------------------------------------------
165 # Categories/Codes/Ordered
166
167 @property
168 def codes(self) -> np.ndarray:
169 """
170 The category codes of this categorical index.
171
172 Codes are an array of integers which are the positions of the actual
173 values in the categories array. There is no setter.
174
175 Returns
176 -------
177 ndarray[int]
178 A non-writable view of the ``codes`` array.
179
180 See Also
181 --------
182 Categorical.from_codes : Make a Categorical from codes.
183 CategoricalIndex : An Index with an underlying ``Categorical``.
184
185 Examples
186 --------
187 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a", "b", "c"])
188 >>> ci.codes
189 array([0, 1, 2, 0, 1, 2], dtype=int8)
190
191 >>> ci = pd.CategoricalIndex(["a", "c"], categories=["c", "b", "a"])
192 >>> ci.codes
193 array([2, 0], dtype=int8)
194 """
195 return self._data.codes
196
197 @property
198 def categories(self) -> Index:
199 """
200 The categories of this CategoricalIndex.
201
202 These are the unique values the index may hold, in category order.
203 Use the ``*_categories`` methods to return an index with changed
204 categories.
205
206 See Also
207 --------
208 CategoricalIndex.rename_categories : Rename categories.
209 CategoricalIndex.reorder_categories : Reorder categories.
210 CategoricalIndex.add_categories : Add new categories.
211 CategoricalIndex.remove_categories : Remove the specified categories.
212 CategoricalIndex.remove_unused_categories : Remove categories which
213 are not used.
214 CategoricalIndex.set_categories : Set the categories to the specified ones.
215
216 Examples
217 --------
218 >>> ci = pd.CategoricalIndex(["a", "c", "b", "a", "c", "b"])
219 >>> ci.categories
220 Index(['a', 'b', 'c'], dtype='str')
221
222 >>> ci = pd.CategoricalIndex(["a", "c"], categories=["c", "b", "a"])
223 >>> ci.categories
224 Index(['c', 'b', 'a'], dtype='str')
225 """
226 return self._data.categories
227
228 @property
229 def ordered(self) -> bool | None:
230 """
231 Whether the categories have an ordered relationship.
232
233 This property returns True if the categories are ordered, meaning
234 they have a meaningful order that allows comparison operations.
235
236 See Also
237 --------
238 CategoricalIndex.as_ordered : Set the CategoricalIndex to be ordered.
239 CategoricalIndex.as_unordered : Set the CategoricalIndex to be unordered.
240
241 Examples
242 --------
243 >>> ci = pd.CategoricalIndex(["a", "b"], ordered=True)
244 >>> ci.ordered
245 True
246
247 >>> ci = pd.CategoricalIndex(["a", "b"], ordered=False)
248 >>> ci.ordered
249 False
250 """
251 return self._data.ordered
252
253 def _wrap_categorical_result(self, result: Categorical) -> Self:
254 return type(self)._simple_new(result, name=self.name)
255
256 def rename_categories(self, new_categories: Any) -> Self:
257 """
258 Rename categories.
259
260 This method is commonly used to re-label or adjust the
261 category names in categorical data without changing the
262 underlying data. It is useful in situations where you want
263 to modify the labels used for clarity, consistency,
264 or readability.
265
266 Parameters
267 ----------
268 new_categories : list-like, dict-like or callable
269
270 New categories which will replace old categories.
271
272 * list-like: all items must be unique and the number of items in
273 the new categories must match the existing number of categories.
274
275 * dict-like: specifies a mapping from
276 old categories to new. Categories not contained in the mapping
277 are passed through and extra categories in the mapping are
278 ignored.
279
280 * callable : a callable that is called on all items in the old
281 categories and whose return values comprise the new categories.
282
283 Returns
284 -------
285 CategoricalIndex
286 CategoricalIndex with renamed categories.
287
288 Raises
289 ------
290 ValueError
291 If new categories are list-like and do not have the same number of
292 items as the current categories or do not validate as categories
293
294 See Also
295 --------
296 CategoricalIndex.reorder_categories : Reorder categories.
297 CategoricalIndex.add_categories : Add new categories.
298 CategoricalIndex.remove_categories : Remove the specified categories.
299 CategoricalIndex.remove_unused_categories : Remove categories which
300 are not used.
301 CategoricalIndex.set_categories : Set the categories to the specified
302 ones.
303
304 Examples
305 --------
306 >>> ci = pd.CategoricalIndex(["a", "a", "b"])
307 >>> ci.rename_categories([0, 1])
308 CategoricalIndex([0, 0, 1], categories=[0, 1], ordered=False, dtype='category')
309
310 For dict-like ``new_categories``, extra keys are ignored and
311 categories not in the dictionary are passed through
312
313 >>> ci.rename_categories({"a": "A", "c": "C"})
314 CategoricalIndex(['A', 'A', 'b'], categories=['A', 'b'], ordered=False,
315 dtype='category')
316
317 You may also provide a callable to create the new categories
318
319 >>> ci.rename_categories(lambda x: x.upper())
320 CategoricalIndex(['A', 'A', 'B'], categories=['A', 'B'], ordered=False,
321 dtype='category')
322 """
323 result = self._data.rename_categories(new_categories)
324 return self._wrap_categorical_result(result)
325
326 def reorder_categories(
327 self, new_categories: Axes, ordered: bool | None = None
328 ) -> Self:
329 """
330 Reorder categories as specified in new_categories.
331
332 ``new_categories`` need to include all old categories and no new category
333 items.
334
335 Parameters
336 ----------
337 new_categories : Index-like
338 The categories in new order.
339 ordered : bool, optional
340 Whether or not the categorical is treated as an ordered categorical.
341 If not given, do not change the ordered information.
342
343 Returns
344 -------
345 CategoricalIndex
346 CategoricalIndex with reordered categories.
347
348 Raises
349 ------
350 ValueError
351 If the new categories do not contain all old category items or any
352 new ones
353
354 See Also
355 --------
356 CategoricalIndex.rename_categories : Rename categories.
357 CategoricalIndex.add_categories : Add new categories.
358 CategoricalIndex.remove_categories : Remove the specified categories.
359 CategoricalIndex.remove_unused_categories : Remove categories which
360 are not used.
361 CategoricalIndex.set_categories : Set the categories to the specified
362 ones.
363
364 Examples
365 --------
366 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"])
367 >>> ci
368 CategoricalIndex(['a', 'b', 'c', 'a'], categories=['a', 'b', 'c'],
369 ordered=False, dtype='category')
370 >>> ci.reorder_categories(["c", "b", "a"], ordered=True)
371 CategoricalIndex(['a', 'b', 'c', 'a'], categories=['c', 'b', 'a'],
372 ordered=True, dtype='category')
373 """
374 result = self._data.reorder_categories(new_categories, ordered=ordered)
375 return self._wrap_categorical_result(result)
376
377 def add_categories(self, new_categories: Any) -> Self:
378 """
379 Add new categories.
380
381 `new_categories` will be included at the last/highest place in the
382 categories and will be unused directly after this call.
383
384 Parameters
385 ----------
386 new_categories : category or list-like of category
387 The new categories to be included.
388
389 Returns
390 -------
391 CategoricalIndex
392 CategoricalIndex with new categories added.
393
394 Raises
395 ------
396 ValueError
397 If the new categories include old categories or do not validate as
398 categories
399
400 See Also
401 --------
402 CategoricalIndex.rename_categories : Rename categories.
403 CategoricalIndex.reorder_categories : Reorder categories.
404 CategoricalIndex.remove_categories : Remove the specified categories.
405 CategoricalIndex.remove_unused_categories : Remove categories which
406 are not used.
407 CategoricalIndex.set_categories : Set the categories to the specified
408 ones.
409
410 Examples
411 --------
412 >>> ci = pd.CategoricalIndex(["c", "b", "c"])
413 >>> ci
414 CategoricalIndex(['c', 'b', 'c'], categories=['b', 'c'], ordered=False,
415 dtype='category')
416
417 >>> ci.add_categories(["d", "a"])
418 CategoricalIndex(['c', 'b', 'c'], categories=['b', 'c', 'd', 'a'],
419 ordered=False, dtype='category')
420 """
421 result = self._data.add_categories(new_categories)
422 return self._wrap_categorical_result(result)
423
424 def remove_categories(self, removals: Any) -> Self:
425 """
426 Remove the specified categories.
427
428 The ``removals`` argument must be a subset of the current categories.
429 Any values that were part of the removed categories will be set to NaN.
430
431 Parameters
432 ----------
433 removals : category or list of categories
434 The categories which should be removed.
435
436 Returns
437 -------
438 CategoricalIndex
439 CategoricalIndex with removed categories.
440
441 Raises
442 ------
443 ValueError
444 If the removals are not contained in the categories
445
446 See Also
447 --------
448 CategoricalIndex.rename_categories : Rename categories.
449 CategoricalIndex.reorder_categories : Reorder categories.
450 CategoricalIndex.add_categories : Add new categories.
451 CategoricalIndex.remove_unused_categories : Remove categories which
452 are not used.
453 CategoricalIndex.set_categories : Set the categories to the specified
454 ones.
455
456 Examples
457 --------
458 >>> ci = pd.CategoricalIndex(["a", "c", "b", "c", "d"])
459 >>> ci
460 CategoricalIndex(['a', 'c', 'b', 'c', 'd'], categories=['a', 'b', 'c', 'd'],
461 ordered=False, dtype='category')
462
463 >>> ci.remove_categories(["d", "a"])
464 CategoricalIndex([nan, 'c', 'b', 'c', nan], categories=['b', 'c'],
465 ordered=False, dtype='category')
466 """
467 result = self._data.remove_categories(removals)
468 return self._wrap_categorical_result(result)
469
470 def remove_unused_categories(self) -> Self:
471 """
472 Remove categories which are not used.
473
474 This method is useful when working with datasets
475 that undergo dynamic changes where categories may no longer be
476 relevant, allowing to maintain a clean, efficient data structure.
477
478 Returns
479 -------
480 CategoricalIndex
481 CategoricalIndex with unused categories dropped.
482
483 See Also
484 --------
485 CategoricalIndex.rename_categories : Rename categories.
486 CategoricalIndex.reorder_categories : Reorder categories.
487 CategoricalIndex.add_categories : Add new categories.
488 CategoricalIndex.remove_categories : Remove the specified categories.
489 CategoricalIndex.set_categories : Set the categories to the specified
490 ones.
491
492 Examples
493 --------
494 >>> ci = pd.CategoricalIndex(["a", "c", "b", "c", "d"])
495 >>> ci
496 CategoricalIndex(['a', 'c', 'b', 'c', 'd'], categories=['a', 'b', 'c', 'd'],
497 ordered=False, dtype='category')
498
499 >>> ci = ci[[0, 1, 0, 1, 1]]
500 >>> ci
501 CategoricalIndex(['a', 'c', 'a', 'c', 'c'], categories=['a', 'b', 'c', 'd'],
502 ordered=False, dtype='category')
503
504 >>> ci.remove_unused_categories()
505 CategoricalIndex(['a', 'c', 'a', 'c', 'c'], categories=['a', 'c'],
506 ordered=False, dtype='category')
507 """
508 result = self._data.remove_unused_categories()
509 return self._wrap_categorical_result(result)
510
511 def set_categories(
512 self,
513 new_categories: Axes,
514 ordered: bool | None = None,
515 rename: bool = False,
516 ) -> Self:
517 """
518 Set the categories to the specified new categories.
519
520 ``new_categories`` can include new categories (which will result in
521 unused categories) or remove old categories (which results in values
522 set to ``NaN``). If ``rename=True``, the categories will simply be renamed
523 (less or more items than in old categories will result in values set to
524 ``NaN`` or in unused categories respectively).
525
526 This method can be used to perform more than one action of adding,
527 removing, and reordering simultaneously and is therefore faster than
528 performing the individual steps via the more specialised methods.
529
530 On the other hand this method does not check whether the old categories
531 are included in the new categories on a reorder, which can result in
532 surprising changes.
533
534 Parameters
535 ----------
536 new_categories : Index-like
537 The categories in new order.
538 ordered : bool, default None
539 Whether or not the categorical is treated as an ordered categorical.
540 If not given, do not change the ordered information.
541 rename : bool, default False
542 Whether or not the new_categories should be considered as a rename
543 of the old categories or as reordered categories.
544
545 Returns
546 -------
547 CategoricalIndex
548 CategoricalIndex with the new categories.
549
550 Raises
551 ------
552 ValueError
553 If new_categories does not validate as categories
554
555 See Also
556 --------
557 CategoricalIndex.rename_categories : Rename categories.
558 CategoricalIndex.reorder_categories : Reorder categories.
559 CategoricalIndex.add_categories : Add new categories.
560 CategoricalIndex.remove_categories : Remove the specified categories.
561 CategoricalIndex.remove_unused_categories : Remove categories which
562 are not used.
563
564 Examples
565 --------
566 >>> ci = pd.CategoricalIndex(
567 ... ["a", "b", "c", None], categories=["a", "b", "c"], ordered=True
568 ... )
569 >>> ci
570 CategoricalIndex(['a', 'b', 'c', nan], categories=['a', 'b', 'c'],
571 ordered=True, dtype='category')
572
573 >>> ci.set_categories(["A", "b", "c"])
574 CategoricalIndex([nan, 'b', 'c', nan], categories=['A', 'b', 'c'],
575 ordered=True, dtype='category')
576 >>> ci.set_categories(["A", "b", "c"], rename=True)
577 CategoricalIndex(['A', 'b', 'c', nan], categories=['A', 'b', 'c'],
578 ordered=True, dtype='category')
579 """
580 result = self._data.set_categories(
581 new_categories, ordered=ordered, rename=rename
582 )
583 return self._wrap_categorical_result(result)
584
585 def as_ordered(self) -> Self:
586 """
587 Set the CategoricalIndex to be ordered.
588
589 This method returns a new CategoricalIndex with the ordered attribute
590 set to True, enabling comparison operations between categories.
591
592 Returns
593 -------
594 CategoricalIndex
595 Ordered CategoricalIndex.
596
597 See Also
598 --------
599 CategoricalIndex.as_unordered : Set the CategoricalIndex to be unordered.
600
601 Examples
602 --------
603 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"])
604 >>> ci.ordered
605 False
606 >>> ci = ci.as_ordered()
607 >>> ci.ordered
608 True
609 """
610 result = self._data.as_ordered()
611 return self._wrap_categorical_result(result)
612
613 def as_unordered(self) -> Self:
614 """
615 Set the CategoricalIndex to be unordered.
616
617 This method returns a new CategoricalIndex with the ordered attribute
618 set to False, disabling comparison operations between categories.
619
620 Returns
621 -------
622 CategoricalIndex
623 Unordered CategoricalIndex.
624
625 See Also
626 --------
627 CategoricalIndex.as_ordered : Set the CategoricalIndex to be ordered.
628
629 Examples
630 --------
631 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"], ordered=True)
632 >>> ci.ordered
633 True
634 >>> ci = ci.as_unordered()
635 >>> ci.ordered
636 False
637 """
638 result = self._data.as_unordered()
639 return self._wrap_categorical_result(result)
640
641 def min(self, *, skipna: bool = True, **kwargs: Any) -> Any: # type: ignore[override]
642 """
643 Return the minimum value of the CategoricalIndex.
644
645 Only an ordered CategoricalIndex has a minimum.
646
647 Parameters
648 ----------
649 skipna : bool, default True
650 Exclude NA/null values when showing the result.
651 **kwargs
652 Additional keyword arguments passed through to the reduction.
653
654 Returns
655 -------
656 scalar
657 The minimum of this CategoricalIndex, NA value if empty.
658
659 Raises
660 ------
661 TypeError
662 If the CategoricalIndex is not ordered.
663
664 See Also
665 --------
666 CategoricalIndex.max : Return the maximum value of the CategoricalIndex.
667
668 Examples
669 --------
670 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"], ordered=True)
671 >>> ci.min()
672 'a'
673 """
674 return self._data.min(skipna=skipna, **kwargs)
675
676 def max(self, *, skipna: bool = True, **kwargs: Any) -> Any: # type: ignore[override]
677 """
678 Return the maximum value of the CategoricalIndex.
679
680 Only an ordered CategoricalIndex has a maximum.
681
682 Parameters
683 ----------
684 skipna : bool, default True
685 Exclude NA/null values when showing the result.
686 **kwargs
687 Additional keyword arguments passed through to the reduction.
688
689 Returns
690 -------
691 scalar
692 The maximum of this CategoricalIndex, NA value if empty.
693
694 Raises
695 ------
696 TypeError
697 If the CategoricalIndex is not ordered.
698
699 See Also
700 --------
701 CategoricalIndex.min : Return the minimum value of the CategoricalIndex.
702
703 Examples
704 --------
705 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"], ordered=True)
706 >>> ci.max()
707 'c'
708 """
709 return self._data.max(skipna=skipna, **kwargs)
710
711 def _reverse_indexer(self) -> dict[Hashable, npt.NDArray[np.intp]]:
712 """See Categorical._reverse_indexer."""
713 return self._data._reverse_indexer()
714
715 @property
716 def _engine_type(self) -> type[libindex.IndexEngine]:
717 # self.codes can have dtype int8, int16, int32 or int64, so we need
718 # to return the corresponding engine type (libindex.Int8Engine, etc.).
719 return {
720 np.int8: libindex.Int8Engine,
721 np.int16: libindex.Int16Engine,
722 np.int32: libindex.Int32Engine,
723 np.int64: libindex.Int64Engine,
724 }[self.codes.dtype.type]
725
726 # --------------------------------------------------------------------
727 # Constructors
728
729 def __new__(
730 cls,
731 data: Axes | None = None,
732 categories: Axes | None = None,
733 ordered: bool | None = None,
734 dtype: Dtype | None = None,
735 copy: bool = False,
736 name: Hashable | None = None,
737 ) -> Self:
738 name = maybe_extract_name(name, data, cls)
739
740 if is_scalar(data):
741 # GH#38944 include None here, which pre-2.0 subbed in []
742 cls._raise_scalar_data_error(data)
743
744 data = Categorical(
745 data, categories=categories, ordered=ordered, dtype=dtype, copy=copy
746 )
747
748 return cls._simple_new(data, name=name)
749
750 # --------------------------------------------------------------------
751
752 def _is_dtype_compat(self, other: Index) -> Categorical:
753 """
754 *this is an internal non-public method*
755
756 provide a comparison between the dtype of self and other (coercing if
757 needed)
758
759 Parameters
760 ----------
761 other : Index
762
763 Returns
764 -------
765 Categorical
766
767 Raises
768 ------
769 TypeError if the dtypes are not compatible
770 """
771 if isinstance(other.dtype, CategoricalDtype):
772 cat = extract_array(other)
773 cat = cast("Categorical", cat)
774 if not cat._categories_match_up_to_permutation(self._values):
775 raise TypeError(
776 "categories must match existing categories when appending"
777 )
778
779 elif other._is_multi:
780 # preempt raising NotImplementedError in isna call
781 raise TypeError("MultiIndex is not dtype-compatible with CategoricalIndex")
782 else:
783 values = other
784
785 codes = self.categories.get_indexer(values)
786 if ((codes == -1) & ~values.isna()).any():
787 # GH#37667 see test_equals_non_category
788 raise TypeError(
789 "categories must match existing categories when appending"
790 )
791 cat = Categorical(other, dtype=self.dtype)
792 other = CategoricalIndex(cat)
793 if not other.isin(values).all():
794 raise TypeError(
795 "cannot append a non-category item to a CategoricalIndex"
796 )
797 cat = other._values
798
799 return cat
800
801 def equals(self, other: object) -> bool:
802 """
803 Determine if two CategoricalIndex objects contain the same elements.
804
805 The order and orderedness of elements matters. The categories matter,
806 but the order of the categories matters only when ``ordered=True``.
807
808 Parameters
809 ----------
810 other : object
811 The CategoricalIndex object to compare with.
812
813 Returns
814 -------
815 bool
816 ``True`` if two :class:`pandas.CategoricalIndex` objects have equal
817 elements, ``False`` otherwise.
818
819 See Also
820 --------
821 Categorical.equals : Returns True if categorical arrays are equal.
822
823 Examples
824 --------
825 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a", "b", "c"])
826 >>> ci2 = pd.CategoricalIndex(pd.Categorical(["a", "b", "c", "a", "b", "c"]))
827 >>> ci.equals(ci2)
828 True
829
830 The order of elements matters.
831
832 >>> ci3 = pd.CategoricalIndex(["c", "b", "a", "a", "b", "c"])
833 >>> ci.equals(ci3)
834 False
835
836 The orderedness also matters.
837
838 >>> ci4 = ci.as_ordered()
839 >>> ci.equals(ci4)
840 False
841
842 The categories matter, but the order of the categories matters only when
843 ``ordered=True``.
844
845 >>> ci5 = ci.set_categories(["a", "b", "c", "d"])
846 >>> ci.equals(ci5)
847 False
848
849 >>> ci6 = ci.set_categories(["b", "c", "a"])
850 >>> ci.equals(ci6)
851 True
852 >>> ci_ordered = pd.CategoricalIndex(
853 ... ["a", "b", "c", "a", "b", "c"], ordered=True
854 ... )
855 >>> ci2_ordered = ci_ordered.set_categories(["b", "c", "a"])
856 >>> ci_ordered.equals(ci2_ordered)
857 False
858 """
859 if self.is_(other): # type: ignore[arg-type]
860 return True
861
862 if not isinstance(other, Index):
863 return False
864
865 try:
866 other = self._is_dtype_compat(other)
867 except (TypeError, ValueError):
868 return False
869
870 return self._data.equals(other)
871
872 # --------------------------------------------------------------------
873 # Rendering Methods
874
875 def _formatter_func(self, val: Hashable) -> str:
876 return self.categories._formatter_func(val)
877
878 def _format_attrs(self) -> list[tuple[str, str | int | bool | None]]:
879 """
880 Return a list of tuples of the (attr,formatted_value)
881 """
882 attrs: list[tuple[str, str | int | bool | None]]
883
884 attrs = [
885 (
886 "categories",
887 f"[{', '.join(self._data._repr_categories())}]",
888 ),
889 ("ordered", self.ordered),
890 ]
891 extra = super()._format_attrs()
892 return attrs + extra
893
894 # --------------------------------------------------------------------
895
896 @property
897 def inferred_type(self) -> str:
898 return "categorical"
899
900 def __contains__(self, key: Any) -> bool:
901 """
902 Return a boolean indicating whether the provided key is in the index.
903
904 Parameters
905 ----------
906 key : label
907 The key to check if it is present in the index.
908
909 Returns
910 -------
911 bool
912 Whether the key search is in the index.
913
914 Raises
915 ------
916 TypeError
917 If the key is not hashable.
918
919 See Also
920 --------
921 Index.isin : Returns an ndarray of boolean dtype indicating whether the
922 list-like key is in the index.
923
924 Examples
925 --------
926 >>> idx = pd.Index([1, 2, 3, 4])
927 >>> idx
928 Index([1, 2, 3, 4], dtype='int64')
929
930 >>> 2 in idx
931 True
932 >>> 6 in idx
933 False
934 """
935 # if key is a NaN, check if any NaN is in self.
936 if is_valid_na_for_dtype(key, self.categories.dtype):
937 return self.hasnans
938 if self.categories._typ == "rangeindex":
939 container: Index | libindex.IndexEngine | libindex.ExtensionEngine = (
940 self.categories
941 )
942 else:
943 container = self._engine
944 return contains(self, key, container=container)
945
946 def reindex(
947 self,
948 target: Axes,
949 method: ReindexMethod | None = None,
950 level: Level | None = None,
951 limit: int | None = None,
952 tolerance: float | None = None,
953 ) -> tuple[Index, npt.NDArray[np.intp] | None]:
954 """
955 Create index with target's values (move/add/delete values as necessary)
956
957 Returns
958 -------
959 new_index : pd.Index
960 Resulting index
961 indexer : np.ndarray[np.intp] or None
962 Indices of output values in original index
963
964 """
965 if method is not None:
966 raise NotImplementedError(
967 "argument method is not implemented for CategoricalIndex.reindex"
968 )
969 if level is not None:
970 raise NotImplementedError(
971 "argument level is not implemented for CategoricalIndex.reindex"
972 )
973 if limit is not None:
974 raise NotImplementedError(
975 "argument limit is not implemented for CategoricalIndex.reindex"
976 )
977 return super().reindex(target)
978
979 # --------------------------------------------------------------------
980 # Indexing Methods
981
982 def _maybe_cast_indexer(self, key: Hashable) -> int:
983 # GH#41933: we have to do this instead of self._data._validate_scalar
984 # because this will correctly get partial-indexing on Interval categories
985 try:
986 return self._data._unbox_scalar(key)
987 except KeyError:
988 if is_valid_na_for_dtype(key, self.categories.dtype):
989 return -1
990 raise
991
992 def _maybe_cast_listlike_indexer(self, values: Axes) -> CategoricalIndex:
993 if isinstance(values, CategoricalIndex):
994 values = values._data
995 if isinstance(values, Categorical):
996 # Indexing on codes is more efficient if categories are the same,
997 # so we can apply some optimizations based on the degree of
998 # dtype-matching.
999 cat = self._data._encode_with_my_categories(values)
1000 codes = cat._codes
1001 else:
1002 codes = self.categories.get_indexer(values)
1003 codes = codes.astype(self.codes.dtype, copy=False)
1004 cat = self._data._from_backing_data(codes)
1005 return type(self)._simple_new(cat)
1006
1007 # --------------------------------------------------------------------
1008
1009 def _is_comparable_dtype(self, dtype: DtypeObj) -> bool:
1010 return self.categories._is_comparable_dtype(dtype)
1011
1012 def _intersection(
1013 self, other: Index, sort: bool = False
1014 ) -> Index | ArrayLike | MultiIndex:
1015 # Reached only via Index.intersection after dtype reconciliation, so
1016 # other is necessarily a CategoricalIndex with matching dtype.
1017 # For unordered CategoricalIndex, libjoin operates on integer codes.
1018 # When two indexes have the same categories in different order, matching
1019 # codes map to different values, giving incorrect results (GH#55335).
1020 # Reorder other's categories to match self so the codes align.
1021 other = cast("CategoricalIndex", other)
1022 if not self.ordered and not self.categories.equals(other.categories):
1023 reordered = other._data.reorder_categories(self.categories)
1024 other = other._shallow_copy(reordered)
1025 return super()._intersection(other, sort=sort)
1026
1027 def _union(self, other: Index, sort: bool | None) -> Index | ArrayLike | MultiIndex:
1028 # See _intersection for explanation of GH#55335.
1029 other = cast("CategoricalIndex", other)
1030 if not self.ordered and not self.categories.equals(other.categories):
1031 reordered = other._data.reorder_categories(self.categories)
1032 other = other._shallow_copy(reordered)
1033 return super()._union(other, sort)
1034
1035 def map(
1036 self,
1037 mapper: Callable[..., Any] | dict[Hashable, Any] | Series,
1038 na_action: Literal["ignore"] | None = None,
1039 ) -> Index:
1040 """
1041 Map values using input an input mapping or function.
1042
1043 Maps the values (their categories, not the codes) of the index to new
1044 categories. If the mapping correspondence is one-to-one the result is a
1045 :class:`~pandas.CategoricalIndex` which has the same order property as
1046 the original, otherwise an :class:`~pandas.Index` is returned.
1047
1048 If a `dict` or :class:`~pandas.Series` is used any unmapped category is
1049 mapped to `NaN`. Note that if this happens an :class:`~pandas.Index`
1050 will be returned.
1051
1052 Parameters
1053 ----------
1054 mapper : function, dict, or Series
1055 Mapping correspondence.
1056 na_action : {None, 'ignore'}, default 'ignore'
1057 If 'ignore', propagate NaN values, without passing them to
1058 the mapping correspondence.
1059
1060 Returns
1061 -------
1062 pandas.CategoricalIndex or pandas.Index
1063 Mapped index.
1064
1065 See Also
1066 --------
1067 Index.map : Apply a mapping correspondence on an
1068 :class:`~pandas.Index`.
1069 Series.map : Apply a mapping correspondence on a
1070 :class:`~pandas.Series`.
1071 Series.apply : Apply more complex functions on a
1072 :class:`~pandas.Series`.
1073
1074 Examples
1075 --------
1076 >>> idx = pd.CategoricalIndex(["a", "b", "c"])
1077 >>> idx
1078 CategoricalIndex(['a', 'b', 'c'], categories=['a', 'b', 'c'],
1079 ordered=False, dtype='category')
1080 >>> idx.map(lambda x: x.upper())
1081 CategoricalIndex(['A', 'B', 'C'], categories=['A', 'B', 'C'],
1082 ordered=False, dtype='category')
1083 >>> idx.map({"a": "first", "b": "second", "c": "third"})
1084 CategoricalIndex(['first', 'second', 'third'], categories=['first',
1085 'second', 'third'], ordered=False, dtype='category')
1086
1087 If the mapping is one-to-one the ordering of the categories is
1088 preserved:
1089
1090 >>> idx = pd.CategoricalIndex(["a", "b", "c"], ordered=True)
1091 >>> idx
1092 CategoricalIndex(['a', 'b', 'c'], categories=['a', 'b', 'c'],
1093 ordered=True, dtype='category')
1094 >>> idx.map({"a": 3, "b": 2, "c": 1})
1095 CategoricalIndex([3, 2, 1], categories=[3, 2, 1], ordered=True,
1096 dtype='category')
1097
1098 If the mapping is not one-to-one an :class:`~pandas.Index` is returned:
1099
1100 >>> idx.map({"a": "first", "b": "second", "c": "first"})
1101 Index(['first', 'second', 'first'], dtype='str')
1102
1103 If a `dict` is used, all unmapped categories are mapped to `NaN` and
1104 the result is an :class:`~pandas.Index`:
1105
1106 >>> idx.map({"a": "first", "b": "second"})
1107 Index(['first', 'second', nan], dtype='str')
1108 """
1109 mapped = self._values.map(mapper, na_action=na_action)
1110 return Index(mapped, name=self.name, copy=False)