1from __future__ import annotations
2
3from collections.abc import (
4 Callable,
5 Collection,
6 Generator,
7 Hashable,
8 Iterable,
9 Sequence,
10)
11from functools import wraps
12from itertools import zip_longest
13from sys import getsizeof
14from typing import (
15 TYPE_CHECKING,
16 Any,
17 Literal,
18 Self,
19 cast,
20)
21import warnings
22
23import numpy as np
24
25from pandas._config import get_option
26
27from pandas._libs import (
28 algos as libalgos,
29 index as libindex,
30 lib,
31)
32from pandas._libs.hashtable import duplicated
33from pandas._typing import (
34 AnyAll,
35 AnyArrayLike,
36 Axis,
37 DropKeep,
38 DtypeObj,
39 F,
40 IgnoreRaise,
41 IndexLabel,
42 IndexT,
43 NaPosition,
44 Scalar,
45 Shape,
46 npt,
47)
48from pandas.compat.numpy import function as nv
49from pandas.errors import (
50 InvalidIndexError,
51 PerformanceWarning,
52 UnsortedIndexError,
53)
54from pandas.util._decorators import (
55 cache_readonly,
56 set_module,
57)
58from pandas.util._exceptions import find_stack_level
59
60from pandas.core.dtypes.cast import (
61 coerce_indexer_dtype,
62 maybe_unbox_numpy_scalar,
63)
64from pandas.core.dtypes.common import (
65 ensure_int64,
66 ensure_platform_int,
67 is_hashable,
68 is_integer,
69 is_iterator,
70 is_list_like,
71 is_object_dtype,
72 is_scalar,
73 is_string_dtype,
74 pandas_dtype,
75)
76from pandas.core.dtypes.dtypes import (
77 CategoricalDtype,
78 ExtensionDtype,
79)
80from pandas.core.dtypes.generic import (
81 ABCDataFrame,
82 ABCSeries,
83)
84from pandas.core.dtypes.inference import is_array_like
85from pandas.core.dtypes.missing import (
86 array_equivalent,
87 isna,
88)
89
90import pandas.core.algorithms as algos
91from pandas.core.array_algos.putmask import validate_putmask
92from pandas.core.arrays import (
93 Categorical,
94 ExtensionArray,
95)
96from pandas.core.arrays.categorical import (
97 factorize_from_iterables,
98 recode_for_categories,
99)
100import pandas.core.common as com
101from pandas.core.construction import sanitize_array
102import pandas.core.indexes.base as ibase
103from pandas.core.indexes.base import (
104 Index,
105 ensure_index,
106 get_unanimous_names,
107)
108from pandas.core.indexes.frozen import FrozenList
109from pandas.core.ops.invalid import make_invalid_op
110from pandas.core.sorting import (
111 get_group_index,
112 lexsort_indexer,
113)
114
115from pandas.io.formats.printing import pprint_thing
116
117if TYPE_CHECKING:
118 from pandas import (
119 CategoricalIndex,
120 DataFrame,
121 Series,
122 )
123
124
125class MultiIndexUInt64Engine(libindex.BaseMultiIndexCodesEngine, libindex.UInt64Engine):
126 """Manages a MultiIndex by mapping label combinations to positive integers.
127
128 The number of possible label combinations must not overflow the 64 bits integers.
129 """
130
131 _base = libindex.UInt64Engine
132 _codes_dtype = "uint64"
133
134
135class MultiIndexUInt32Engine(libindex.BaseMultiIndexCodesEngine, libindex.UInt32Engine):
136 """Manages a MultiIndex by mapping label combinations to positive integers.
137
138 The number of possible label combinations must not overflow the 32 bits integers.
139 """
140
141 _base = libindex.UInt32Engine
142 _codes_dtype = "uint32"
143
144
145class MultiIndexUInt16Engine(libindex.BaseMultiIndexCodesEngine, libindex.UInt16Engine):
146 """Manages a MultiIndex by mapping label combinations to positive integers.
147
148 The number of possible label combinations must not overflow the 16 bits integers.
149 """
150
151 _base = libindex.UInt16Engine
152 _codes_dtype = "uint16"
153
154
155class MultiIndexUInt8Engine(libindex.BaseMultiIndexCodesEngine, libindex.UInt8Engine):
156 """Manages a MultiIndex by mapping label combinations to positive integers.
157
158 The number of possible label combinations must not overflow the 8 bits integers.
159 """
160
161 _base = libindex.UInt8Engine
162 _codes_dtype = "uint8"
163
164
165class MultiIndexPyIntEngine(libindex.BaseMultiIndexCodesEngine, libindex.ObjectEngine):
166 """Manages a MultiIndex by mapping label combinations to positive integers.
167
168 This class manages those (extreme) cases in which the number of possible
169 label combinations overflows the 64 bits integers, and uses an ObjectEngine
170 containing Python integers.
171 """
172
173 _base = libindex.ObjectEngine
174 _codes_dtype = "object"
175
176
177def names_compat(meth: F) -> F:
178 """
179 A decorator to allow either `name` or `names` keyword but not both.
180
181 This makes it easier to share code with base class.
182 """
183
184 @wraps(meth)
185 def new_meth(self_or_cls, *args, **kwargs):
186 if "name" in kwargs and "names" in kwargs:
187 raise TypeError("Can only provide one of `names` and `name`")
188 if "name" in kwargs:
189 kwargs["names"] = kwargs.pop("name")
190
191 return meth(self_or_cls, *args, **kwargs)
192
193 return cast(F, new_meth)
194
195
196@set_module("pandas")
197class MultiIndex(Index):
198 """
199 A multi-level, or hierarchical, index object for pandas objects.
200
201 Parameters
202 ----------
203 levels : sequence of arrays
204 The unique labels for each level.
205 codes : sequence of arrays
206 Integers for each level designating which label at each location.
207 sortorder : optional int
208 Level of sortedness (must be lexicographically sorted by that
209 level).
210 names : optional sequence of objects
211 Names for each of the index levels. (name is accepted for compat).
212 copy : bool, default False
213 Copy the meta-data.
214 name : Label
215 Kept for compatibility with 1-dimensional Index. Should not be used.
216 verify_integrity : bool, default True
217 Check that the levels/codes are consistent and valid.
218
219 Attributes
220 ----------
221 names
222 levels
223 codes
224 nlevels
225 levshape
226 dtypes
227
228 Methods
229 -------
230 from_arrays
231 from_tuples
232 from_product
233 from_frame
234 set_levels
235 set_codes
236 to_frame
237 to_flat_index
238 sortlevel
239 droplevel
240 swaplevel
241 reorder_levels
242 remove_unused_levels
243 get_level_values
244 get_indexer
245 get_loc
246 get_locs
247 get_loc_level
248 drop
249
250 See Also
251 --------
252 MultiIndex.from_arrays : Convert list of arrays to MultiIndex.
253 MultiIndex.from_product : Create a MultiIndex from the cartesian product
254 of iterables.
255 MultiIndex.from_tuples : Convert list of tuples to a MultiIndex.
256 MultiIndex.from_frame : Make a MultiIndex from a DataFrame.
257 Index : The base pandas Index type.
258
259 Notes
260 -----
261 See the `user guide
262 <https://pandas.pydata.org/pandas-docs/stable/user_guide/advanced.html>`__
263 for more.
264
265 Examples
266 --------
267 A new ``MultiIndex`` is typically constructed using one of the helper
268 methods :meth:`MultiIndex.from_arrays`, :meth:`MultiIndex.from_product`
269 and :meth:`MultiIndex.from_tuples`. For example (using ``.from_arrays``):
270
271 >>> arrays = [[1, 1, 2, 2], ["red", "blue", "red", "blue"]]
272 >>> pd.MultiIndex.from_arrays(arrays, names=("number", "color"))
273 MultiIndex([(1, 'red'),
274 (1, 'blue'),
275 (2, 'red'),
276 (2, 'blue')],
277 names=['number', 'color'])
278
279 See further examples for how to construct a MultiIndex in the doc strings
280 of the mentioned helper methods.
281 """
282
283 _hidden_attrs = Index._hidden_attrs | frozenset()
284
285 # initialize to zero-length tuples to make everything work
286 _typ = "multiindex"
287 _names: list[Hashable | None] = []
288 _levels = FrozenList()
289 _codes = FrozenList()
290 _comparables = ["names"]
291
292 sortorder: int | None
293
294 # --------------------------------------------------------------------
295 # Constructors
296
297 def __new__(
298 cls,
299 levels=None,
300 codes=None,
301 sortorder=None,
302 names=None,
303 copy: bool = False,
304 name=None,
305 verify_integrity: bool = True,
306 ) -> Self:
307 # compat with Index
308 if name is not None:
309 names = name
310 if levels is None or codes is None:
311 raise TypeError("Must pass both levels and codes")
312 if len(levels) != len(codes):
313 raise ValueError("Length of levels and codes must be the same.")
314 if len(levels) == 0:
315 raise ValueError("Must pass non-zero number of levels/codes")
316
317 result = object.__new__(cls)
318 result._cache = {}
319
320 # we've already validated levels and codes, so shortcut here
321 result._set_levels(levels, copy=copy, validate=False)
322 result._set_codes(codes, copy=copy, validate=False)
323
324 result._names = [None] * len(levels)
325 if names is not None:
326 # handles name validation
327 result._set_names(names)
328
329 if sortorder is not None:
330 result.sortorder = int(sortorder)
331 else:
332 result.sortorder = sortorder
333
334 if verify_integrity:
335 new_codes = result._verify_integrity()
336 result._codes = new_codes
337
338 result._reset_identity()
339 result._references = None
340
341 return result
342
343 def _validate_codes(self, level: Index, code: np.ndarray) -> np.ndarray:
344 """
345 Reassign code values as -1 if their corresponding levels are NaN.
346
347 Parameters
348 ----------
349 code : Index
350 Code to reassign.
351 level : np.ndarray
352 Level to check for missing values (NaN, NaT, None).
353
354 Returns
355 -------
356 new code where code value = -1 if it corresponds
357 to a level with missing values (NaN, NaT, None).
358 """
359 null_mask = isna(level)
360 if np.any(null_mask):
361 code = np.where(null_mask[code], -1, code)
362 return code
363
364 def _verify_integrity(
365 self,
366 codes: list | None = None,
367 levels: list | None = None,
368 levels_to_verify: list[int] | range | None = None,
369 ) -> FrozenList:
370 """
371 Parameters
372 ----------
373 codes : optional list
374 Codes to check for validity. Defaults to current codes.
375 levels : optional list
376 Levels to check for validity. Defaults to current levels.
377 levels_to_validate: optional list
378 Specifies the levels to verify.
379
380 Raises
381 ------
382 ValueError
383 If length of levels and codes don't match, if the codes for any
384 level would exceed level bounds, or there are any duplicate levels.
385
386 Returns
387 -------
388 new codes where code value = -1 if it corresponds to a
389 NaN level.
390 """
391 # NOTE: Currently does not check, among other things, that cached
392 # nlevels matches nor that sortorder matches actually sortorder.
393 codes = codes or self.codes
394 levels = levels or self.levels
395 if levels_to_verify is None:
396 levels_to_verify = range(len(levels))
397
398 if len(levels) != len(codes):
399 raise ValueError(
400 "Length of levels and codes must match. NOTE: "
401 "this index is in an inconsistent state."
402 )
403 codes_length = len(codes[0])
404 for i in levels_to_verify:
405 level = levels[i]
406 level_codes = codes[i]
407
408 if len(level_codes) != codes_length:
409 raise ValueError(
410 f"Unequal code lengths: {[len(code_) for code_ in codes]}"
411 )
412 if len(level_codes) and level_codes.max() >= len(level):
413 raise ValueError(
414 f"On level {i}, code max ({level_codes.max()}) >= length of "
415 f"level ({len(level)}). NOTE: this index is in an "
416 "inconsistent state"
417 )
418 if len(level_codes) and level_codes.min() < -1:
419 raise ValueError(f"On level {i}, code value ({level_codes.min()}) < -1")
420 if not level.is_unique:
421 raise ValueError(
422 f"Level values must be unique: {list(level)} on level {i}"
423 )
424 if self.sortorder is not None:
425 if self.sortorder > _lexsort_depth(self.codes, self.nlevels):
426 raise ValueError(
427 "Value for sortorder must be inferior or equal to actual "
428 f"lexsort_depth: sortorder {self.sortorder} "
429 f"with lexsort_depth {_lexsort_depth(self.codes, self.nlevels)}"
430 )
431
432 result_codes = []
433 for i in range(len(levels)):
434 if i in levels_to_verify:
435 result_codes.append(self._validate_codes(levels[i], codes[i]))
436 else:
437 result_codes.append(codes[i])
438
439 new_codes = FrozenList(result_codes)
440 return new_codes
441
442 @classmethod
443 def from_arrays(
444 cls,
445 arrays,
446 sortorder: int | None = None,
447 names: Sequence[Hashable] | Hashable | lib.NoDefault = lib.no_default,
448 ) -> MultiIndex:
449 """
450 Convert arrays to MultiIndex.
451
452 Parameters
453 ----------
454 arrays : list / sequence of array-likes
455 Each array-like gives one level's value for each data point.
456 len(arrays) is the number of levels.
457 sortorder : int or None
458 Level of sortedness (must be lexicographically sorted by that
459 level).
460 names : list / sequence of str, optional
461 Names for the levels in the index.
462
463 Returns
464 -------
465 MultiIndex
466
467 See Also
468 --------
469 MultiIndex.from_tuples : Convert list of tuples to MultiIndex.
470 MultiIndex.from_product : Make a MultiIndex from cartesian product
471 of iterables.
472 MultiIndex.from_frame : Make a MultiIndex from a DataFrame.
473
474 Examples
475 --------
476 >>> arrays = [[1, 1, 2, 2], ["red", "blue", "red", "blue"]]
477 >>> pd.MultiIndex.from_arrays(arrays, names=("number", "color"))
478 MultiIndex([(1, 'red'),
479 (1, 'blue'),
480 (2, 'red'),
481 (2, 'blue')],
482 names=['number', 'color'])
483 """
484 error_msg = "Input must be a list / sequence of array-likes."
485 if not is_list_like(arrays):
486 raise TypeError(error_msg)
487 if is_iterator(arrays):
488 arrays = list(arrays)
489
490 # Check if elements of array are list-like
491 for array in arrays:
492 if not is_list_like(array):
493 raise TypeError(error_msg)
494
495 # Check if lengths of all arrays are equal or not,
496 # raise ValueError, if not
497 for i in range(1, len(arrays)):
498 if len(arrays[i]) != len(arrays[i - 1]):
499 raise ValueError("all arrays must be same length")
500
501 codes, levels = factorize_from_iterables(arrays)
502 if names is lib.no_default:
503 names = [getattr(arr, "name", None) for arr in arrays]
504
505 return cls(
506 levels=levels,
507 codes=codes,
508 sortorder=sortorder,
509 names=names,
510 verify_integrity=False,
511 )
512
513 @classmethod
514 @names_compat
515 def from_tuples(
516 cls,
517 tuples: Iterable[tuple[Hashable, ...]],
518 sortorder: int | None = None,
519 names: Sequence[Hashable] | Hashable | None = None,
520 ) -> MultiIndex:
521 """
522 Convert list of tuples to MultiIndex.
523
524 Parameters
525 ----------
526 tuples : list / sequence of tuple-likes
527 Each tuple is the index of one row/column.
528 sortorder : int or None
529 Level of sortedness (must be lexicographically sorted by that
530 level).
531 names : list / sequence of str, optional
532 Names for the levels in the index.
533
534 Returns
535 -------
536 MultiIndex
537
538 See Also
539 --------
540 MultiIndex.from_arrays : Convert list of arrays to MultiIndex.
541 MultiIndex.from_product : Make a MultiIndex from cartesian product
542 of iterables.
543 MultiIndex.from_frame : Make a MultiIndex from a DataFrame.
544
545 Examples
546 --------
547 >>> tuples = [(1, "red"), (1, "blue"), (2, "red"), (2, "blue")]
548 >>> pd.MultiIndex.from_tuples(tuples, names=("number", "color"))
549 MultiIndex([(1, 'red'),
550 (1, 'blue'),
551 (2, 'red'),
552 (2, 'blue')],
553 names=['number', 'color'])
554 """
555 if not is_list_like(tuples):
556 raise TypeError("Input must be a list / sequence of tuple-likes.")
557 if is_iterator(tuples):
558 tuples = list(tuples)
559 tuples = cast(Collection[tuple[Hashable, ...]], tuples)
560
561 # handling the empty tuple cases
562 if len(tuples) and all(isinstance(e, tuple) and not e for e in tuples):
563 codes = [np.zeros(len(tuples))]
564 levels = [Index(com.asarray_tuplesafe(tuples, dtype=np.dtype("object")))]
565 return cls(
566 levels=levels,
567 codes=codes,
568 sortorder=sortorder,
569 names=names,
570 verify_integrity=False,
571 )
572
573 arrays: list[Sequence[Hashable]]
574 if len(tuples) == 0:
575 if names is None:
576 raise TypeError("Cannot infer number of levels from empty list")
577 # error: Argument 1 to "len" has incompatible type "Hashable";
578 # expected "Sized"
579 arrays = [[]] * len(names) # type: ignore[arg-type]
580 elif isinstance(tuples, (np.ndarray, Index)):
581 if isinstance(tuples, Index):
582 tuples = np.asarray(tuples._values)
583
584 arrays = list(lib.tuples_to_object_array(tuples).T)
585 elif isinstance(tuples, list):
586 arrays = list(lib.to_object_array_tuples(tuples).T)
587 else:
588 arrs = zip_longest(*tuples, fillvalue=np.nan)
589 arrays = cast(list[Sequence[Hashable]], arrs)
590
591 return cls.from_arrays(arrays, sortorder=sortorder, names=names)
592
593 @classmethod
594 def from_product(
595 cls,
596 iterables: Sequence[Iterable[Hashable]],
597 sortorder: int | None = None,
598 names: Sequence[Hashable] | Hashable | lib.NoDefault = lib.no_default,
599 ) -> MultiIndex:
600 """
601 Make a MultiIndex from the cartesian product of multiple iterables.
602
603 Parameters
604 ----------
605 iterables : list / sequence of iterables
606 Each iterable has unique labels for each level of the index.
607 sortorder : int or None
608 Level of sortedness (must be lexicographically sorted by that
609 level).
610 names : list / sequence of str, optional
611 Names for the levels in the index.
612 If not explicitly provided, names will be inferred from the
613 elements of iterables if an element has a name attribute.
614
615 Returns
616 -------
617 MultiIndex
618
619 See Also
620 --------
621 MultiIndex.from_arrays : Convert list of arrays to MultiIndex.
622 MultiIndex.from_tuples : Convert list of tuples to MultiIndex.
623 MultiIndex.from_frame : Make a MultiIndex from a DataFrame.
624
625 Examples
626 --------
627 >>> numbers = [0, 1, 2]
628 >>> colors = ["green", "purple"]
629 >>> pd.MultiIndex.from_product([numbers, colors], names=["number", "color"])
630 MultiIndex([(0, 'green'),
631 (0, 'purple'),
632 (1, 'green'),
633 (1, 'purple'),
634 (2, 'green'),
635 (2, 'purple')],
636 names=['number', 'color'])
637 """
638
639 if not is_list_like(iterables):
640 raise TypeError("Input must be a list / sequence of iterables.")
641 if is_iterator(iterables):
642 iterables = list(iterables)
643
644 codes, levels = factorize_from_iterables(iterables)
645 if names is lib.no_default:
646 names = [getattr(it, "name", None) for it in iterables]
647
648 # codes are all ndarrays, so cartesian_product is lossless
649 codes = cartesian_product(codes)
650 return cls(levels, codes, sortorder=sortorder, names=names)
651
652 @classmethod
653 def from_frame(
654 cls,
655 df: DataFrame,
656 sortorder: int | None = None,
657 names: Sequence[Hashable] | Hashable | None = None,
658 ) -> MultiIndex:
659 """
660 Make a MultiIndex from a DataFrame.
661
662 Parameters
663 ----------
664 df : DataFrame
665 DataFrame to be converted to MultiIndex.
666 sortorder : int, optional
667 Level of sortedness (must be lexicographically sorted by that
668 level).
669 names : list-like, optional
670 If no names are provided, use the column names, or tuple of column
671 names if the columns is a MultiIndex. If a sequence, overwrite
672 names with the given sequence.
673
674 Returns
675 -------
676 MultiIndex
677 The MultiIndex representation of the given DataFrame.
678
679 See Also
680 --------
681 MultiIndex.from_arrays : Convert list of arrays to MultiIndex.
682 MultiIndex.from_tuples : Convert list of tuples to MultiIndex.
683 MultiIndex.from_product : Make a MultiIndex from cartesian product
684 of iterables.
685
686 Examples
687 --------
688 >>> df = pd.DataFrame(
689 ... [["HI", "Temp"], ["HI", "Precip"], ["NJ", "Temp"], ["NJ", "Precip"]],
690 ... columns=["a", "b"],
691 ... )
692 >>> df
693 a b
694 0 HI Temp
695 1 HI Precip
696 2 NJ Temp
697 3 NJ Precip
698
699 >>> pd.MultiIndex.from_frame(df)
700 MultiIndex([('HI', 'Temp'),
701 ('HI', 'Precip'),
702 ('NJ', 'Temp'),
703 ('NJ', 'Precip')],
704 names=['a', 'b'])
705
706 Using explicit names, instead of the column names
707
708 >>> pd.MultiIndex.from_frame(df, names=["state", "observation"])
709 MultiIndex([('HI', 'Temp'),
710 ('HI', 'Precip'),
711 ('NJ', 'Temp'),
712 ('NJ', 'Precip')],
713 names=['state', 'observation'])
714 """
715 if not isinstance(df, ABCDataFrame):
716 raise TypeError("Input must be a DataFrame")
717
718 column_names, columns = zip(*df.items(), strict=True)
719 names = column_names if names is None else names
720 return cls.from_arrays(columns, sortorder=sortorder, names=names)
721
722 # --------------------------------------------------------------------
723
724 @cache_readonly
725 def _values(self) -> np.ndarray:
726 # We override here, since our parent uses _data, which we don't use.
727 values = []
728
729 for i in range(self.nlevels):
730 index = self.levels[i]
731 codes = self.codes[i]
732
733 vals = index
734 if isinstance(vals.dtype, CategoricalDtype):
735 vals = cast("CategoricalIndex", vals)
736 vals = vals._data._internal_get_values()
737
738 if isinstance(vals.dtype, ExtensionDtype) or lib.is_np_dtype(
739 vals.dtype, "mM"
740 ):
741 vals = vals.astype(object)
742
743 array_vals = np.asarray(vals)
744 array_vals = algos.take_nd(array_vals, codes, fill_value=index._na_value)
745 values.append(array_vals)
746
747 arr = lib.fast_zip(values)
748 return arr
749
750 @property
751 def values(self) -> np.ndarray:
752 return self._values
753
754 @property
755 def array(self):
756 """
757 Raises a ValueError for `MultiIndex` because there's no single
758 array backing a MultiIndex.
759
760 Raises
761 ------
762 ValueError
763 """
764 raise ValueError(
765 "MultiIndex has no single backing array. Use "
766 "'MultiIndex.to_numpy()' to get a NumPy array of tuples."
767 )
768
769 @cache_readonly
770 def dtypes(self) -> Series:
771 """
772 Return the dtypes as a Series for the underlying MultiIndex.
773
774 See Also
775 --------
776 Index.dtype : Return the dtype object of the underlying data.
777 Series.dtypes : Return the data type of the underlying Series.
778
779 Examples
780 --------
781 >>> idx = pd.MultiIndex.from_product(
782 ... [(0, 1, 2), ("green", "purple")], names=["number", "color"]
783 ... )
784 >>> idx
785 MultiIndex([(0, 'green'),
786 (0, 'purple'),
787 (1, 'green'),
788 (1, 'purple'),
789 (2, 'green'),
790 (2, 'purple')],
791 names=['number', 'color'])
792 >>> idx.dtypes
793 number int64
794 color object
795 dtype: object
796 """
797 from pandas import Series
798
799 names = com.fill_missing_names(self.names)
800 return Series([level.dtype for level in self.levels], index=Index(names))
801
802 def __len__(self) -> int:
803 return len(self.codes[0])
804
805 @property
806 def size(self) -> int:
807 """
808 Return the number of elements in the underlying data.
809 """
810 # override Index.size to avoid materializing _values
811 return len(self)
812
813 # --------------------------------------------------------------------
814 # Levels Methods
815
816 @cache_readonly
817 def levels(self) -> FrozenList:
818 """
819 Levels of the MultiIndex.
820
821 Levels refer to the different hierarchical levels or layers in a MultiIndex.
822 In a MultiIndex, each level represents a distinct dimension or category of
823 the index.
824
825 To access the levels, you can use the levels attribute of the MultiIndex,
826 which returns a tuple of Index objects. Each Index object represents a
827 level in the MultiIndex and contains the unique values found in that
828 specific level.
829
830 If a MultiIndex is created with levels A, B, C, and the DataFrame using
831 it filters out all rows of the level C, MultiIndex.levels will still
832 return A, B, C.
833
834 See Also
835 --------
836 MultiIndex.codes : The codes of the levels in the MultiIndex.
837 MultiIndex.get_level_values : Return vector of label values for requested
838 level.
839
840 Examples
841 --------
842 >>> index = pd.MultiIndex.from_product(
843 ... [["mammal"], ("goat", "human", "cat", "dog")],
844 ... names=["Category", "Animals"],
845 ... )
846 >>> leg_num = pd.DataFrame(data=(4, 2, 4, 4), index=index, columns=["Legs"])
847 >>> leg_num
848 Legs
849 Category Animals
850 mammal goat 4
851 human 2
852 cat 4
853 dog 4
854
855 >>> leg_num.index.levels
856 FrozenList([['mammal'], ['cat', 'dog', 'goat', 'human']])
857
858 MultiIndex levels will not change even if the DataFrame using the MultiIndex
859 does not contain all them anymore.
860 See how "human" is not in the DataFrame, but it is still in levels:
861
862 >>> large_leg_num = leg_num[leg_num.Legs > 2]
863 >>> large_leg_num
864 Legs
865 Category Animals
866 mammal goat 4
867 cat 4
868 dog 4
869
870 >>> large_leg_num.index.levels
871 FrozenList([['mammal'], ['cat', 'dog', 'goat', 'human']])
872 """
873 # Use cache_readonly to ensure that self.get_locs doesn't repeatedly
874 # create new IndexEngine
875 # https://github.com/pandas-dev/pandas/issues/31648
876 result = [
877 x._rename(name=name)
878 for x, name in zip(self._levels, self._names, strict=True)
879 ]
880 for level in result:
881 # disallow midx.levels[0].name = "foo"
882 level._no_setting_name = True
883 return FrozenList(result)
884
885 def _set_levels(
886 self,
887 levels,
888 *,
889 level=None,
890 copy: bool = False,
891 validate: bool = True,
892 verify_integrity: bool = False,
893 ) -> None:
894 # This is NOT part of the levels property because it should be
895 # externally not allowed to set levels. User beware if you change
896 # _levels directly
897 if validate:
898 if len(levels) == 0:
899 raise ValueError("Must set non-zero number of levels.")
900 if level is None and len(levels) != self.nlevels:
901 raise ValueError("Length of levels must match number of levels.")
902 if level is not None and len(levels) != len(level):
903 raise ValueError("Length of levels must match length of level.")
904
905 if level is None:
906 new_levels = FrozenList(
907 ensure_index(lev, copy=copy)._view() for lev in levels
908 )
909 level_numbers: range | list[int] = range(len(new_levels))
910 else:
911 level_numbers = [self._get_level_number(lev) for lev in level]
912 new_levels_list = list(self._levels)
913 for lev_num, lev in zip(level_numbers, levels, strict=True):
914 new_levels_list[lev_num] = ensure_index(lev, copy=copy)._view()
915 new_levels = FrozenList(new_levels_list)
916
917 if verify_integrity:
918 new_codes = self._verify_integrity(
919 levels=new_levels, levels_to_verify=level_numbers
920 )
921 self._codes = new_codes
922
923 names = self.names
924 self._levels = new_levels
925 if any(names):
926 self._set_names(names)
927
928 self._reset_cache()
929
930 def set_levels(
931 self, levels, *, level=None, verify_integrity: bool = True
932 ) -> MultiIndex:
933 """
934 Set new levels on MultiIndex. Defaults to returning new index.
935
936 The `set_levels` method provides a flexible way to change the levels of a
937 `MultiIndex`. This is particularly useful when you need to update the
938 index structure of your DataFrame without altering the data. The method
939 returns a new `MultiIndex` unless the operation is performed in-place,
940 ensuring that the original index remains unchanged unless explicitly
941 modified.
942
943 The method checks the integrity of the new levels against the existing
944 codes by default, but this can be disabled if you are confident that
945 your levels are consistent with the underlying data. This can be useful
946 when you want to perform optimizations or make specific adjustments to
947 the index levels that do not strictly adhere to the original structure.
948
949 Parameters
950 ----------
951 levels : sequence or list of sequence
952 New level(s) to apply.
953 level : int, level name, or sequence of int/level names (default None)
954 Level(s) to set (None for all levels).
955 verify_integrity : bool, default True
956 If True, checks that levels and codes are compatible.
957
958 Returns
959 -------
960 MultiIndex
961 A new `MultiIndex` with the updated levels.
962
963 See Also
964 --------
965 MultiIndex.set_codes : Set new codes on the existing `MultiIndex`.
966 MultiIndex.remove_unused_levels : Create new MultiIndex from current that
967 removes unused levels.
968 Index.set_names : Set Index or MultiIndex name.
969
970 Examples
971 --------
972 >>> idx = pd.MultiIndex.from_tuples(
973 ... [
974 ... (1, "one"),
975 ... (1, "two"),
976 ... (2, "one"),
977 ... (2, "two"),
978 ... (3, "one"),
979 ... (3, "two"),
980 ... ],
981 ... names=["foo", "bar"],
982 ... )
983 >>> idx
984 MultiIndex([(1, 'one'),
985 (1, 'two'),
986 (2, 'one'),
987 (2, 'two'),
988 (3, 'one'),
989 (3, 'two')],
990 names=['foo', 'bar'])
991
992 >>> idx.set_levels([["a", "b", "c"], [1, 2]])
993 MultiIndex([('a', 1),
994 ('a', 2),
995 ('b', 1),
996 ('b', 2),
997 ('c', 1),
998 ('c', 2)],
999 names=['foo', 'bar'])
1000 >>> idx.set_levels(["a", "b", "c"], level=0)
1001 MultiIndex([('a', 'one'),
1002 ('a', 'two'),
1003 ('b', 'one'),
1004 ('b', 'two'),
1005 ('c', 'one'),
1006 ('c', 'two')],
1007 names=['foo', 'bar'])
1008 >>> idx.set_levels(["a", "b"], level="bar")
1009 MultiIndex([(1, 'a'),
1010 (1, 'b'),
1011 (2, 'a'),
1012 (2, 'b'),
1013 (3, 'a'),
1014 (3, 'b')],
1015 names=['foo', 'bar'])
1016
1017 If any of the levels passed to ``set_levels()`` exceeds the
1018 existing length, all of the values from that argument will
1019 be stored in the MultiIndex levels, though the values will
1020 be truncated in the MultiIndex output.
1021
1022 >>> idx.set_levels([["a", "b", "c"], [1, 2, 3, 4]], level=[0, 1])
1023 MultiIndex([('a', 1),
1024 ('a', 2),
1025 ('b', 1),
1026 ('b', 2),
1027 ('c', 1),
1028 ('c', 2)],
1029 names=['foo', 'bar'])
1030 >>> idx.set_levels([["a", "b", "c"], [1, 2, 3, 4]], level=[0, 1]).levels
1031 FrozenList([['a', 'b', 'c'], [1, 2, 3, 4]])
1032 """
1033
1034 if isinstance(levels, Index):
1035 pass
1036 elif is_array_like(levels):
1037 levels = Index(levels)
1038 elif is_list_like(levels):
1039 levels = list(levels)
1040
1041 level, levels = _require_listlike(level, levels, "Levels")
1042 idx = self._view()
1043 idx._reset_identity()
1044 idx._set_levels(
1045 levels, level=level, validate=True, verify_integrity=verify_integrity
1046 )
1047 return idx
1048
1049 @property
1050 def nlevels(self) -> int:
1051 """
1052 Integer number of levels in this MultiIndex.
1053
1054 See Also
1055 --------
1056 MultiIndex.levels : Get the levels of the MultiIndex.
1057 MultiIndex.codes : Get the codes of the MultiIndex.
1058 MultiIndex.from_arrays : Convert arrays to MultiIndex.
1059 MultiIndex.from_tuples : Convert list of tuples to MultiIndex.
1060
1061 Examples
1062 --------
1063 >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"], ["c"]])
1064 >>> mi
1065 MultiIndex([('a', 'b', 'c')],
1066 )
1067 >>> mi.nlevels
1068 3
1069 """
1070 return len(self._levels)
1071
1072 @property
1073 def levshape(self) -> Shape:
1074 """
1075 A tuple representing the length of each level in the MultiIndex.
1076
1077 In a `MultiIndex`, each level can contain multiple unique values. The
1078 `levshape` property provides a quick way to assess the size of each
1079 level by returning a tuple where each entry represents the number of
1080 unique values in that specific level. This is particularly useful in
1081 scenarios where you need to understand the structure and distribution
1082 of your index levels, such as when working with multidimensional data.
1083
1084 See Also
1085 --------
1086 MultiIndex.shape : Return a tuple of the shape of the MultiIndex.
1087 MultiIndex.levels : Returns the levels of the MultiIndex.
1088
1089 Examples
1090 --------
1091 >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"], ["c"]])
1092 >>> mi
1093 MultiIndex([('a', 'b', 'c')],
1094 )
1095 >>> mi.levshape
1096 (1, 1, 1)
1097 """
1098 return tuple(len(x) for x in self.levels)
1099
1100 # --------------------------------------------------------------------
1101 # Codes Methods
1102
1103 @property
1104 def codes(self) -> FrozenList:
1105 """
1106 Codes of the MultiIndex.
1107
1108 Codes are the position of the index value in the list of level values
1109 for each level.
1110
1111 Returns
1112 -------
1113 tuple of numpy.ndarray
1114 The codes of the MultiIndex. Each array in the tuple corresponds
1115 to a level in the MultiIndex.
1116
1117 See Also
1118 --------
1119 MultiIndex.set_codes : Set new codes on MultiIndex.
1120
1121 Examples
1122 --------
1123 >>> arrays = [[1, 1, 2, 2], ["red", "blue", "red", "blue"]]
1124 >>> mi = pd.MultiIndex.from_arrays(arrays, names=("number", "color"))
1125 >>> mi.codes
1126 FrozenList([[0, 0, 1, 1], [1, 0, 1, 0]])
1127 """
1128 return self._codes
1129
1130 def _set_codes(
1131 self,
1132 codes,
1133 *,
1134 level=None,
1135 copy: bool = False,
1136 validate: bool = True,
1137 verify_integrity: bool = False,
1138 ) -> None:
1139 if validate:
1140 if level is None and len(codes) != self.nlevels:
1141 raise ValueError("Length of codes must match number of levels")
1142 if level is not None and len(codes) != len(level):
1143 raise ValueError("Length of codes must match length of levels.")
1144
1145 level_numbers: list[int] | range
1146 if level is None:
1147 new_codes = FrozenList(
1148 _coerce_indexer_frozen(level_codes, lev, copy=copy).view()
1149 for lev, level_codes in zip(self._levels, codes, strict=True)
1150 )
1151 level_numbers = range(len(new_codes))
1152 else:
1153 level_numbers = [self._get_level_number(lev) for lev in level]
1154 new_codes_list = list(self._codes)
1155 for lev_num, level_codes in zip(level_numbers, codes, strict=True):
1156 lev = self.levels[lev_num]
1157 new_codes_list[lev_num] = _coerce_indexer_frozen(
1158 level_codes, lev, copy=copy
1159 )
1160 new_codes = FrozenList(new_codes_list)
1161
1162 if verify_integrity:
1163 new_codes = self._verify_integrity(
1164 codes=new_codes, levels_to_verify=level_numbers
1165 )
1166
1167 self._codes = new_codes
1168
1169 self._reset_cache()
1170
1171 def set_codes(
1172 self, codes, *, level=None, verify_integrity: bool = True
1173 ) -> MultiIndex:
1174 """
1175 Set new codes on MultiIndex. Defaults to returning new index.
1176
1177 Parameters
1178 ----------
1179 codes : sequence or list of sequence
1180 New codes to apply.
1181 level : int, level name, or sequence of int/level names (default None)
1182 Level(s) to set (None for all levels).
1183 verify_integrity : bool, default True
1184 If True, checks that levels and codes are compatible.
1185
1186 Returns
1187 -------
1188 new index (of same type and class...etc) or None
1189 The same type as the caller or None if ``inplace=True``.
1190
1191 See Also
1192 --------
1193 MultiIndex.set_levels : Set new levels on MultiIndex.
1194 MultiIndex.codes : Get the codes of the levels in the MultiIndex.
1195 MultiIndex.levels : Get the levels of the MultiIndex.
1196
1197 Examples
1198 --------
1199 >>> idx = pd.MultiIndex.from_tuples(
1200 ... [(1, "one"), (1, "two"), (2, "one"), (2, "two")], names=["foo", "bar"]
1201 ... )
1202 >>> idx
1203 MultiIndex([(1, 'one'),
1204 (1, 'two'),
1205 (2, 'one'),
1206 (2, 'two')],
1207 names=['foo', 'bar'])
1208
1209 >>> idx.set_codes([[1, 0, 1, 0], [0, 0, 1, 1]])
1210 MultiIndex([(2, 'one'),
1211 (1, 'one'),
1212 (2, 'two'),
1213 (1, 'two')],
1214 names=['foo', 'bar'])
1215 >>> idx.set_codes([1, 0, 1, 0], level=0)
1216 MultiIndex([(2, 'one'),
1217 (1, 'two'),
1218 (2, 'one'),
1219 (1, 'two')],
1220 names=['foo', 'bar'])
1221 >>> idx.set_codes([0, 0, 1, 1], level="bar")
1222 MultiIndex([(1, 'one'),
1223 (1, 'one'),
1224 (2, 'two'),
1225 (2, 'two')],
1226 names=['foo', 'bar'])
1227 >>> idx.set_codes([[1, 0, 1, 0], [0, 0, 1, 1]], level=[0, 1])
1228 MultiIndex([(2, 'one'),
1229 (1, 'one'),
1230 (2, 'two'),
1231 (1, 'two')],
1232 names=['foo', 'bar'])
1233 """
1234
1235 level, codes = _require_listlike(level, codes, "Codes")
1236 idx = self._view()
1237 idx._reset_identity()
1238 idx._set_codes(codes, level=level, verify_integrity=verify_integrity)
1239 return idx
1240
1241 # --------------------------------------------------------------------
1242 # Index Internals
1243
1244 @cache_readonly
1245 def _engine(self):
1246 # Calculate the number of bits needed to represent labels in each
1247 # level, as log2 of their sizes:
1248 # NaN values are shifted to 1 and missing values in other while
1249 # calculating the indexer are shifted to 0
1250 sizes = np.ceil(
1251 np.log2(
1252 [len(level) + libindex.multiindex_nulls_shift for level in self.levels]
1253 )
1254 )
1255
1256 # Sum bit counts, starting from the _right_....
1257 lev_bits = np.cumsum(sizes[::-1])[::-1]
1258
1259 # ... in order to obtain offsets such that sorting the combination of
1260 # shifted codes (one for each level, resulting in a unique integer) is
1261 # equivalent to sorting lexicographically the codes themselves. Notice
1262 # that each level needs to be shifted by the number of bits needed to
1263 # represent the _previous_ ones:
1264 offsets = np.concatenate([lev_bits[1:], [0]])
1265 # Downcast the type if possible, to prevent upcasting when shifting codes:
1266 offsets = offsets.astype(np.min_scalar_type(int(offsets[0])))
1267
1268 # Check the total number of bits needed for our representation:
1269 if lev_bits[0] > 64:
1270 # The levels would overflow a 64 bit uint - use Python integers:
1271 return MultiIndexPyIntEngine(self.levels, self.codes, offsets)
1272 if lev_bits[0] > 32:
1273 # The levels would overflow a 32 bit uint - use uint64
1274 return MultiIndexUInt64Engine(self.levels, self.codes, offsets)
1275 if lev_bits[0] > 16:
1276 # The levels would overflow a 16 bit uint - use uint8
1277 return MultiIndexUInt32Engine(self.levels, self.codes, offsets)
1278 if lev_bits[0] > 8:
1279 # The levels would overflow a 8 bit uint - use uint16
1280 return MultiIndexUInt16Engine(self.levels, self.codes, offsets)
1281 # The levels fit in an 8 bit uint - use uint8
1282 return MultiIndexUInt8Engine(self.levels, self.codes, offsets)
1283
1284 # Return type "Callable[..., MultiIndex]" of "_constructor" incompatible with return
1285 # type "Type[MultiIndex]" in supertype "Index"
1286 @property
1287 def _constructor(self) -> Callable[..., MultiIndex]: # type: ignore[override]
1288 return type(self).from_tuples
1289
1290 def _shallow_copy(self, values: np.ndarray, name=lib.no_default) -> MultiIndex:
1291 """
1292 Create a new Index with the same class as the caller, don't copy the
1293 data, use the same object attributes with passed in attributes taking
1294 precedence.
1295
1296 *this is an internal non-public method*
1297
1298 Parameters
1299 ----------
1300 values : the values to create the new Index, optional
1301 name : Label, defaults to self.name
1302 """
1303 names = name if name is not lib.no_default else self.names
1304
1305 return type(self).from_tuples(values, sortorder=None, names=names)
1306
1307 def _view(self) -> MultiIndex:
1308 result = type(self)(
1309 levels=self.levels,
1310 codes=self.codes,
1311 sortorder=self.sortorder,
1312 names=self.names,
1313 verify_integrity=False,
1314 )
1315 result._cache = self._cache.copy()
1316 result._reset_cache("levels") # GH32669
1317 return result
1318
1319 # --------------------------------------------------------------------
1320
1321 # error: Signature of "copy" incompatible with supertype "Index"
1322 def copy( # type: ignore[override]
1323 self,
1324 names=None,
1325 deep: bool = False,
1326 name=None,
1327 ) -> Self:
1328 """
1329 Make a copy of this object. Names, dtype, levels and codes can be passed and \
1330 will be set on new copy.
1331
1332 The `copy` method provides a mechanism to create a duplicate of an
1333 existing MultiIndex object. This is particularly useful in scenarios where
1334 modifications are required on an index, but the original MultiIndex should
1335 remain unchanged. By specifying the `deep` parameter, users can control
1336 whether the copy should be a deep or shallow copy, providing flexibility
1337 depending on the size and complexity of the MultiIndex.
1338
1339 Parameters
1340 ----------
1341 names : sequence, optional
1342 Names to set on the new MultiIndex object.
1343 deep : bool, default False
1344 If False, the new object will be a shallow copy. If True, a deep copy
1345 will be attempted. Deep copying can be potentially expensive for large
1346 MultiIndex objects.
1347 name : Label
1348 Kept for compatibility with 1-dimensional Index. Should not be used.
1349
1350 Returns
1351 -------
1352 MultiIndex
1353 A new MultiIndex object with the specified modifications.
1354
1355 See Also
1356 --------
1357 MultiIndex.from_arrays : Convert arrays to MultiIndex.
1358 MultiIndex.from_tuples : Convert list of tuples to MultiIndex.
1359 MultiIndex.from_frame : Convert DataFrame to MultiIndex.
1360
1361 Notes
1362 -----
1363 In most cases, there should be no functional difference from using
1364 ``deep``, but if ``deep`` is passed it will attempt to deepcopy.
1365 This could be potentially expensive on large MultiIndex objects.
1366
1367 Examples
1368 --------
1369 >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"], ["c"]])
1370 >>> mi
1371 MultiIndex([('a', 'b', 'c')],
1372 )
1373 >>> mi.copy()
1374 MultiIndex([('a', 'b', 'c')],
1375 )
1376 """
1377 names = self._validate_names(name=name, names=names, deep=deep)
1378 keep_id = not deep
1379 levels, codes = None, None
1380
1381 if deep:
1382 from copy import deepcopy
1383
1384 levels = deepcopy(self.levels)
1385 codes = deepcopy(self.codes)
1386
1387 levels = levels if levels is not None else self.levels
1388 codes = codes if codes is not None else self.codes
1389
1390 new_index = type(self)(
1391 levels=levels,
1392 codes=codes,
1393 sortorder=self.sortorder,
1394 names=names,
1395 verify_integrity=False,
1396 )
1397 new_index._cache = self._cache.copy()
1398 new_index._reset_cache("levels") # GH32669
1399 if keep_id:
1400 new_index._id = self._id
1401 return new_index
1402
1403 def __array__(self, dtype=None, copy=None) -> np.ndarray:
1404 """the array interface, return my values"""
1405 if copy is False:
1406 # self.values is always a newly construct array, so raise.
1407 raise ValueError(
1408 "Unable to avoid copy while creating an array as requested."
1409 )
1410 if copy is True:
1411 # explicit np.array call to ensure a copy is made and unique objects
1412 # are returned, because self.values is cached
1413 return np.array(self.values, dtype=dtype)
1414 return self.values
1415
1416 def view(self, cls=None) -> Self:
1417 """this is defined as a copy with the same identity"""
1418 result = self.copy()
1419 result._id = self._id
1420 return result
1421
1422 def __contains__(self, key: Any) -> bool:
1423 """
1424 Return a boolean indicating whether the provided key is in the index.
1425
1426 Parameters
1427 ----------
1428 key : label
1429 The key to check if it is present in the index.
1430
1431 Returns
1432 -------
1433 bool
1434 Whether the key search is in the index.
1435
1436 Raises
1437 ------
1438 TypeError
1439 If the key is not hashable.
1440
1441 See Also
1442 --------
1443 Index.isin : Returns an ndarray of boolean dtype indicating whether the
1444 list-like key is in the index.
1445
1446 Examples
1447 --------
1448 >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"], ["c"]])
1449 >>> mi
1450 MultiIndex([('a', 'b', 'c')],
1451 )
1452
1453 >>> "a" in mi
1454 True
1455 >>> "x" in mi
1456 False
1457 """
1458 hash(key)
1459 try:
1460 self.get_loc(key)
1461 return True
1462 except (LookupError, TypeError, ValueError):
1463 return False
1464
1465 @cache_readonly
1466 def dtype(self) -> np.dtype:
1467 return np.dtype("O")
1468
1469 @cache_readonly
1470 def _is_memory_usage_qualified(self) -> bool:
1471 """return a boolean if we need a qualified .info display"""
1472
1473 def f(dtype) -> bool:
1474 return is_object_dtype(dtype) or (
1475 is_string_dtype(dtype) and dtype.storage == "python"
1476 )
1477
1478 return any(f(level.dtype) for level in self.levels)
1479
1480 # Cannot determine type of "memory_usage"
1481 def memory_usage(self, deep: bool = False) -> int:
1482 """
1483 Memory usage of the values.
1484
1485 Parameters
1486 ----------
1487 deep : bool, default False
1488 Introspect the data deeply, interrogate
1489 `object` dtypes for system-level memory consumption.
1490
1491 Returns
1492 -------
1493 bytes used
1494 Returns memory usage of the values in the Index in bytes.
1495
1496 See Also
1497 --------
1498 numpy.ndarray.nbytes : Total bytes consumed by the elements of the
1499 array.
1500
1501 Notes
1502 -----
1503 Memory usage does not include memory consumed by elements that
1504 are not components of the array if deep=False or if used on PyPy
1505
1506 Examples
1507 --------
1508 >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"], ["c"]])
1509 >>> mi.memory_usage()
1510 81
1511 """
1512 # we are overwriting our base class to avoid
1513 # computing .values here which could materialize
1514 # a tuple representation unnecessarily
1515 return self._nbytes(deep)
1516
1517 @cache_readonly
1518 def nbytes(self) -> int:
1519 """return the number of bytes in the underlying data"""
1520 return self._nbytes(False)
1521
1522 def _nbytes(self, deep: bool = False) -> int:
1523 """
1524 return the number of bytes in the underlying data
1525 deeply introspect the level data if deep=True
1526
1527 include the engine hashtable
1528
1529 *this is in internal routine*
1530
1531 """
1532 # for implementations with no useful getsizeof (PyPy)
1533 objsize = 24
1534
1535 level_nbytes = sum(i.memory_usage(deep=deep) for i in self.levels)
1536 label_nbytes = sum(i.nbytes for i in self.codes)
1537 names_nbytes = sum(getsizeof(i, objsize) for i in self.names)
1538 result = level_nbytes + label_nbytes + names_nbytes
1539
1540 # include our engine hashtable, only if it's already cached
1541 if "_engine" in self._cache:
1542 result += self._engine.sizeof(deep=deep)
1543 return result
1544
1545 # --------------------------------------------------------------------
1546 # Rendering Methods
1547
1548 def _formatter_func(self, tup):
1549 """
1550 Formats each item in tup according to its level's formatter function.
1551 """
1552 formatter_funcs = (level._formatter_func for level in self.levels)
1553 return tuple(func(val) for func, val in zip(formatter_funcs, tup, strict=True))
1554
1555 def _get_values_for_csv(
1556 self, *, na_rep: str = "nan", **kwargs
1557 ) -> npt.NDArray[np.object_]:
1558 new_levels = []
1559 new_codes = []
1560
1561 # go through the levels and format them
1562 for level, level_codes in zip(self.levels, self.codes, strict=True):
1563 level_strs = level._get_values_for_csv(na_rep=na_rep, **kwargs)
1564 # add nan values, if there are any
1565 mask = level_codes == -1
1566 if mask.any():
1567 nan_index = len(level_strs)
1568 # numpy 1.21 deprecated implicit string casting
1569 level_strs = level_strs.astype(str)
1570 level_strs = np.append(level_strs, na_rep)
1571 assert not level_codes.flags.writeable # i.e. copy is needed
1572 level_codes = level_codes.copy() # make writeable
1573 level_codes[mask] = nan_index
1574 new_levels.append(level_strs)
1575 new_codes.append(level_codes)
1576
1577 if len(new_levels) == 1:
1578 # a single-level multi-index
1579 return Index(
1580 new_levels[0].take(new_codes[0]), copy=False
1581 )._get_values_for_csv()
1582 else:
1583 # reconstruct the multi-index
1584 mi = MultiIndex(
1585 levels=new_levels,
1586 codes=new_codes,
1587 names=self.names,
1588 sortorder=self.sortorder,
1589 verify_integrity=False,
1590 )
1591 return mi._values
1592
1593 def _format_multi(
1594 self,
1595 *,
1596 include_names: bool,
1597 sparsify: bool | None | lib.NoDefault,
1598 formatter: Callable | None = None,
1599 ) -> list:
1600 if len(self) == 0:
1601 return []
1602
1603 stringified_levels = []
1604 for lev, level_codes in zip(self.levels, self.codes, strict=True):
1605 na = _get_na_rep(lev.dtype)
1606
1607 if len(lev) > 0:
1608 taken = formatted = lev.take(level_codes)
1609 formatted = taken._format_flat(include_name=False, formatter=formatter)
1610
1611 # we have some NA
1612 mask = level_codes == -1
1613 if mask.any():
1614 formatted = np.array(formatted, dtype=object)
1615 formatted[mask] = na
1616 formatted = formatted.tolist()
1617
1618 else:
1619 # weird all NA case
1620 formatted = [
1621 pprint_thing(na if isna(x) else x, escape_chars=("\t", "\r", "\n"))
1622 for x in algos.take_nd(lev._values, level_codes)
1623 ]
1624 stringified_levels.append(formatted)
1625
1626 result_levels = []
1627 for lev, lev_name in zip(stringified_levels, self.names, strict=True):
1628 level = []
1629
1630 if include_names:
1631 level.append(
1632 pprint_thing(lev_name, escape_chars=("\t", "\r", "\n"))
1633 if lev_name is not None
1634 else ""
1635 )
1636
1637 level.extend(np.array(lev, dtype=object))
1638 result_levels.append(level)
1639
1640 if sparsify is None:
1641 sparsify = get_option("display.multi_sparse")
1642
1643 if sparsify:
1644 sentinel: Literal[""] | bool | lib.NoDefault = ""
1645 # GH3547 use value of sparsify as sentinel if it's "Falsey"
1646 assert isinstance(sparsify, bool) or sparsify is lib.no_default
1647 if sparsify is lib.no_default:
1648 sentinel = sparsify
1649 # little bit of a kludge job for #1217
1650 result_levels = sparsify_labels(
1651 result_levels, start=int(include_names), sentinel=sentinel
1652 )
1653
1654 return result_levels
1655
1656 # --------------------------------------------------------------------
1657 # Names Methods
1658
1659 def _get_names(self) -> FrozenList:
1660 return FrozenList(self._names)
1661
1662 def _set_names(self, names, *, level=None) -> None:
1663 """
1664 Set new names on index. Each name has to be a hashable type.
1665
1666 Parameters
1667 ----------
1668 values : str or sequence
1669 name(s) to set
1670 level : int, level name, or sequence of int/level names (default None)
1671 If the index is a MultiIndex (hierarchical), level(s) to set (None
1672 for all levels). Otherwise level must be None
1673
1674 Raises
1675 ------
1676 TypeError if each name is not hashable.
1677
1678 Notes
1679 -----
1680 sets names on levels. WARNING: mutates!
1681
1682 Note that you generally want to set this *after* changing levels, so
1683 that it only acts on copies
1684 """
1685 # GH 15110
1686 # Don't allow a single string for names in a MultiIndex
1687 if names is not None and not is_list_like(names):
1688 raise ValueError("Names should be list-like for a MultiIndex")
1689 names = list(names)
1690
1691 if level is not None and len(names) != len(level):
1692 raise ValueError("Length of names must match length of level.")
1693 if level is None and len(names) != self.nlevels:
1694 raise ValueError(
1695 "Length of names must match number of levels in MultiIndex."
1696 )
1697
1698 if level is None:
1699 level = range(self.nlevels)
1700 else:
1701 level = (self._get_level_number(lev) for lev in level)
1702
1703 # set the name
1704 for lev, name in zip(level, names, strict=True):
1705 if name is not None:
1706 # GH 20527
1707 # All items in 'names' need to be hashable:
1708 if not is_hashable(name):
1709 raise TypeError(
1710 f"{type(self).__name__}.name must be a hashable type"
1711 )
1712 self._names[lev] = name
1713
1714 # If .levels has been accessed, the .name of each level in our cache
1715 # will be stale.
1716 self._reset_cache("levels")
1717
1718 names = property(
1719 fset=_set_names,
1720 fget=_get_names,
1721 doc="""
1722 Names of levels in MultiIndex.
1723
1724 This attribute provides access to the names of the levels in a `MultiIndex`.
1725 The names are stored as a `FrozenList`, which is an immutable list-like
1726 container. Each name corresponds to a level in the `MultiIndex`, and can be
1727 used to identify or manipulate the levels individually.
1728
1729 See Also
1730 --------
1731 MultiIndex.set_names : Set Index or MultiIndex name.
1732 MultiIndex.rename : Rename specific levels in a MultiIndex.
1733 Index.names : Get names on index.
1734
1735 Examples
1736 --------
1737 >>> mi = pd.MultiIndex.from_arrays(
1738 ... [[1, 2], [3, 4], [5, 6]], names=['x', 'y', 'z']
1739 ... )
1740 >>> mi
1741 MultiIndex([(1, 3, 5),
1742 (2, 4, 6)],
1743 names=['x', 'y', 'z'])
1744 >>> mi.names
1745 FrozenList(['x', 'y', 'z'])
1746 """,
1747 )
1748
1749 # --------------------------------------------------------------------
1750
1751 @cache_readonly
1752 def inferred_type(self) -> str:
1753 return "mixed"
1754
1755 def _get_level_number(self, level) -> int:
1756 count = self.names.count(level)
1757 if (count > 1) and not is_integer(level):
1758 raise ValueError(
1759 f"The name {level} occurs multiple times, use a level number"
1760 )
1761 try:
1762 level = self.names.index(level)
1763 except ValueError as err:
1764 if not is_integer(level):
1765 raise KeyError(f"Level {level} not found") from err
1766 if level < 0:
1767 level += self.nlevels
1768 if level < 0:
1769 orig_level = level - self.nlevels
1770 raise IndexError(
1771 f"Too many levels: Index has only {self.nlevels} levels, "
1772 f"{orig_level} is not a valid level number"
1773 ) from err
1774 # Note: levels are zero-based
1775 elif level >= self.nlevels:
1776 raise IndexError(
1777 f"Too many levels: Index has only {self.nlevels} levels, "
1778 f"not {level + 1}"
1779 ) from err
1780 return level
1781
1782 @cache_readonly
1783 def is_monotonic_increasing(self) -> bool:
1784 """
1785 Return a boolean if the values are equal or increasing.
1786 """
1787 if any(-1 in code for code in self.codes):
1788 return False
1789
1790 if all(level.is_monotonic_increasing for level in self.levels):
1791 # If each level is sorted, we can operate on the codes directly. GH27495
1792 return libalgos.is_lexsorted(
1793 [x.astype("int64", copy=False) for x in self.codes]
1794 )
1795
1796 # reversed() because lexsort() wants the most significant key last.
1797 values = [
1798 self._get_level_values(i)._values for i in reversed(range(len(self.levels)))
1799 ]
1800 try:
1801 # error: Argument 1 to "lexsort" has incompatible type
1802 # "List[Union[ExtensionArray, ndarray[Any, Any]]]";
1803 # expected "Union[_SupportsArray[dtype[Any]],
1804 # _NestedSequence[_SupportsArray[dtype[Any]]], bool,
1805 # int, float, complex, str, bytes, _NestedSequence[Union
1806 # [bool, int, float, complex, str, bytes]]]"
1807 sort_order = np.lexsort(values) # type: ignore[arg-type]
1808 return Index(sort_order, copy=False).is_monotonic_increasing
1809 except TypeError:
1810 # we have mixed types and np.lexsort is not happy
1811 return Index(self._values, copy=False).is_monotonic_increasing
1812
1813 @cache_readonly
1814 def is_monotonic_decreasing(self) -> bool:
1815 """
1816 Return a boolean if the values are equal or decreasing.
1817 """
1818 # monotonic decreasing if and only if reverse is monotonic increasing
1819 return self[::-1].is_monotonic_increasing
1820
1821 def duplicated(self, keep: DropKeep = "first") -> npt.NDArray[np.bool_]:
1822 """
1823 Indicate duplicate index values.
1824
1825 Duplicated values are indicated as ``True`` values in the resulting
1826 array. Either all duplicates, all except the first, or all except the
1827 last occurrence of duplicates can be indicated.
1828
1829 Parameters
1830 ----------
1831 keep : {'first', 'last', False}, default 'first'
1832 The value or values in a set of duplicates to mark as missing.
1833
1834 - 'first' : Mark duplicates as ``True`` except for the first
1835 occurrence.
1836 - 'last' : Mark duplicates as ``True`` except for the last
1837 occurrence.
1838 - ``False`` : Mark all duplicates as ``True``.
1839
1840 Returns
1841 -------
1842 np.ndarray[bool]
1843 A numpy array of boolean values indicating duplicate index values.
1844
1845 See Also
1846 --------
1847 Series.duplicated : Equivalent method on pandas.Series.
1848 DataFrame.duplicated : Equivalent method on pandas.DataFrame.
1849 Index.drop_duplicates : Remove duplicate values from Index.
1850
1851 Examples
1852 --------
1853 By default, for each set of duplicated values, the first occurrence is
1854 set to False and all others to True:
1855
1856 >>> mi = pd.MultiIndex.from_arrays((list("abca"), list("defd")))
1857 >>> mi.duplicated()
1858 array([False, False, False, True])
1859
1860 which is equivalent to
1861
1862 >>> mi.duplicated(keep="first")
1863 array([False, False, False, True])
1864
1865 By using 'last', the last occurrence of each set of duplicated values
1866 is set on False and all others on True:
1867
1868 >>> mi.duplicated(keep="last")
1869 array([ True, False, False, False])
1870
1871 By setting keep on ``False``, all duplicates are True:
1872
1873 >>> mi.duplicated(keep=False)
1874 array([ True, False, False, True])
1875 """
1876 shape = tuple(len(lev) for lev in self.levels)
1877 ids = get_group_index(self.codes, shape, sort=False, xnull=False)
1878
1879 return duplicated(ids, keep)
1880
1881 # error: Cannot override final attribute "_duplicated"
1882 # (previously declared in base class "IndexOpsMixin")
1883 _duplicated = duplicated # type: ignore[misc]
1884
1885 def fillna(self, value):
1886 """
1887 fillna is not implemented for MultiIndex
1888 """
1889 raise NotImplementedError("fillna is not defined for MultiIndex")
1890
1891 def dropna(self, how: AnyAll = "any") -> MultiIndex:
1892 """
1893 Return MultiIndex without NA/NaN values.
1894
1895 Parameters
1896 ----------
1897 how : {'any', 'all'}, default 'any'
1898 Drop the value when any or all levels are NaN.
1899
1900 Returns
1901 -------
1902 Index
1903 Returns an MultiIndex object after removing NA/NaN values.
1904
1905 See Also
1906 --------
1907 Index.fillna : Fill NA/NaN values with the specified value.
1908 Index.isna : Detect missing values.
1909
1910 Examples
1911 --------
1912 >>> mi = pd.MultiIndex.from_arrays(([np.nan, np.nan, 2.0], [3.0, np.nan, 4.0]))
1913 >>> mi.dropna()
1914 MultiIndex([(2.0, 4.0)],
1915 )
1916 >>> mi.dropna(how="all")
1917 MultiIndex([(nan, 3.0),
1918 (2.0, 4.0)],
1919 )
1920 """
1921 nans = [level_codes == -1 for level_codes in self.codes]
1922 if how == "any":
1923 indexer = np.any(nans, axis=0)
1924 elif how == "all":
1925 indexer = np.all(nans, axis=0)
1926 else:
1927 raise ValueError(f"invalid how option: {how}")
1928
1929 new_codes = [level_codes[~indexer] for level_codes in self.codes]
1930 return self.set_codes(codes=new_codes)
1931
1932 def _get_level_values(self, level: int, unique: bool = False) -> Index:
1933 """
1934 Return vector of label values for requested level,
1935 equal to the length of the index
1936
1937 **this is an internal method**
1938
1939 Parameters
1940 ----------
1941 level : int
1942 unique : bool, default False
1943 if True, drop duplicated values
1944
1945 Returns
1946 -------
1947 Index
1948 """
1949 lev = self.levels[level]
1950 level_codes = self.codes[level]
1951 name = self._names[level]
1952 if unique:
1953 level_codes = algos.unique(level_codes)
1954 filled = algos.take_nd(lev._values, level_codes, fill_value=lev._na_value)
1955 return lev._shallow_copy(filled, name=name)
1956
1957 def get_level_values(self, level) -> Index:
1958 """
1959 Return vector of label values for requested level.
1960
1961 Length of returned vector is equal to the length of the index.
1962 The `get_level_values` method is a crucial utility for extracting
1963 specific level values from a `MultiIndex`. This function is particularly
1964 useful when working with multi-level data, allowing you to isolate
1965 and manipulate individual levels without having to deal with the
1966 complexity of the entire `MultiIndex` structure. It seamlessly handles
1967 both integer and string-based level access, providing flexibility in
1968 how you can interact with the data. Additionally, this method ensures
1969 that the returned `Index` maintains the integrity of the original data,
1970 even when missing values are present, by appropriately casting the
1971 result to a suitable data type.
1972
1973 Parameters
1974 ----------
1975 level : int or str
1976 ``level`` is either the integer position of the level in the
1977 MultiIndex, or the name of the level.
1978
1979 Returns
1980 -------
1981 Index
1982 Values is a level of this MultiIndex converted to
1983 a single :class:`Index` (or subclass thereof).
1984
1985 See Also
1986 --------
1987 MultiIndex : A multi-level, or hierarchical, index object for pandas objects.
1988 Index : Immutable sequence used for indexing and alignment.
1989 MultiIndex.remove_unused_levels : Create new MultiIndex from current that
1990 removes unused levels.
1991
1992 Notes
1993 -----
1994 If the level contains missing values, the result may be casted to
1995 ``float`` with missing values specified as ``NaN``. This is because
1996 the level is converted to a regular ``Index``.
1997
1998 Examples
1999 --------
2000 Create a MultiIndex:
2001
2002 >>> mi = pd.MultiIndex.from_arrays((list("abc"), list("def")))
2003 >>> mi.names = ["level_1", "level_2"]
2004
2005 Get level values by supplying level as either integer or name:
2006
2007 >>> mi.get_level_values(0)
2008 Index(['a', 'b', 'c'], dtype='str', name='level_1')
2009 >>> mi.get_level_values("level_2")
2010 Index(['d', 'e', 'f'], dtype='str', name='level_2')
2011
2012 If a level contains missing values, the return type of the level
2013 may be cast to ``float``.
2014
2015 >>> pd.MultiIndex.from_arrays([[1, None, 2], [3, 4, 5]]).dtypes
2016 level_0 int64
2017 level_1 int64
2018 dtype: object
2019 >>> pd.MultiIndex.from_arrays([[1, None, 2], [3, 4, 5]]).get_level_values(0)
2020 Index([1.0, nan, 2.0], dtype='float64')
2021 """
2022 level = self._get_level_number(level)
2023 values = self._get_level_values(level)
2024 return values
2025
2026 def unique(self, level=None):
2027 """
2028 Return unique values in the index.
2029
2030 Unique values are returned in order of appearance, this does NOT sort.
2031
2032 Parameters
2033 ----------
2034 level : int or hashable, optional
2035 Only return values from specified level (for MultiIndex).
2036 If int, gets the level by integer position, else by level name.
2037
2038 Returns
2039 -------
2040 MultiIndex
2041 Unique values in the MultiIndex.
2042
2043 See Also
2044 --------
2045 unique : Numpy array of unique values in that column.
2046 Series.unique : Return unique values of Series object.
2047
2048 Examples
2049 --------
2050 >>> mi = pd.MultiIndex.from_arrays((list("abca"), list("defd")))
2051 >>> mi
2052 MultiIndex([('a', 'd'),
2053 ('b', 'e'),
2054 ('c', 'f'),
2055 ('a', 'd')],
2056 )
2057 >>> mi.unique()
2058 MultiIndex([('a', 'd'),
2059 ('b', 'e'),
2060 ('c', 'f')],
2061 )
2062 """
2063 if level is None:
2064 return self.drop_duplicates()
2065 else:
2066 level = self._get_level_number(level)
2067 return self._get_level_values(level=level, unique=True)
2068
2069 def to_frame(
2070 self,
2071 index: bool = True,
2072 name=lib.no_default,
2073 allow_duplicates: bool = False,
2074 ) -> DataFrame:
2075 """
2076 Create a DataFrame with the levels of the MultiIndex as columns.
2077
2078 Column ordering is determined by the DataFrame constructor with data as
2079 a dict.
2080
2081 Parameters
2082 ----------
2083 index : bool, default True
2084 Set the index of the returned DataFrame as the original MultiIndex.
2085
2086 name : list / sequence of str, optional
2087 The passed names should substitute index level names.
2088
2089 allow_duplicates : bool, optional default False
2090 Allow duplicate column labels to be created.
2091
2092 Returns
2093 -------
2094 DataFrame
2095 DataFrame representation of the MultiIndex, with levels as columns.
2096
2097 See Also
2098 --------
2099 DataFrame : Two-dimensional, size-mutable, potentially heterogeneous
2100 tabular data.
2101
2102 Examples
2103 --------
2104 >>> mi = pd.MultiIndex.from_arrays([["a", "b"], ["c", "d"]])
2105 >>> mi
2106 MultiIndex([('a', 'c'),
2107 ('b', 'd')],
2108 )
2109
2110 >>> df = mi.to_frame()
2111 >>> df
2112 0 1
2113 a c a c
2114 b d b d
2115
2116 >>> df = mi.to_frame(index=False)
2117 >>> df
2118 0 1
2119 0 a c
2120 1 b d
2121
2122 >>> df = mi.to_frame(name=["x", "y"])
2123 >>> df
2124 x y
2125 a c a c
2126 b d b d
2127 """
2128 from pandas import DataFrame
2129
2130 if name is not lib.no_default:
2131 if not is_list_like(name):
2132 raise TypeError("'name' must be a list / sequence of column names.")
2133
2134 if len(name) != len(self.levels):
2135 raise ValueError(
2136 "'name' should have same length as number of levels on index."
2137 )
2138 idx_names = name
2139 else:
2140 idx_names = self._get_level_names()
2141
2142 if not allow_duplicates and len(set(idx_names)) != len(idx_names):
2143 raise ValueError(
2144 "Cannot create duplicate column labels if allow_duplicates is False"
2145 )
2146
2147 # Guarantee resulting column order - PY36+ dict maintains insertion order
2148 result = DataFrame(
2149 {level: self._get_level_values(level) for level in range(len(self.levels))},
2150 copy=False,
2151 )
2152 result.columns = idx_names
2153
2154 if index:
2155 result.index = self
2156 return result
2157
2158 # error: Return type "Index" of "to_flat_index" incompatible with return type
2159 # "MultiIndex" in supertype "Index"
2160 def to_flat_index(self) -> Index: # type: ignore[override]
2161 """
2162 Convert a MultiIndex to an Index of Tuples containing the level values.
2163
2164 Returns
2165 -------
2166 pd.Index
2167 Index with the MultiIndex data represented in Tuples.
2168
2169 See Also
2170 --------
2171 MultiIndex.from_tuples : Convert flat index back to MultiIndex.
2172
2173 Notes
2174 -----
2175 This method will simply return the caller if called by anything other
2176 than a MultiIndex.
2177
2178 Examples
2179 --------
2180 >>> index = pd.MultiIndex.from_product(
2181 ... [["foo", "bar"], ["baz", "qux"]], names=["a", "b"]
2182 ... )
2183 >>> index.to_flat_index()
2184 Index([('foo', 'baz'), ('foo', 'qux'),
2185 ('bar', 'baz'), ('bar', 'qux')],
2186 dtype='object')
2187 """
2188 return Index(self._values, tupleize_cols=False, copy=False)
2189
2190 def _is_lexsorted(self) -> bool:
2191 """
2192 Return True if the codes are lexicographically sorted.
2193
2194 Returns
2195 -------
2196 bool
2197
2198 Examples
2199 --------
2200 In the below examples, the first level of the MultiIndex is sorted because
2201 a<b<c, so there is no need to look at the next level.
2202
2203 >>> pd.MultiIndex.from_arrays(
2204 ... [["a", "b", "c"], ["d", "e", "f"]]
2205 ... )._is_lexsorted()
2206 True
2207 >>> pd.MultiIndex.from_arrays(
2208 ... [["a", "b", "c"], ["d", "f", "e"]]
2209 ... )._is_lexsorted()
2210 True
2211
2212 In case there is a tie, the lexicographical sorting looks
2213 at the next level of the MultiIndex.
2214
2215 >>> pd.MultiIndex.from_arrays([[0, 1, 1], ["a", "b", "c"]])._is_lexsorted()
2216 True
2217 >>> pd.MultiIndex.from_arrays([[0, 1, 1], ["a", "c", "b"]])._is_lexsorted()
2218 False
2219 >>> pd.MultiIndex.from_arrays(
2220 ... [["a", "a", "b", "b"], ["aa", "bb", "aa", "bb"]]
2221 ... )._is_lexsorted()
2222 True
2223 >>> pd.MultiIndex.from_arrays(
2224 ... [["a", "a", "b", "b"], ["bb", "aa", "aa", "bb"]]
2225 ... )._is_lexsorted()
2226 False
2227 """
2228 return self._lexsort_depth == self.nlevels
2229
2230 @cache_readonly
2231 def _lexsort_depth(self) -> int:
2232 """
2233 Compute and return the lexsort_depth, the number of levels of the
2234 MultiIndex that are sorted lexically
2235
2236 Returns
2237 -------
2238 int
2239 """
2240 if self.sortorder is not None:
2241 return self.sortorder
2242 return _lexsort_depth(self.codes, self.nlevels)
2243
2244 def _sort_levels_monotonic(self, raise_if_incomparable: bool = False) -> MultiIndex:
2245 """
2246 This is an *internal* function.
2247
2248 Create a new MultiIndex from the current to monotonically sorted
2249 items IN the levels. This does not actually make the entire MultiIndex
2250 monotonic, JUST the levels.
2251
2252 The resulting MultiIndex will have the same outward
2253 appearance, meaning the same .values and ordering. It will also
2254 be .equals() to the original.
2255
2256 Returns
2257 -------
2258 MultiIndex
2259
2260 Examples
2261 --------
2262 >>> mi = pd.MultiIndex(
2263 ... levels=[["a", "b"], ["bb", "aa"]], codes=[[0, 0, 1, 1], [0, 1, 0, 1]]
2264 ... )
2265 >>> mi
2266 MultiIndex([('a', 'bb'),
2267 ('a', 'aa'),
2268 ('b', 'bb'),
2269 ('b', 'aa')],
2270 )
2271
2272 >>> mi.sort_values()
2273 MultiIndex([('a', 'aa'),
2274 ('a', 'bb'),
2275 ('b', 'aa'),
2276 ('b', 'bb')],
2277 )
2278 """
2279 if self._is_lexsorted() and self.is_monotonic_increasing:
2280 return self
2281
2282 new_levels = []
2283 new_codes = []
2284
2285 for lev, level_codes in zip(self.levels, self.codes, strict=True):
2286 if not lev.is_monotonic_increasing:
2287 try:
2288 # indexer to reorder the levels
2289 indexer = lev.argsort()
2290 except TypeError:
2291 if raise_if_incomparable:
2292 raise
2293 else:
2294 lev = lev.take(indexer)
2295
2296 # indexer to reorder the level codes
2297 indexer = ensure_platform_int(indexer)
2298 ri = lib.get_reverse_indexer(indexer, len(indexer))
2299 level_codes = algos.take_nd(ri, level_codes, fill_value=-1)
2300
2301 new_levels.append(lev)
2302 new_codes.append(level_codes)
2303
2304 return MultiIndex(
2305 new_levels,
2306 new_codes,
2307 names=self.names,
2308 sortorder=self.sortorder,
2309 verify_integrity=False,
2310 )
2311
2312 def remove_unused_levels(self) -> MultiIndex:
2313 """
2314 Create new MultiIndex from current that removes unused levels.
2315
2316 Unused level(s) means levels that are not expressed in the
2317 labels. The resulting MultiIndex will have the same outward
2318 appearance, meaning the same .values and ordering. It will
2319 also be .equals() to the original.
2320
2321 The `remove_unused_levels` method is useful in cases where you have a
2322 MultiIndex with hierarchical levels, but some of these levels are no
2323 longer needed due to filtering or subsetting operations. By removing
2324 the unused levels, the resulting MultiIndex becomes more compact and
2325 efficient, which can improve performance in subsequent operations.
2326
2327 Returns
2328 -------
2329 MultiIndex
2330 A new MultiIndex with unused levels removed.
2331
2332 See Also
2333 --------
2334 MultiIndex.droplevel : Remove specified levels from a MultiIndex.
2335 MultiIndex.reorder_levels : Rearrange levels of a MultiIndex.
2336 MultiIndex.set_levels : Set new levels on a MultiIndex.
2337
2338 Examples
2339 --------
2340 >>> mi = pd.MultiIndex.from_product([range(2), list("ab")])
2341 >>> mi
2342 MultiIndex([(0, 'a'),
2343 (0, 'b'),
2344 (1, 'a'),
2345 (1, 'b')],
2346 )
2347
2348 >>> mi[2:]
2349 MultiIndex([(1, 'a'),
2350 (1, 'b')],
2351 )
2352
2353 The 0 from the first level is not represented
2354 and can be removed
2355
2356 >>> mi2 = mi[2:].remove_unused_levels()
2357 >>> mi2.levels
2358 FrozenList([[1], ['a', 'b']])
2359 """
2360 new_levels = []
2361 new_codes = []
2362
2363 changed = False
2364 for lev, level_codes in zip(self.levels, self.codes, strict=True):
2365 # Since few levels are typically unused, bincount() is more
2366 # efficient than unique() - however it only accepts positive values
2367 # (and drops order):
2368 uniques = np.where(np.bincount(level_codes + 1) > 0)[0] - 1
2369 has_na = int(len(uniques) and (uniques[0] == -1))
2370
2371 if len(uniques) != len(lev) + has_na:
2372 if lev.isna().any() and len(uniques) == len(lev):
2373 break
2374 # We have unused levels
2375 changed = True
2376
2377 # Recalculate uniques, now preserving order.
2378 # Can easily be cythonized by exploiting the already existing
2379 # "uniques" and stop parsing "level_codes" when all items
2380 # are found:
2381 uniques = algos.unique(level_codes)
2382 if has_na:
2383 na_idx = np.where(uniques == -1)[0]
2384 # Just ensure that -1 is in first position:
2385 uniques[[0, na_idx[0]]] = uniques[[na_idx[0], 0]]
2386
2387 # codes get mapped from uniques to 0:len(uniques)
2388 # -1 (if present) is mapped to last position
2389 code_mapping = np.zeros(len(lev) + has_na)
2390 # ... and reassigned value -1:
2391 code_mapping[uniques] = np.arange(len(uniques)) - has_na
2392
2393 level_codes = code_mapping[level_codes]
2394
2395 # new levels are simple
2396 lev = lev.take(uniques[has_na:])
2397
2398 new_levels.append(lev)
2399 new_codes.append(level_codes)
2400
2401 result = self.view()
2402
2403 if changed:
2404 result._reset_identity()
2405 result._set_levels(new_levels, validate=False)
2406 result._set_codes(new_codes, validate=False)
2407
2408 return result
2409
2410 # --------------------------------------------------------------------
2411 # Pickling Methods
2412
2413 def __reduce__(self):
2414 """Necessary for making this object picklable"""
2415 d = {
2416 "levels": list(self.levels),
2417 "codes": list(self.codes),
2418 "sortorder": self.sortorder,
2419 "names": list(self.names),
2420 }
2421 return ibase._new_Index, (type(self), d), None
2422
2423 # --------------------------------------------------------------------
2424
2425 def __getitem__(self, key):
2426 key = lib.item_from_zerodim(key)
2427 if is_scalar(key):
2428 key = com.cast_scalar_indexer(key)
2429
2430 retval = []
2431 for lev, level_codes in zip(self.levels, self.codes, strict=True):
2432 if level_codes[key] == -1:
2433 retval.append(np.nan)
2434 else:
2435 retval.append(lev[level_codes[key]])
2436
2437 return tuple(retval)
2438 else:
2439 # in general cannot be sure whether the result will be sorted
2440 sortorder = None
2441 if com.is_bool_indexer(key):
2442 key = np.asarray(key, dtype=bool)
2443 sortorder = self.sortorder
2444 elif isinstance(key, slice):
2445 if key.step is None or key.step > 0:
2446 sortorder = self.sortorder
2447 elif isinstance(key, Index):
2448 key = np.asarray(key)
2449
2450 new_codes = [level_codes[key] for level_codes in self.codes]
2451
2452 return MultiIndex(
2453 levels=self.levels,
2454 codes=new_codes,
2455 names=self.names,
2456 sortorder=sortorder,
2457 verify_integrity=False,
2458 )
2459
2460 def _getitem_slice(self: MultiIndex, slobj: slice) -> MultiIndex:
2461 """
2462 Fastpath for __getitem__ when we know we have a slice.
2463 """
2464 sortorder = None
2465 if slobj.step is None or slobj.step > 0:
2466 sortorder = self.sortorder
2467
2468 new_codes = [level_codes[slobj] for level_codes in self.codes]
2469
2470 return type(self)(
2471 levels=self.levels,
2472 codes=new_codes,
2473 names=self._names,
2474 sortorder=sortorder,
2475 verify_integrity=False,
2476 )
2477
2478 def take(
2479 self: MultiIndex,
2480 indices,
2481 axis: Axis = 0,
2482 allow_fill: bool = True,
2483 fill_value=None,
2484 **kwargs,
2485 ) -> MultiIndex:
2486 """
2487 Return a new MultiIndex of the values selected by the indices.
2488
2489 For internal compatibility with numpy arrays.
2490
2491 Parameters
2492 ----------
2493 indices : array-like
2494 Indices to be taken.
2495 axis : {0 or 'index'}, optional
2496 The axis over which to select values, always 0 or 'index'.
2497 allow_fill : bool, default True
2498 How to handle negative values in `indices`.
2499
2500 * False: negative values in `indices` indicate positional indices
2501 from the right (the default). This is similar to
2502 :func:`numpy.take`.
2503
2504 * True: negative values in `indices` indicate
2505 missing values. These values are set to `fill_value`. Any other
2506 other negative values raise a ``ValueError``.
2507
2508 fill_value : scalar, default None
2509 If allow_fill=True and fill_value is not None, indices specified by
2510 -1 are regarded as NA. If Index doesn't hold NA, raise ValueError.
2511 **kwargs
2512 Required for compatibility with numpy.
2513
2514 Returns
2515 -------
2516 Index
2517 An index formed of elements at the given indices. Will be the same
2518 type as self, except for RangeIndex.
2519
2520 See Also
2521 --------
2522 numpy.ndarray.take: Return an array formed from the
2523 elements of a at the given indices.
2524
2525 Examples
2526 --------
2527 >>> idx = pd.MultiIndex.from_arrays([["a", "b", "c"], [1, 2, 3]])
2528 >>> idx
2529 MultiIndex([('a', 1),
2530 ('b', 2),
2531 ('c', 3)],
2532 )
2533 >>> idx.take([2, 2, 1, 0])
2534 MultiIndex([('c', 3),
2535 ('c', 3),
2536 ('b', 2),
2537 ('a', 1)],
2538 )
2539 """
2540 nv.validate_take((), kwargs)
2541 indices = ensure_platform_int(indices)
2542
2543 # only fill if we are passing a non-None fill_value
2544 allow_fill = self._maybe_disallow_fill(allow_fill, fill_value, indices)
2545
2546 if indices.ndim == 1 and lib.is_range_indexer(indices, len(self)):
2547 return self.copy()
2548
2549 na_value = -1
2550
2551 taken = [lab.take(indices) for lab in self.codes]
2552 if allow_fill:
2553 mask = indices == -1
2554 if mask.any():
2555 masked = []
2556 for new_label in taken:
2557 label_values = new_label
2558 label_values[mask] = na_value
2559 masked.append(np.asarray(label_values))
2560 taken = masked
2561
2562 return MultiIndex(
2563 levels=self.levels, codes=taken, names=self.names, verify_integrity=False
2564 )
2565
2566 def append(self, other):
2567 """
2568 Append a collection of Index options together.
2569
2570 The `append` method is used to combine multiple `Index` objects into a single
2571 `Index`. This is particularly useful when dealing with multi-level indexing
2572 (MultiIndex) where you might need to concatenate different levels of indices.
2573 The method handles the alignment of the levels and codes of the indices being
2574 appended to ensure consistency in the resulting `MultiIndex`.
2575
2576 Parameters
2577 ----------
2578 other : Index or list/tuple of indices
2579 Index or list/tuple of Index objects to be appended.
2580
2581 Returns
2582 -------
2583 Index
2584 The combined index.
2585
2586 See Also
2587 --------
2588 MultiIndex: A multi-level, or hierarchical, index object for pandas objects.
2589 Index.append : Append a collection of Index options together.
2590 concat : Concatenate pandas objects along a particular axis.
2591
2592 Examples
2593 --------
2594 >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"]])
2595 >>> mi
2596 MultiIndex([('a', 'b')],
2597 )
2598 >>> mi.append(mi)
2599 MultiIndex([('a', 'b'), ('a', 'b')],
2600 )
2601 """
2602 if not isinstance(other, (list, tuple)):
2603 other = [other]
2604
2605 if all(
2606 (isinstance(o, MultiIndex) and o.nlevels >= self.nlevels) for o in other
2607 ):
2608 codes = []
2609 levels = []
2610 names = []
2611 for i in range(self.nlevels):
2612 level_values = self.levels[i]
2613 for mi in other:
2614 level_values = level_values.union(mi.levels[i])
2615 level_codes = [
2616 recode_for_categories(
2617 mi.codes[i], mi.levels[i], level_values, copy=False
2618 )
2619 for mi in ([self, *other])
2620 ]
2621 level_name = self.names[i]
2622 if any(mi.names[i] != level_name for mi in other):
2623 level_name = None
2624 codes.append(np.concatenate(level_codes))
2625 levels.append(level_values)
2626 names.append(level_name)
2627 return MultiIndex(
2628 codes=codes, levels=levels, names=names, verify_integrity=False
2629 )
2630
2631 to_concat = (self._values, *tuple(k._values for k in other))
2632 new_tuples = np.concatenate(to_concat)
2633
2634 # if all(isinstance(x, MultiIndex) for x in other):
2635 try:
2636 # We only get here if other contains at least one index with tuples,
2637 # setting names to None automatically
2638 return MultiIndex.from_tuples(new_tuples)
2639 except (TypeError, IndexError):
2640 return Index(new_tuples, copy=False)
2641
2642 def argsort(
2643 self, *args, na_position: NaPosition = "last", **kwargs
2644 ) -> npt.NDArray[np.intp]:
2645 """
2646 Return the integer indices that would sort the index.
2647
2648 Parameters
2649 ----------
2650 *args
2651 Passed to `numpy.ndarray.argsort`.
2652 na_position : {'first' or 'last'}, default 'last'
2653 Argument 'first' puts NaNs at the beginning, 'last' puts NaNs at
2654 the end.
2655 **kwargs
2656 Passed to `numpy.ndarray.argsort`.
2657
2658 Returns
2659 -------
2660 np.ndarray[np.intp]
2661 Integer indices that would sort the index if used as
2662 an indexer.
2663
2664 See Also
2665 --------
2666 numpy.argsort : Similar method for NumPy arrays.
2667 Index.argsort : Similar method for Index.
2668
2669 Examples
2670 --------
2671 >>> midx = pd.MultiIndex.from_arrays([[3, 2], ["e", "c"]])
2672 >>> midx
2673 MultiIndex([(3, 'e'),
2674 (2, 'c')],
2675 )
2676
2677 >>> order = midx.argsort()
2678 >>> order
2679 array([1, 0])
2680
2681 >>> midx[order]
2682 MultiIndex([(2, 'c'),
2683 (3, 'e')],
2684 )
2685
2686 >>> midx = pd.MultiIndex.from_arrays([[2, 2], [np.nan, 0]])
2687 >>> midx.argsort(na_position="first")
2688 array([0, 1])
2689
2690 >>> midx.argsort()
2691 array([1, 0])
2692 """
2693 target = self._sort_levels_monotonic(raise_if_incomparable=True)
2694 keys = [lev.codes for lev in target._get_codes_for_sorting()]
2695 return lexsort_indexer(keys, na_position=na_position, codes_given=True)
2696
2697 def repeat(self, repeats: int, axis=None) -> MultiIndex:
2698 """
2699 Repeat elements of a MultiIndex.
2700
2701 Returns a new MultiIndex where each element of the current MultiIndex
2702 is repeated consecutively a given number of times.
2703
2704 Parameters
2705 ----------
2706 repeats : int or array of ints
2707 The number of repetitions for each element. This should be a
2708 non-negative integer. Repeating 0 times will return an empty
2709 MultiIndex.
2710 axis : None
2711 Must be ``None``. Has no effect but is accepted for compatibility
2712 with numpy.
2713
2714 Returns
2715 -------
2716 MultiIndex
2717 Newly created MultiIndex with repeated elements.
2718
2719 See Also
2720 --------
2721 Series.repeat : Equivalent function for Series.
2722 numpy.repeat : Similar method for :class:`numpy.ndarray`.
2723
2724 Examples
2725 --------
2726 >>> idx = pd.MultiIndex.from_arrays([["a", "b", "c"], [1, 2, 3]])
2727 >>> idx
2728 MultiIndex([('a', 1),
2729 ('b', 2),
2730 ('c', 3)],
2731 )
2732 >>> idx.repeat(2)
2733 MultiIndex([('a', 1),
2734 ('a', 1),
2735 ('b', 2),
2736 ('b', 2),
2737 ('c', 3),
2738 ('c', 3)],
2739 )
2740 >>> idx.repeat([1, 2, 3])
2741 MultiIndex([('a', 1),
2742 ('b', 2),
2743 ('b', 2),
2744 ('c', 3),
2745 ('c', 3),
2746 ('c', 3)],
2747 )
2748 """
2749 nv.validate_repeat((), {"axis": axis})
2750 # error: Incompatible types in assignment (expression has type "ndarray",
2751 # variable has type "int")
2752 repeats = ensure_platform_int(repeats) # type: ignore[assignment]
2753 return MultiIndex(
2754 levels=self.levels,
2755 codes=[
2756 level_codes.view(np.ndarray).astype(np.intp, copy=False).repeat(repeats)
2757 for level_codes in self.codes
2758 ],
2759 names=self.names,
2760 sortorder=self.sortorder,
2761 verify_integrity=False,
2762 )
2763
2764 # error: Signature of "drop" incompatible with supertype "Index"
2765 def drop( # type: ignore[override]
2766 self,
2767 codes,
2768 level: Index | np.ndarray | Iterable[Hashable] | None = None,
2769 errors: IgnoreRaise = "raise",
2770 ) -> MultiIndex:
2771 """
2772 Make a new :class:`pandas.MultiIndex` with the passed list of codes deleted.
2773
2774 This method allows for the removal of specified labels from a MultiIndex.
2775 The labels to be removed can be provided as a list of tuples if no level
2776 is specified, or as a list of labels from a specific level if the level
2777 parameter is provided. This can be useful for refining the structure of a
2778 MultiIndex to fit specific requirements.
2779
2780 Parameters
2781 ----------
2782 codes : array-like
2783 Must be a list of tuples when ``level`` is not specified.
2784 level : int or level name, default None
2785 Level from which the labels will be dropped.
2786 errors : str, default 'raise'
2787 If 'ignore', suppress error and existing labels are dropped.
2788
2789 Returns
2790 -------
2791 MultiIndex
2792 A new MultiIndex with the specified labels removed.
2793
2794 See Also
2795 --------
2796 MultiIndex.remove_unused_levels : Create new MultiIndex from current that
2797 removes unused levels.
2798 MultiIndex.reorder_levels : Rearrange levels using input order.
2799 MultiIndex.rename : Rename levels in a MultiIndex.
2800
2801 Examples
2802 --------
2803 >>> idx = pd.MultiIndex.from_product(
2804 ... [(0, 1, 2), ("green", "purple")], names=["number", "color"]
2805 ... )
2806 >>> idx
2807 MultiIndex([(0, 'green'),
2808 (0, 'purple'),
2809 (1, 'green'),
2810 (1, 'purple'),
2811 (2, 'green'),
2812 (2, 'purple')],
2813 names=['number', 'color'])
2814 >>> idx.drop([(1, "green"), (2, "purple")])
2815 MultiIndex([(0, 'green'),
2816 (0, 'purple'),
2817 (1, 'purple'),
2818 (2, 'green')],
2819 names=['number', 'color'])
2820
2821 We can also drop from a specific level.
2822
2823 >>> idx.drop("green", level="color")
2824 MultiIndex([(0, 'purple'),
2825 (1, 'purple'),
2826 (2, 'purple')],
2827 names=['number', 'color'])
2828
2829 >>> idx.drop([1, 2], level=0)
2830 MultiIndex([(0, 'green'),
2831 (0, 'purple')],
2832 names=['number', 'color'])
2833 """
2834 if level is not None:
2835 return self._drop_from_level(codes, level, errors)
2836
2837 if not isinstance(codes, (np.ndarray, Index)):
2838 try:
2839 codes = com.index_labels_to_array(codes, dtype=np.dtype("object"))
2840 except ValueError:
2841 pass
2842
2843 inds = []
2844 for level_codes in codes:
2845 try:
2846 loc = self.get_loc(level_codes)
2847 # get_loc returns either an integer, a slice, or a boolean
2848 # mask
2849 if isinstance(loc, int):
2850 inds.append(loc)
2851 elif isinstance(loc, slice):
2852 step = loc.step if loc.step is not None else 1
2853 inds.extend(range(loc.start, loc.stop, step))
2854 elif com.is_bool_indexer(loc):
2855 if get_option("performance_warnings") and self._lexsort_depth == 0:
2856 warnings.warn(
2857 "dropping on a non-lexsorted multi-index "
2858 "without a level parameter may impact performance.",
2859 PerformanceWarning,
2860 stacklevel=find_stack_level(),
2861 )
2862 loc = loc.nonzero()[0]
2863 inds.extend(loc)
2864 else:
2865 msg = f"unsupported indexer of type {type(loc)}"
2866 raise AssertionError(msg)
2867 except KeyError:
2868 if errors != "ignore":
2869 raise
2870
2871 return self.delete(inds)
2872
2873 def _drop_from_level(
2874 self, codes, level, errors: IgnoreRaise = "raise"
2875 ) -> MultiIndex:
2876 codes = com.index_labels_to_array(codes)
2877 i = self._get_level_number(level)
2878 index = self.levels[i]
2879 values = index.get_indexer(codes)
2880 # If nan should be dropped it will equal -1 here. We have to check which values
2881 # are not nan and equal -1, this means they are missing in the index
2882 nan_codes = isna(codes)
2883 values[(np.equal(nan_codes, False)) & (values == -1)] = -2
2884 if index.shape[0] == self.shape[0]:
2885 values[np.equal(nan_codes, True)] = -2
2886
2887 not_found = codes[values == -2]
2888 if len(not_found) != 0 and errors != "ignore":
2889 raise KeyError(f"labels {not_found} not found in level")
2890 mask = ~algos.isin(self.codes[i], values)
2891
2892 return self[mask]
2893
2894 def swaplevel(self, i=-2, j=-1) -> MultiIndex:
2895 """
2896 Swap level i with level j.
2897
2898 Calling this method does not change the ordering of the values.
2899
2900 Default is to swap the last two levels of the MultiIndex.
2901
2902 Parameters
2903 ----------
2904 i : int, str, default -2
2905 First level of index to be swapped. Can pass level name as string.
2906 Type of parameters can be mixed. If i is a negative int, the first
2907 level is indexed relative to the end of the MultiIndex.
2908 j : int, str, default -1
2909 Second level of index to be swapped. Can pass level name as string.
2910 Type of parameters can be mixed. If j is a negative int, the second
2911 level is indexed relative to the end of the MultiIndex.
2912
2913 Returns
2914 -------
2915 MultiIndex
2916 A new MultiIndex.
2917
2918 See Also
2919 --------
2920 Series.swaplevel : Swap levels i and j in a MultiIndex.
2921 DataFrame.swaplevel : Swap levels i and j in a MultiIndex on a
2922 particular axis.
2923
2924 Examples
2925 --------
2926 >>> mi = pd.MultiIndex(
2927 ... levels=[["a", "b"], ["bb", "aa"], ["aaa", "bbb"]],
2928 ... codes=[[0, 0, 1, 1], [0, 1, 0, 1], [1, 0, 1, 0]],
2929 ... )
2930 >>> mi
2931 MultiIndex([('a', 'bb', 'bbb'),
2932 ('a', 'aa', 'aaa'),
2933 ('b', 'bb', 'bbb'),
2934 ('b', 'aa', 'aaa')],
2935 )
2936 >>> mi.swaplevel()
2937 MultiIndex([('a', 'bbb', 'bb'),
2938 ('a', 'aaa', 'aa'),
2939 ('b', 'bbb', 'bb'),
2940 ('b', 'aaa', 'aa')],
2941 )
2942 >>> mi.swaplevel(0)
2943 MultiIndex([('bbb', 'bb', 'a'),
2944 ('aaa', 'aa', 'a'),
2945 ('bbb', 'bb', 'b'),
2946 ('aaa', 'aa', 'b')],
2947 )
2948 >>> mi.swaplevel(0, 1)
2949 MultiIndex([('bb', 'a', 'bbb'),
2950 ('aa', 'a', 'aaa'),
2951 ('bb', 'b', 'bbb'),
2952 ('aa', 'b', 'aaa')],
2953 )
2954 """
2955 new_levels = list(self.levels)
2956 new_codes = list(self.codes)
2957 new_names = list(self.names)
2958
2959 i = self._get_level_number(i)
2960 j = self._get_level_number(j)
2961
2962 new_levels[i], new_levels[j] = new_levels[j], new_levels[i]
2963 new_codes[i], new_codes[j] = new_codes[j], new_codes[i]
2964 new_names[i], new_names[j] = new_names[j], new_names[i]
2965
2966 return MultiIndex(
2967 levels=new_levels, codes=new_codes, names=new_names, verify_integrity=False
2968 )
2969
2970 def reorder_levels(self, order) -> MultiIndex:
2971 """
2972 Rearrange levels using input order. May not drop or duplicate levels.
2973
2974 `reorder_levels` is useful when you need to change the order of levels in
2975 a MultiIndex, such as when reordering levels for hierarchical indexing. It
2976 maintains the integrity of the MultiIndex, ensuring that all existing levels
2977 are present and no levels are duplicated. This method is helpful for aligning
2978 the index structure with other data structures or for optimizing the order
2979 for specific data operations.
2980
2981 Parameters
2982 ----------
2983 order : list of int or list of str
2984 List representing new level order. Reference level by number
2985 (position) or by key (label).
2986
2987 Returns
2988 -------
2989 MultiIndex
2990 A new MultiIndex with levels rearranged according to the specified order.
2991
2992 See Also
2993 --------
2994 MultiIndex.swaplevel : Swap two levels of the MultiIndex.
2995 MultiIndex.set_names : Set names for the MultiIndex levels.
2996 DataFrame.reorder_levels : Reorder levels in a DataFrame with a MultiIndex.
2997
2998 Examples
2999 --------
3000 >>> mi = pd.MultiIndex.from_arrays([[1, 2], [3, 4]], names=["x", "y"])
3001 >>> mi
3002 MultiIndex([(1, 3),
3003 (2, 4)],
3004 names=['x', 'y'])
3005
3006 >>> mi.reorder_levels(order=[1, 0])
3007 MultiIndex([(3, 1),
3008 (4, 2)],
3009 names=['y', 'x'])
3010
3011 >>> mi.reorder_levels(order=["y", "x"])
3012 MultiIndex([(3, 1),
3013 (4, 2)],
3014 names=['y', 'x'])
3015 """
3016 order = [self._get_level_number(i) for i in order]
3017 result = self._reorder_ilevels(order)
3018 return result
3019
3020 def _reorder_ilevels(self, order) -> MultiIndex:
3021 if len(order) != self.nlevels:
3022 raise AssertionError(
3023 f"Length of order must be same as number of levels ({self.nlevels}), "
3024 f"got {len(order)}"
3025 )
3026 new_levels = [self.levels[i] for i in order]
3027 new_codes = [self.codes[i] for i in order]
3028 new_names = [self.names[i] for i in order]
3029
3030 return MultiIndex(
3031 levels=new_levels, codes=new_codes, names=new_names, verify_integrity=False
3032 )
3033
3034 def _recode_for_new_levels(
3035 self, new_levels, *, copy: bool
3036 ) -> Generator[np.ndarray]:
3037 if len(new_levels) > self.nlevels:
3038 raise AssertionError(
3039 f"Length of new_levels ({len(new_levels)}) "
3040 f"must be <= self.nlevels ({self.nlevels})"
3041 )
3042 for i in range(len(new_levels)):
3043 yield recode_for_categories(
3044 self.codes[i], self.levels[i], new_levels[i], copy=copy
3045 )
3046
3047 def _get_codes_for_sorting(self) -> list[Categorical]:
3048 """
3049 we are categorizing our codes by using the
3050 available categories (all, not just observed)
3051 excluding any missing ones (-1); this is in preparation
3052 for sorting, where we need to disambiguate that -1 is not
3053 a valid valid
3054 """
3055
3056 def cats(level_codes: np.ndarray) -> np.ndarray:
3057 return np.arange(
3058 level_codes.max() + 1 if len(level_codes) else 0,
3059 dtype=level_codes.dtype,
3060 )
3061
3062 return [
3063 Categorical.from_codes(level_codes, cats(level_codes), True, validate=False)
3064 for level_codes in self.codes
3065 ]
3066
3067 def sortlevel(
3068 self,
3069 level: IndexLabel = 0,
3070 ascending: bool | list[bool] = True,
3071 sort_remaining: bool = True,
3072 na_position: str = "first",
3073 ) -> tuple[MultiIndex, npt.NDArray[np.intp]]:
3074 """
3075 Sort MultiIndex at the requested level.
3076
3077 This method is useful when dealing with MultiIndex objects, allowing for
3078 sorting at a specific level of the index. The function preserves the
3079 relative ordering of data within the same level while sorting
3080 the overall MultiIndex. The method provides flexibility with the `ascending`
3081 parameter to define the sort order and with the `sort_remaining` parameter to
3082 control whether the remaining levels should also be sorted. Sorting a
3083 MultiIndex can be crucial when performing operations that require ordered
3084 indices, such as grouping or merging datasets. The `na_position` argument is
3085 important in handling missing values consistently across different levels.
3086
3087 Parameters
3088 ----------
3089 level : list-like, int or str, default 0
3090 If a string is given, must be a name of the level.
3091 If list-like must be names or ints of levels.
3092 ascending : bool, default True
3093 False to sort in descending order.
3094 Can also be a list to specify a directed ordering.
3095 sort_remaining : bool, default True
3096 If True, sorts by the remaining levels after sorting by the specified
3097 `level`.
3098 na_position : {'first' or 'last'}, default 'first'
3099 Argument 'first' puts NaNs at the beginning, 'last' puts NaNs at
3100 the end.
3101
3102 .. versionadded:: 2.1.0
3103
3104 Returns
3105 -------
3106 sorted_index : pd.MultiIndex
3107 Resulting index.
3108 indexer : np.ndarray[np.intp]
3109 Indices of output values in original index.
3110
3111 See Also
3112 --------
3113 MultiIndex : A multi-level, or hierarchical, index object for pandas objects.
3114 Index.sort_values : Sort Index values.
3115 DataFrame.sort_index : Sort DataFrame by the index.
3116 Series.sort_index : Sort Series by the index.
3117
3118 Examples
3119 --------
3120 >>> mi = pd.MultiIndex.from_arrays([[0, 0], [2, 1]])
3121 >>> mi
3122 MultiIndex([(0, 2),
3123 (0, 1)],
3124 )
3125
3126 >>> mi.sortlevel()
3127 (MultiIndex([(0, 1),
3128 (0, 2)],
3129 ), array([1, 0]))
3130
3131 >>> mi.sortlevel(sort_remaining=False)
3132 (MultiIndex([(0, 2),
3133 (0, 1)],
3134 ), array([0, 1]))
3135
3136 >>> mi.sortlevel(1)
3137 (MultiIndex([(0, 1),
3138 (0, 2)],
3139 ), array([1, 0]))
3140
3141 >>> mi.sortlevel(1, ascending=False)
3142 (MultiIndex([(0, 2),
3143 (0, 1)],
3144 ), array([0, 1]))
3145 """
3146 if not is_list_like(level):
3147 level = [level]
3148 # error: Item "Hashable" of "Union[Hashable, Sequence[Hashable]]" has
3149 # no attribute "__iter__" (not iterable)
3150 level = [
3151 self._get_level_number(lev)
3152 for lev in level # type: ignore[union-attr]
3153 ]
3154 sortorder = None
3155
3156 codes = [self.codes[lev] for lev in level]
3157 # we have a directed ordering via ascending
3158 if isinstance(ascending, list):
3159 if not len(level) == len(ascending):
3160 raise ValueError("level must have same length as ascending")
3161 elif sort_remaining:
3162 codes.extend(
3163 [self.codes[lev] for lev in range(len(self.levels)) if lev not in level]
3164 )
3165 else:
3166 sortorder = level[0]
3167
3168 indexer = lexsort_indexer(
3169 codes, orders=ascending, na_position=na_position, codes_given=True
3170 )
3171
3172 indexer = ensure_platform_int(indexer)
3173 new_codes = [level_codes.take(indexer) for level_codes in self.codes]
3174
3175 new_index = MultiIndex(
3176 codes=new_codes,
3177 levels=self.levels,
3178 names=self.names,
3179 sortorder=sortorder,
3180 verify_integrity=False,
3181 )
3182
3183 return new_index, indexer
3184
3185 def _wrap_reindex_result(self, target, indexer, preserve_names: bool):
3186 if not isinstance(target, MultiIndex):
3187 if indexer is None:
3188 target = self
3189 elif (indexer >= 0).all():
3190 target = self.take(indexer)
3191 else:
3192 try:
3193 target = MultiIndex.from_tuples(target)
3194 except TypeError:
3195 # not all tuples, see test_constructor_dict_multiindex_reindex_flat
3196 return target
3197
3198 target = self._maybe_preserve_names(target, preserve_names)
3199 return target
3200
3201 def _maybe_preserve_names(self, target: IndexT, preserve_names: bool) -> IndexT:
3202 if (
3203 preserve_names
3204 and target.nlevels == self.nlevels
3205 and target.names != self.names
3206 ):
3207 target = target.copy(deep=False)
3208 target.names = self.names
3209 return target
3210
3211 # --------------------------------------------------------------------
3212 # Indexing Methods
3213
3214 def _check_indexing_error(self, key) -> None:
3215 if not is_hashable(key) or is_iterator(key):
3216 # We allow tuples if they are hashable, whereas other Index
3217 # subclasses require scalar.
3218 # We have to explicitly exclude generators, as these are hashable.
3219 raise InvalidIndexError(key)
3220
3221 @cache_readonly
3222 def _should_fallback_to_positional(self) -> bool:
3223 """
3224 Should integer key(s) be treated as positional?
3225 """
3226 # GH#33355
3227 return self.levels[0]._should_fallback_to_positional
3228
3229 def _get_indexer_strict(
3230 self, key, axis_name: str
3231 ) -> tuple[Index, npt.NDArray[np.intp]]:
3232 keyarr = key
3233 if not isinstance(keyarr, Index):
3234 keyarr = com.asarray_tuplesafe(keyarr)
3235
3236 if len(keyarr) and not isinstance(keyarr[0], tuple):
3237 indexer = self._get_indexer_level_0(keyarr)
3238
3239 self._raise_if_missing(key, indexer, axis_name)
3240 return self[indexer], indexer
3241
3242 return super()._get_indexer_strict(key, axis_name)
3243
3244 def _raise_if_missing(self, key, indexer, axis_name: str) -> None:
3245 keyarr = key
3246 if not isinstance(key, Index):
3247 keyarr = com.asarray_tuplesafe(key)
3248
3249 if len(keyarr) and not isinstance(keyarr[0], tuple):
3250 # i.e. same condition for special case in MultiIndex._get_indexer_strict
3251
3252 mask = indexer == -1
3253 if mask.any():
3254 check = self.levels[0].get_indexer(keyarr)
3255 cmask = check == -1
3256 if cmask.any():
3257 raise KeyError(f"{keyarr[cmask]} not in index")
3258 # We get here when levels still contain values which are not
3259 # actually in Index anymore
3260 raise KeyError(f"{keyarr} not in index")
3261 else:
3262 return super()._raise_if_missing(key, indexer, axis_name)
3263
3264 def _get_indexer_level_0(self, target) -> npt.NDArray[np.intp]:
3265 """
3266 Optimized equivalent to `self.get_level_values(0).get_indexer_for(target)`.
3267 """
3268 lev = self.levels[0]
3269 codes = self._codes[0]
3270 cat = Categorical.from_codes(codes=codes, categories=lev, validate=False)
3271 ci = Index(cat, copy=False)
3272 return ci.get_indexer_for(target)
3273
3274 def get_slice_bound(
3275 self,
3276 label: Hashable | Sequence[Hashable],
3277 side: Literal["left", "right"],
3278 ) -> int:
3279 """
3280 For an ordered MultiIndex, compute slice bound
3281 that corresponds to given label.
3282
3283 Returns leftmost (one-past-the-rightmost if `side=='right') position
3284 of given label.
3285
3286 Parameters
3287 ----------
3288 label : object or tuple of objects
3289 side : {'left', 'right'}
3290
3291 Returns
3292 -------
3293 int
3294 Index of label.
3295
3296 Notes
3297 -----
3298 This method only works if level 0 index of the MultiIndex is lexsorted.
3299
3300 Examples
3301 --------
3302 >>> mi = pd.MultiIndex.from_arrays([list("abbc"), list("gefd")])
3303
3304 Get the locations from the leftmost 'b' in the first level
3305 until the end of the multiindex:
3306
3307 >>> mi.get_slice_bound("b", side="left")
3308 1
3309
3310 Like above, but if you get the locations from the rightmost
3311 'b' in the first level and 'f' in the second level:
3312
3313 >>> mi.get_slice_bound(("b", "f"), side="right")
3314 3
3315
3316 See Also
3317 --------
3318 MultiIndex.get_loc : Get location for a label or a tuple of labels.
3319 MultiIndex.get_locs : Get location for a label/slice/list/mask or a
3320 sequence of such.
3321 """
3322 if not isinstance(label, tuple):
3323 label = (label,)
3324 result = self._partial_tup_index(label, side=side)
3325 result = maybe_unbox_numpy_scalar(result)
3326 return result
3327
3328 def slice_locs(self, start=None, end=None, step=None) -> tuple[int, int]:
3329 """
3330 For an ordered MultiIndex, compute the slice locations for input
3331 labels.
3332
3333 The input labels can be tuples representing partial levels, e.g. for a
3334 MultiIndex with 3 levels, you can pass a single value (corresponding to
3335 the first level), or a 1-, 2-, or 3-tuple.
3336
3337 Parameters
3338 ----------
3339 start : label or tuple, default None
3340 If None, defaults to the beginning
3341 end : label or tuple
3342 If None, defaults to the end
3343 step : int or None
3344 Slice step
3345
3346 Returns
3347 -------
3348 (start, end) : (int, int)
3349
3350 Notes
3351 -----
3352 This method only works if the MultiIndex is properly lexsorted. So,
3353 if only the first 2 levels of a 3-level MultiIndex are lexsorted,
3354 you can only pass two levels to ``.slice_locs``.
3355
3356 Examples
3357 --------
3358 >>> mi = pd.MultiIndex.from_arrays(
3359 ... [list("abbd"), list("deff")], names=["A", "B"]
3360 ... )
3361
3362 Get the slice locations from the beginning of 'b' in the first level
3363 until the end of the multiindex:
3364
3365 >>> mi.slice_locs(start="b")
3366 (1, 4)
3367
3368 Like above, but stop at the end of 'b' in the first level and 'f' in
3369 the second level:
3370
3371 >>> mi.slice_locs(start="b", end=("b", "f"))
3372 (1, 3)
3373
3374 See Also
3375 --------
3376 MultiIndex.get_loc : Get location for a label or a tuple of labels.
3377 MultiIndex.get_locs : Get location for a label/slice/list/mask or a
3378 sequence of such.
3379 """
3380 # This function adds nothing to its parent implementation (the magic
3381 # happens in get_slice_bound method), but it adds meaningful doc.
3382 return super().slice_locs(start, end, step)
3383
3384 def _partial_tup_index(self, tup: tuple, side: Literal["left", "right"] = "left"):
3385 if len(tup) > self._lexsort_depth:
3386 raise UnsortedIndexError(
3387 f"Key length ({len(tup)}) was greater than MultiIndex lexsort depth "
3388 f"({self._lexsort_depth})"
3389 )
3390
3391 n = len(tup)
3392 start, end = 0, len(self)
3393 zipped = zip(tup, self.levels, self.codes, strict=True)
3394 for k, (lab, lev, level_codes) in enumerate(zipped):
3395 section = level_codes[start:end]
3396
3397 loc: npt.NDArray[np.intp] | np.intp | int
3398 if lab not in lev and not isna(lab):
3399 # short circuit
3400 try:
3401 loc = algos.searchsorted(lev, lab, side=side)
3402 except TypeError as err:
3403 # non-comparable e.g. test_slice_locs_with_type_mismatch
3404 raise TypeError(f"Level type mismatch: {lab}") from err
3405 if not is_integer(loc):
3406 # non-comparable level, e.g. test_groupby_example
3407 raise TypeError(f"Level type mismatch: {lab}")
3408 if side == "right" and loc >= 0:
3409 loc -= 1
3410 return start + algos.searchsorted(section, loc, side=side)
3411
3412 idx = self._get_loc_single_level_index(lev, lab)
3413 if isinstance(idx, slice) and k < n - 1:
3414 # Get start and end value from slice, necessary when a non-integer
3415 # interval is given as input GH#37707
3416 start = idx.start
3417 end = idx.stop
3418 elif k < n - 1:
3419 # error: Incompatible types in assignment (expression has type
3420 # "Union[ndarray[Any, dtype[signedinteger[Any]]]
3421 end = start + algos.searchsorted( # type: ignore[assignment]
3422 section, idx, side="right"
3423 )
3424 # error: Incompatible types in assignment (expression has type
3425 # "Union[ndarray[Any, dtype[signedinteger[Any]]]
3426 start = start + algos.searchsorted( # type: ignore[assignment]
3427 section, idx, side="left"
3428 )
3429 elif isinstance(idx, slice):
3430 idx = idx.start
3431 return start + algos.searchsorted(section, idx, side=side)
3432 else:
3433 return start + algos.searchsorted(section, idx, side=side)
3434
3435 def _get_loc_single_level_index(self, level_index: Index, key: Hashable) -> int:
3436 """
3437 If key is NA value, location of index unify as -1.
3438
3439 Parameters
3440 ----------
3441 level_index: Index
3442 key : label
3443
3444 Returns
3445 -------
3446 loc : int
3447 If key is NA value, loc is -1
3448 Else, location of key in index.
3449
3450 See Also
3451 --------
3452 Index.get_loc : The get_loc method for (single-level) index.
3453 """
3454 if is_scalar(key) and isna(key):
3455 # TODO: need is_valid_na_for_dtype(key, level_index.dtype)
3456 return -1
3457 else:
3458 return level_index.get_loc(key)
3459
3460 def get_loc(self, key):
3461 """
3462 Get location for a label or a tuple of labels. The location is returned \
3463 as an integer/slice or boolean mask.
3464
3465 This method returns the integer location, slice object, or boolean mask
3466 corresponding to the specified key, which can be a single label or a tuple
3467 of labels. The key represents a position in the MultiIndex, and the location
3468 indicates where the key is found within the index.
3469
3470 Parameters
3471 ----------
3472 key : label or tuple of labels (one for each level)
3473 A label or tuple of labels that correspond to the levels of the MultiIndex.
3474 The key must match the structure of the MultiIndex.
3475
3476 Returns
3477 -------
3478 int, slice object or boolean mask
3479 If the key is past the lexsort depth, the return may be a
3480 boolean mask array, otherwise it is always a slice or int.
3481
3482 See Also
3483 --------
3484 Index.get_loc : The get_loc method for (single-level) index.
3485 MultiIndex.slice_locs : Get slice location given start label(s) and
3486 end label(s).
3487 MultiIndex.get_locs : Get location for a label/slice/list/mask or a
3488 sequence of such.
3489
3490 Notes
3491 -----
3492 The key cannot be a slice, list of same-level labels, a boolean mask,
3493 or a sequence of such. If you want to use those, use
3494 :meth:`MultiIndex.get_locs` instead.
3495
3496 Examples
3497 --------
3498 >>> mi = pd.MultiIndex.from_arrays([list("abb"), list("def")])
3499
3500 >>> mi.get_loc("b")
3501 slice(1, 3, None)
3502
3503 >>> mi.get_loc(("b", "e"))
3504 1
3505 """
3506 self._check_indexing_error(key)
3507
3508 def _maybe_to_slice(loc):
3509 """convert integer indexer to boolean mask or slice if possible"""
3510 if not isinstance(loc, np.ndarray) or loc.dtype != np.intp:
3511 return loc
3512
3513 loc = lib.maybe_indices_to_slice(loc, len(self))
3514 if isinstance(loc, slice):
3515 return loc
3516
3517 mask = np.empty(len(self), dtype="bool")
3518 mask.fill(False)
3519 mask[loc] = True
3520 return mask
3521
3522 if not isinstance(key, tuple):
3523 loc = self._get_level_indexer(key, level=0)
3524 return _maybe_to_slice(loc)
3525
3526 keylen = len(key)
3527 if self.nlevels < keylen:
3528 raise KeyError(
3529 f"Key length ({keylen}) exceeds index depth ({self.nlevels})"
3530 )
3531
3532 if keylen == self.nlevels and self.is_unique:
3533 # TODO: what if we have an IntervalIndex level?
3534 # i.e. do we need _index_as_unique on that level?
3535 try:
3536 return self._engine.get_loc(key)
3537 except KeyError as err:
3538 raise KeyError(key) from err
3539 except TypeError:
3540 # e.g. test_partial_slicing_with_multiindex partial string slicing
3541 loc, _ = self.get_loc_level(key, range(self.nlevels))
3542 return loc
3543
3544 # -- partial selection or non-unique index
3545 # break the key into 2 parts based on the lexsort_depth of the index;
3546 # the first part returns a continuous slice of the index; the 2nd part
3547 # needs linear search within the slice
3548 i = self._lexsort_depth
3549 lead_key, follow_key = key[:i], key[i:]
3550
3551 if not lead_key:
3552 start = 0
3553 stop = len(self)
3554 else:
3555 try:
3556 start, stop = self.slice_locs(lead_key, lead_key)
3557 except TypeError as err:
3558 # e.g. test_groupby_example key = ((0, 0, 1, 2), "new_col")
3559 # when self has 5 integer levels
3560 raise KeyError(key) from err
3561
3562 if start == stop:
3563 raise KeyError(key)
3564
3565 if not follow_key:
3566 return slice(start, stop)
3567
3568 if get_option("performance_warnings"):
3569 warnings.warn(
3570 "indexing past lexsort depth may impact performance.",
3571 PerformanceWarning,
3572 stacklevel=find_stack_level(),
3573 )
3574
3575 loc = np.arange(start, stop, dtype=np.intp)
3576
3577 for i, k in enumerate(follow_key, len(lead_key)):
3578 mask = self.codes[i][loc] == self._get_loc_single_level_index(
3579 self.levels[i], k
3580 )
3581 if not mask.all():
3582 loc = loc[mask]
3583 if not len(loc):
3584 raise KeyError(key)
3585
3586 return _maybe_to_slice(loc) if len(loc) != stop - start else slice(start, stop)
3587
3588 def get_loc_level(self, key, level: IndexLabel = 0, drop_level: bool = True):
3589 """
3590 Get location and sliced index for requested label(s)/level(s).
3591
3592 The `get_loc_level` method is a more advanced form of `get_loc`, allowing
3593 users to specify not just a label or sequence of labels, but also the level(s)
3594 in which to search. This method is useful when you need to isolate particular
3595 sections of a MultiIndex, either for further analysis or for slicing and
3596 dicing the data. The method provides flexibility in terms of maintaining
3597 or dropping levels from the resulting index based on the `drop_level`
3598 parameter.
3599
3600 Parameters
3601 ----------
3602 key : label or sequence of labels
3603 The label(s) for which to get the location.
3604 level : int/level name or list thereof, optional
3605 The level(s) in the MultiIndex to consider. If not provided, defaults
3606 to the first level.
3607 drop_level : bool, default True
3608 If ``False``, the resulting index will not drop any level.
3609
3610 Returns
3611 -------
3612 tuple
3613 A 2-tuple where the elements :
3614
3615 Element 0: int, slice object or boolean array.
3616
3617 Element 1: The resulting sliced multiindex/index. If the key
3618 contains all levels, this will be ``None``.
3619
3620 See Also
3621 --------
3622 MultiIndex.get_loc : Get location for a label or a tuple of labels.
3623 MultiIndex.get_locs : Get location for a label/slice/list/mask or a
3624 sequence of such.
3625
3626 Examples
3627 --------
3628 >>> mi = pd.MultiIndex.from_arrays([list("abb"), list("def")], names=["A", "B"])
3629
3630 >>> mi.get_loc_level("b")
3631 (slice(1, 3, None), Index(['e', 'f'], dtype='str', name='B'))
3632
3633 >>> mi.get_loc_level("e", level="B")
3634 (array([False, True, False]), Index(['b'], dtype='str', name='A'))
3635
3636 >>> mi.get_loc_level(["b", "e"])
3637 (1, None)
3638 """
3639 if not isinstance(level, (range, list, tuple)):
3640 level = self._get_level_number(level)
3641 else:
3642 level = [self._get_level_number(lev) for lev in level]
3643
3644 loc, mi = self._get_loc_level(key, level=level)
3645 if not drop_level:
3646 if lib.is_integer(loc):
3647 # Slice index must be an integer or None
3648 mi = self[loc : loc + 1]
3649 else:
3650 mi = self[loc]
3651 return loc, mi
3652
3653 def _get_loc_level(self, key, level: int | list[int] = 0):
3654 """
3655 get_loc_level but with `level` known to be positional, not name-based.
3656 """
3657
3658 # different name to distinguish from maybe_droplevels
3659 def maybe_mi_droplevels(indexer, levels):
3660 """
3661 If level does not exist or all levels were dropped, the exception
3662 has to be handled outside.
3663 """
3664 new_index = self[indexer]
3665
3666 for i in sorted(levels, reverse=True):
3667 new_index = new_index._drop_level_numbers([i])
3668
3669 return new_index
3670
3671 if isinstance(level, (tuple, list)):
3672 if len(key) != len(level):
3673 raise AssertionError(
3674 "Key for location must have same length as number of levels"
3675 )
3676 result = None
3677 for lev, k in zip(level, key, strict=True):
3678 loc, new_index = self._get_loc_level(k, level=lev)
3679 if isinstance(loc, slice):
3680 mask = np.zeros(len(self), dtype=bool)
3681 mask[loc] = True
3682 loc = mask
3683 result = loc if result is None else result & loc
3684
3685 try:
3686 # FIXME: we should be only dropping levels on which we are
3687 # scalar-indexing
3688 mi = maybe_mi_droplevels(result, level)
3689 except ValueError:
3690 # droplevel failed because we tried to drop all levels,
3691 # i.e. len(level) == self.nlevels
3692 mi = self[result]
3693
3694 return result, mi
3695
3696 # kludge for #1796
3697 if isinstance(key, list):
3698 key = tuple(key)
3699
3700 if isinstance(key, tuple) and level == 0:
3701 try:
3702 # Check if this tuple is a single key in our first level
3703 if key in self.levels[0]:
3704 indexer = self._get_level_indexer(key, level=level)
3705 new_index = maybe_mi_droplevels(indexer, [0])
3706 return indexer, new_index
3707 except (TypeError, InvalidIndexError):
3708 pass
3709
3710 if not any(isinstance(k, slice) for k in key):
3711 if len(key) == self.nlevels and self.is_unique:
3712 # Complete key in unique index -> standard get_loc
3713 try:
3714 return (self._engine.get_loc(key), None)
3715 except KeyError as err:
3716 raise KeyError(key) from err
3717 except TypeError:
3718 # e.g. partial string indexing
3719 # test_partial_string_timestamp_multiindex
3720 pass
3721
3722 # partial selection
3723 indexer = self.get_loc(key)
3724 ilevels = [i for i in range(len(key)) if key[i] != slice(None, None)]
3725 if len(ilevels) == self.nlevels:
3726 if is_integer(indexer):
3727 # we are dropping all levels
3728 return indexer, None
3729
3730 # TODO: in some cases we still need to drop some levels,
3731 # e.g. test_multiindex_perf_warn
3732 # test_partial_string_timestamp_multiindex
3733 ilevels = [
3734 i
3735 for i in range(len(key))
3736 if (
3737 not isinstance(key[i], str)
3738 or not self.levels[i]._supports_partial_string_indexing
3739 )
3740 and key[i] != slice(None, None)
3741 ]
3742 if len(ilevels) == self.nlevels:
3743 # TODO: why?
3744 ilevels = []
3745 return indexer, maybe_mi_droplevels(indexer, ilevels)
3746
3747 else:
3748 indexer = None
3749 for i, k in enumerate(key):
3750 if not isinstance(k, slice):
3751 loc_level = self._get_level_indexer(k, level=i)
3752 if isinstance(loc_level, slice):
3753 if com.is_null_slice(loc_level) or com.is_full_slice(
3754 loc_level, len(self)
3755 ):
3756 # everything
3757 continue
3758
3759 # e.g. test_xs_IndexSlice_argument_not_implemented
3760 k_index = np.zeros(len(self), dtype=bool)
3761 k_index[loc_level] = True
3762
3763 else:
3764 k_index = loc_level
3765
3766 elif com.is_null_slice(k):
3767 # taking everything, does not affect `indexer` below
3768 continue
3769
3770 else:
3771 # FIXME: this message can be inaccurate, e.g.
3772 # test_series_varied_multiindex_alignment
3773 raise TypeError(f"Expected label or tuple of labels, got {key}")
3774
3775 if indexer is None:
3776 indexer = k_index
3777 else:
3778 indexer &= k_index
3779 if indexer is None:
3780 indexer = slice(None, None)
3781 ilevels = [i for i in range(len(key)) if key[i] != slice(None, None)]
3782 return indexer, maybe_mi_droplevels(indexer, ilevels)
3783 else:
3784 indexer = self._get_level_indexer(key, level=level)
3785 if (
3786 isinstance(key, str)
3787 and self.levels[level]._supports_partial_string_indexing
3788 ):
3789 # check to see if we did an exact lookup vs sliced
3790 check = self.levels[level].get_loc(key)
3791 if not is_integer(check):
3792 # e.g. test_partial_string_timestamp_multiindex
3793 return indexer, self[indexer]
3794
3795 try:
3796 result_index = maybe_mi_droplevels(indexer, [level])
3797 except ValueError:
3798 result_index = self[indexer]
3799
3800 return indexer, result_index
3801
3802 def _get_level_indexer(
3803 self, key, level: int = 0, indexer: npt.NDArray[np.bool_] | None = None
3804 ):
3805 # `level` kwarg is _always_ positional, never name
3806 # return a boolean array or slice showing where the key is
3807 # in the totality of values
3808 # if the indexer is provided, then use this
3809
3810 level_index = self.levels[level]
3811 level_codes = self.codes[level]
3812
3813 def convert_indexer(start, stop, step, indexer=indexer, codes=level_codes):
3814 # Compute a bool indexer to identify the positions to take.
3815 # If we have an existing indexer, we only need to examine the
3816 # subset of positions where the existing indexer is True.
3817 if indexer is not None:
3818 # we only need to look at the subset of codes where the
3819 # existing indexer equals True
3820 codes = codes[indexer]
3821
3822 if step is None or step == 1:
3823 new_indexer = (codes >= start) & (codes < stop)
3824 else:
3825 r = np.arange(start, stop, step, dtype=codes.dtype)
3826 new_indexer = algos.isin(codes, r)
3827
3828 if indexer is None:
3829 return new_indexer
3830
3831 indexer = indexer.copy()
3832 indexer[indexer] = new_indexer
3833 return indexer
3834
3835 if isinstance(key, slice):
3836 # handle a slice, returning a slice if we can
3837 # otherwise a boolean indexer
3838 step = key.step
3839 is_negative_step = step is not None and step < 0
3840
3841 try:
3842 if key.start is not None:
3843 start = level_index.get_loc(key.start)
3844 elif is_negative_step:
3845 start = len(level_index) - 1
3846 else:
3847 start = 0
3848
3849 if key.stop is not None:
3850 stop = level_index.get_loc(key.stop)
3851 elif is_negative_step:
3852 stop = 0
3853 elif isinstance(start, slice):
3854 stop = len(level_index)
3855 else:
3856 stop = len(level_index) - 1
3857 except KeyError:
3858 # we have a partial slice (like looking up a partial date
3859 # string)
3860 start = stop = level_index.slice_indexer(key.start, key.stop, key.step)
3861 step = start.step
3862
3863 if isinstance(start, slice) or isinstance(stop, slice):
3864 # we have a slice for start and/or stop
3865 # a partial date slicer on a DatetimeIndex generates a slice
3866 # note that the stop ALREADY includes the stopped point (if
3867 # it was a string sliced)
3868 start = getattr(start, "start", start)
3869 stop = getattr(stop, "stop", stop)
3870 return convert_indexer(start, stop, step)
3871
3872 elif level > 0 or self._lexsort_depth == 0 or step is not None:
3873 # need to have like semantics here to right
3874 # searching as when we are using a slice
3875 # so adjust the stop by 1 (so we include stop)
3876 stop = (stop - 1) if is_negative_step else (stop + 1)
3877 return convert_indexer(start, stop, step)
3878 else:
3879 # sorted, so can return slice object -> view
3880 i = algos.searchsorted(level_codes, start, side="left")
3881 j = algos.searchsorted(level_codes, stop, side="right")
3882 return slice(i, j, step)
3883
3884 else:
3885 idx = self._get_loc_single_level_index(level_index, key)
3886
3887 if level > 0 or self._lexsort_depth == 0:
3888 # Desired level is not sorted
3889 if isinstance(idx, slice):
3890 # test_get_loc_partial_timestamp_multiindex
3891 locs = (level_codes >= idx.start) & (level_codes < idx.stop)
3892 return locs
3893
3894 locs = np.asarray(level_codes == idx, dtype=bool)
3895
3896 if not locs.any():
3897 # The label is present in self.levels[level] but unused:
3898 raise KeyError(key)
3899 return locs
3900
3901 if isinstance(idx, slice):
3902 # e.g. test_partial_string_timestamp_multiindex
3903 start = algos.searchsorted(level_codes, idx.start, side="left")
3904 # NB: "left" here bc of slice semantics
3905 end = algos.searchsorted(level_codes, idx.stop, side="left")
3906 else:
3907 start = algos.searchsorted(level_codes, idx, side="left")
3908 end = algos.searchsorted(level_codes, idx, side="right")
3909
3910 if start == end:
3911 # The label is present in self.levels[level] but unused:
3912 raise KeyError(key)
3913 return slice(maybe_unbox_numpy_scalar(start), maybe_unbox_numpy_scalar(end))
3914
3915 def get_locs(self, seq) -> npt.NDArray[np.intp]:
3916 """
3917 Get location for a sequence of labels.
3918
3919 Parameters
3920 ----------
3921 seq : label, slice, list, mask or a sequence of such
3922 You should use one of the above for each level.
3923 If a level should not be used, set it to ``slice(None)``.
3924
3925 Returns
3926 -------
3927 numpy.ndarray
3928 NumPy array of integers suitable for passing to iloc.
3929
3930 See Also
3931 --------
3932 MultiIndex.get_loc : Get location for a label or a tuple of labels.
3933 MultiIndex.slice_locs : Get slice location given start label(s) and
3934 end label(s).
3935
3936 Examples
3937 --------
3938 >>> mi = pd.MultiIndex.from_arrays([list("abb"), list("def")])
3939
3940 >>> mi.get_locs("b") # doctest: +SKIP
3941 array([1, 2], dtype=int64)
3942
3943 >>> mi.get_locs([slice(None), ["e", "f"]]) # doctest: +SKIP
3944 array([1, 2], dtype=int64)
3945
3946 >>> mi.get_locs([[True, False, True], slice("e", "f")]) # doctest: +SKIP
3947 array([2], dtype=int64)
3948 """
3949
3950 # must be lexsorted to at least as many levels
3951 true_slices = [i for (i, s) in enumerate(com.is_true_slices(seq)) if s]
3952 if true_slices and true_slices[-1] >= self._lexsort_depth:
3953 raise UnsortedIndexError(
3954 "MultiIndex slicing requires the index to be lexsorted: slicing "
3955 f"on levels {true_slices}, lexsort depth {self._lexsort_depth}"
3956 )
3957
3958 if any(x is Ellipsis for x in seq):
3959 raise NotImplementedError(
3960 "MultiIndex does not support indexing with Ellipsis"
3961 )
3962
3963 n = len(self)
3964
3965 def _to_bool_indexer(indexer) -> npt.NDArray[np.bool_]:
3966 if isinstance(indexer, slice):
3967 new_indexer = np.zeros(n, dtype=np.bool_)
3968 new_indexer[indexer] = True
3969 return new_indexer
3970 return indexer
3971
3972 # a bool indexer for the positions we want to take
3973 indexer: npt.NDArray[np.bool_] | None = None
3974
3975 for i, k in enumerate(seq):
3976 lvl_indexer: npt.NDArray[np.bool_] | slice | None = None
3977
3978 if com.is_bool_indexer(k):
3979 if len(k) != n:
3980 raise ValueError(
3981 "cannot index with a boolean indexer that "
3982 "is not the same length as the index"
3983 )
3984 if isinstance(k, (ABCSeries, Index)):
3985 k = k._values
3986 lvl_indexer = np.asarray(k)
3987 if indexer is None:
3988 lvl_indexer = lvl_indexer.copy()
3989
3990 elif is_list_like(k):
3991 # a collection of labels to include from this level (these are or'd)
3992
3993 # GH#27591 check if this is a single tuple key in the level
3994 try:
3995 lvl_indexer = self._get_level_indexer(k, level=i, indexer=indexer)
3996 except (InvalidIndexError, TypeError, KeyError) as err:
3997 # InvalidIndexError e.g. non-hashable, fall back to treating
3998 # this as a sequence of labels
3999 # KeyError it can be ambiguous if this is a label or sequence
4000 # of labels
4001 # github.com/pandas-dev/pandas/issues/39424#issuecomment-871626708
4002 for x in k:
4003 if not is_hashable(x):
4004 # e.g. slice
4005 raise err
4006 # GH 39424: Ignore not founds
4007 # GH 42351: No longer ignore not founds & enforced in 2.0
4008 # TODO: how to handle IntervalIndex level? (no test cases)
4009 item_indexer = self._get_level_indexer(
4010 x, level=i, indexer=indexer
4011 )
4012 if lvl_indexer is None:
4013 lvl_indexer = _to_bool_indexer(item_indexer)
4014 elif isinstance(item_indexer, slice):
4015 lvl_indexer[item_indexer] = True # type: ignore[index]
4016 else:
4017 lvl_indexer |= item_indexer
4018
4019 if lvl_indexer is None:
4020 # no matches we are done
4021 # test_loc_getitem_duplicates_multiindex_empty_indexer
4022 return np.array([], dtype=np.intp)
4023
4024 elif com.is_null_slice(k):
4025 # empty slice
4026 if indexer is None and i == len(seq) - 1:
4027 return np.arange(n, dtype=np.intp)
4028 continue
4029
4030 else:
4031 # a slice or a single label
4032 lvl_indexer = self._get_level_indexer(k, level=i, indexer=indexer)
4033
4034 # update indexer
4035 lvl_indexer = _to_bool_indexer(lvl_indexer)
4036 if indexer is None:
4037 indexer = lvl_indexer
4038 else:
4039 indexer &= lvl_indexer
4040 if not np.any(indexer) and np.any(lvl_indexer):
4041 raise KeyError(seq)
4042
4043 # empty indexer
4044 if indexer is None:
4045 return np.array([], dtype=np.intp)
4046
4047 pos_indexer = indexer.nonzero()[0]
4048 return self._reorder_indexer(seq, pos_indexer)
4049
4050 # --------------------------------------------------------------------
4051
4052 def _reorder_indexer(
4053 self,
4054 seq: tuple[Scalar | Iterable | AnyArrayLike, ...],
4055 indexer: npt.NDArray[np.intp],
4056 ) -> npt.NDArray[np.intp]:
4057 """
4058 Reorder an indexer of a MultiIndex (self) so that the labels are in the
4059 same order as given in seq
4060
4061 Parameters
4062 ----------
4063 seq : label/slice/list/mask or a sequence of such
4064 indexer: a position indexer of self
4065
4066 Returns
4067 -------
4068 indexer : a sorted position indexer of self ordered as seq
4069 """
4070
4071 # check if sorting is necessary
4072 need_sort = False
4073 for i, k in enumerate(seq):
4074 if com.is_null_slice(k) or com.is_bool_indexer(k) or is_scalar(k):
4075 pass
4076 elif is_list_like(k):
4077 if len(k) <= 1: # type: ignore[arg-type]
4078 pass
4079 elif self._is_lexsorted():
4080 # If the index is lexsorted and the list_like label
4081 # in seq are sorted then we do not need to sort
4082 k_codes = self.levels[i].get_indexer(k)
4083 k_codes = k_codes[k_codes >= 0] # Filter absent keys
4084 # True if the given codes are not ordered
4085 need_sort = (k_codes[:-1] > k_codes[1:]).any()
4086 else:
4087 need_sort = True
4088 elif isinstance(k, slice):
4089 if self._is_lexsorted():
4090 need_sort = k.step is not None and k.step < 0
4091 else:
4092 need_sort = True
4093 else:
4094 need_sort = True
4095 if need_sort:
4096 break
4097 if not need_sort:
4098 return indexer
4099
4100 n = len(self)
4101 keys: tuple[np.ndarray, ...] = ()
4102 # For each level of the sequence in seq, map the level codes with the
4103 # order they appears in a list-like sequence
4104 # This mapping is then use to reorder the indexer
4105 for i, k in enumerate(seq):
4106 if is_scalar(k):
4107 # GH#34603 we want to treat a scalar the same as an all equal list
4108 k = [k]
4109 if com.is_bool_indexer(k):
4110 new_order = np.arange(n)[indexer]
4111 elif is_list_like(k):
4112 # Generate a map with all level codes as sorted initially
4113 if not isinstance(k, (np.ndarray, ExtensionArray, Index, ABCSeries)):
4114 k = sanitize_array(k, None)
4115 k = algos.unique(k)
4116 key_order_map = np.ones(len(self.levels[i]), dtype=np.uint64) * len(
4117 self.levels[i]
4118 )
4119 # Set order as given in the indexer list
4120 level_indexer = self.levels[i].get_indexer(k)
4121 level_indexer = level_indexer[level_indexer >= 0] # Filter absent keys
4122 key_order_map[level_indexer] = np.arange(len(level_indexer))
4123
4124 new_order = key_order_map[self.codes[i][indexer]]
4125 elif isinstance(k, slice) and k.step is not None and k.step < 0:
4126 # flip order for negative step
4127 new_order = np.arange(n - 1, -1, -1)[indexer]
4128 elif isinstance(k, slice) and k.start is None and k.stop is None:
4129 # slice(None) should not determine order GH#31330
4130 new_order = np.ones((n,), dtype=np.intp)[indexer]
4131 else:
4132 # For all other case, use the same order as the level
4133 new_order = np.arange(n)[indexer]
4134 keys = (new_order, *keys)
4135
4136 # Find the reordering using lexsort on the keys mapping
4137 ind = np.lexsort(keys)
4138 return indexer[ind]
4139
4140 def truncate(self, before=None, after=None) -> MultiIndex:
4141 """
4142 Slice index between two labels / tuples, return new MultiIndex.
4143
4144 Parameters
4145 ----------
4146 before : label or tuple, can be partial. Default None
4147 None defaults to start.
4148 after : label or tuple, can be partial. Default None
4149 None defaults to end.
4150
4151 Returns
4152 -------
4153 MultiIndex
4154 The truncated MultiIndex.
4155
4156 See Also
4157 --------
4158 DataFrame.truncate : Truncate a DataFrame before and after some index values.
4159 Series.truncate : Truncate a Series before and after some index values.
4160
4161 Examples
4162 --------
4163 >>> mi = pd.MultiIndex.from_arrays([["a", "b", "c"], ["x", "y", "z"]])
4164 >>> mi
4165 MultiIndex([('a', 'x'), ('b', 'y'), ('c', 'z')],
4166 )
4167 >>> mi.truncate(before="a", after="b")
4168 MultiIndex([('a', 'x'), ('b', 'y')],
4169 )
4170 """
4171 if after and before and after < before:
4172 raise ValueError("after < before")
4173
4174 i, j = self.levels[0].slice_locs(before, after)
4175 left, right = self.slice_locs(before, after)
4176
4177 new_levels = list(self.levels)
4178 new_levels[0] = new_levels[0][i:j]
4179
4180 new_codes = [level_codes[left:right] for level_codes in self.codes]
4181 new_codes[0] = new_codes[0] - i
4182
4183 return MultiIndex(
4184 levels=new_levels,
4185 codes=new_codes,
4186 names=self._names,
4187 verify_integrity=False,
4188 )
4189
4190 def equals(self, other: object) -> bool:
4191 """
4192 Determines if two MultiIndex objects have the same labeling information
4193 (the levels themselves do not necessarily have to be the same)
4194
4195 See Also
4196 --------
4197 equal_levels
4198 """
4199 if self.is_(other):
4200 return True
4201
4202 if not isinstance(other, Index):
4203 return False
4204
4205 if len(self) != len(other):
4206 return False
4207
4208 if not isinstance(other, MultiIndex):
4209 # d-level MultiIndex can equal d-tuple Index
4210 if not self._should_compare(other):
4211 # object Index or Categorical[object] may contain tuples
4212 return False
4213 return array_equivalent(self._values, other._values)
4214
4215 if self.nlevels != other.nlevels:
4216 return False
4217
4218 for i in range(self.nlevels):
4219 self_codes = self.codes[i]
4220 other_codes = other.codes[i]
4221 self_mask = self_codes == -1
4222 other_mask = other_codes == -1
4223 if not np.array_equal(self_mask, other_mask):
4224 return False
4225 self_level = self.levels[i]
4226 other_level = other.levels[i]
4227 new_codes = recode_for_categories(
4228 other_codes, other_level, self_level, copy=False
4229 )
4230 if not np.array_equal(self_codes, new_codes):
4231 return False
4232 if not self_level[:0].equals(other_level[:0]):
4233 # e.g. Int64 != int64
4234 return False
4235 return True
4236
4237 def equal_levels(self, other: MultiIndex) -> bool:
4238 """
4239 Return True if the levels of both MultiIndex objects are the same
4240
4241 """
4242 if self.nlevels != other.nlevels:
4243 return False
4244
4245 for i in range(self.nlevels):
4246 if not self.levels[i].equals(other.levels[i]):
4247 return False
4248 return True
4249
4250 # --------------------------------------------------------------------
4251 # Set Methods
4252
4253 def _union(self, other, sort) -> MultiIndex:
4254 other, result_names = self._convert_can_do_setop(other)
4255 if other.has_duplicates:
4256 # This is only necessary if other has dupes,
4257 # otherwise difference is faster
4258 result = super(MultiIndex, self.rename(result_names))._union(
4259 other.rename(result_names), sort
4260 )
4261
4262 if isinstance(result, MultiIndex):
4263 return result
4264 return MultiIndex.from_arrays(
4265 zip(*result, strict=True), sortorder=None, names=result_names
4266 )
4267
4268 else:
4269 right_missing = other.difference(self, sort=False)
4270 if len(right_missing):
4271 result = self.append(right_missing)
4272 else:
4273 result = self._get_reconciled_name_object(other)
4274
4275 if sort is not False:
4276 try:
4277 result = result.sort_values()
4278 except TypeError:
4279 if sort is True:
4280 raise
4281 warnings.warn(
4282 "The values in the array are unorderable. "
4283 "Pass `sort=False` to suppress this warning.",
4284 RuntimeWarning,
4285 stacklevel=find_stack_level(),
4286 )
4287 return result
4288
4289 def _is_comparable_dtype(self, dtype: DtypeObj) -> bool:
4290 return is_object_dtype(dtype)
4291
4292 def _get_reconciled_name_object(self, other) -> MultiIndex:
4293 """
4294 If the result of a set operation will be self,
4295 return a shallow copy of self.
4296 """
4297 names = self._maybe_match_names(other)
4298 if self.names != names:
4299 return self.rename(names)
4300 return self.copy(deep=False)
4301
4302 def _maybe_match_names(self, other):
4303 """
4304 Try to find common names to attach to the result of an operation between
4305 a and b. Return a consensus list of names if they match at least partly
4306 or list of None if they have completely different names.
4307 """
4308 if len(self.names) != len(other.names):
4309 return [None] * len(self.names)
4310 names = []
4311 for a_name, b_name in zip(self.names, other.names, strict=True):
4312 if a_name == b_name:
4313 names.append(a_name)
4314 else:
4315 # TODO: what if they both have np.nan for their names?
4316 names.append(None)
4317 return names
4318
4319 def _wrap_intersection_result(self, other, result) -> MultiIndex:
4320 _, result_names = self._convert_can_do_setop(other)
4321 return result.set_names(result_names)
4322
4323 def _wrap_difference_result(self, other, result: MultiIndex) -> MultiIndex:
4324 _, result_names = self._convert_can_do_setop(other)
4325
4326 if len(result) == 0:
4327 return result.remove_unused_levels().set_names(result_names)
4328 else:
4329 return result.set_names(result_names)
4330
4331 def _convert_can_do_setop(self, other):
4332 result_names = self.names
4333
4334 if not isinstance(other, Index):
4335 if len(other) == 0:
4336 return self[:0], self.names
4337 else:
4338 msg = "other must be a MultiIndex or a list of tuples"
4339 try:
4340 other = MultiIndex.from_tuples(other, names=self.names)
4341 except (ValueError, TypeError) as err:
4342 # ValueError raised by tuples_to_object_array if we
4343 # have non-object dtype
4344 raise TypeError(msg) from err
4345 else:
4346 result_names = get_unanimous_names(self, other)
4347
4348 return other, result_names
4349
4350 # --------------------------------------------------------------------
4351
4352 def astype(self, dtype, copy: bool = True):
4353 """
4354 Create an MultiIndex with values cast to dtypes.
4355
4356 The class of a new Index is determined by dtype. When conversion is
4357 impossible, a TypeError exception is raised.
4358
4359 Parameters
4360 ----------
4361 dtype : numpy dtype or pandas type
4362 Note that any signed integer `dtype` is treated as ``'int64'``,
4363 and any unsigned integer `dtype` is treated as ``'uint64'``,
4364 regardless of the size.
4365 copy : bool, default True
4366 By default, astype always returns a newly allocated object.
4367 If copy is set to False and internal requirements on dtype are
4368 satisfied, the original data is used to create a new Index
4369 or the original Index is returned.
4370
4371 Returns
4372 -------
4373 MultiIndex
4374 MultiIndex with values cast to specified dtype.
4375
4376 See Also
4377 --------
4378 Index.dtype: Return the dtype object of the underlying data.
4379 Index.dtypes: Return the dtype object of the underlying data.
4380 Index.convert_dtypes: Convert columns to the best possible dtypes.
4381
4382 Examples
4383 --------
4384 >>> mi = pd.MultiIndex.from_arrays(([1, 2, 3], [4, 5, 6]))
4385 >>> mi
4386 MultiIndex([(1, 4),
4387 (2, 5),
4388 (3, 6)],
4389 )
4390 >>> mi.astype("object")
4391 MultiIndex([(1, 4),
4392 (2, 5),
4393 (3, 6)],
4394 )
4395 """
4396 dtype = pandas_dtype(dtype)
4397 if isinstance(dtype, CategoricalDtype):
4398 msg = "> 1 ndim Categorical are not supported at this time"
4399 raise NotImplementedError(msg)
4400 if not is_object_dtype(dtype):
4401 raise TypeError(
4402 "Setting a MultiIndex dtype to anything other than object "
4403 "is not supported"
4404 )
4405 if copy is True:
4406 return self._view()
4407 return self
4408
4409 def _validate_fill_value(self, item):
4410 if isinstance(item, MultiIndex):
4411 # GH#43212
4412 if item.nlevels != self.nlevels:
4413 raise ValueError("Item must have length equal to number of levels.")
4414 return item._values
4415 elif not isinstance(item, tuple):
4416 # Pad the key with empty strings if lower levels of the key
4417 # aren't specified:
4418 item = (item,) + ("",) * (self.nlevels - 1)
4419 elif len(item) != self.nlevels:
4420 raise ValueError("Item must have length equal to number of levels.")
4421 return item
4422
4423 def putmask(self, mask, value: MultiIndex) -> MultiIndex:
4424 """
4425 Return a new MultiIndex of the values set with the mask.
4426
4427 Parameters
4428 ----------
4429 mask : array like
4430 value : MultiIndex
4431 Must either be the same length as self or length one
4432
4433 Returns
4434 -------
4435 MultiIndex
4436 """
4437 mask, noop = validate_putmask(self, mask)
4438 if noop:
4439 return self.copy()
4440
4441 if len(mask) == len(value):
4442 subset = value[mask].remove_unused_levels()
4443 else:
4444 subset = value.remove_unused_levels()
4445
4446 new_levels = []
4447 new_codes = []
4448
4449 for i, (value_level, level, level_codes) in enumerate(
4450 zip(subset.levels, self.levels, self.codes, strict=True)
4451 ):
4452 new_level = level.union(value_level, sort=False)
4453 value_codes = new_level.get_indexer_for(subset.get_level_values(i))
4454 new_code = ensure_int64(level_codes)
4455 new_code[mask] = value_codes
4456 new_levels.append(new_level)
4457 new_codes.append(new_code)
4458
4459 return MultiIndex(
4460 levels=new_levels, codes=new_codes, names=self.names, verify_integrity=False
4461 )
4462
4463 def insert(self, loc: int, item) -> MultiIndex:
4464 """
4465 Make new MultiIndex inserting new item at location
4466
4467 Parameters
4468 ----------
4469 loc : int
4470 item : tuple
4471 Must be same length as number of levels in the MultiIndex
4472
4473 Returns
4474 -------
4475 new_index : Index
4476 """
4477 item = self._validate_fill_value(item)
4478
4479 new_levels = []
4480 new_codes = []
4481 for k, level, level_codes in zip(item, self.levels, self.codes, strict=True):
4482 if k not in level:
4483 # have to insert into level
4484 # must insert at end otherwise you have to recompute all the
4485 # other codes
4486 lev_loc = len(level)
4487 level = level.insert(lev_loc, k)
4488 if isna(level[lev_loc]): # GH 59003, 60388
4489 lev_loc = -1
4490 else:
4491 lev_loc = level.get_loc(k)
4492
4493 new_levels.append(level)
4494 new_codes.append(np.insert(ensure_int64(level_codes), loc, lev_loc))
4495
4496 return MultiIndex(
4497 levels=new_levels, codes=new_codes, names=self.names, verify_integrity=False
4498 )
4499
4500 def delete(self, loc) -> MultiIndex:
4501 """
4502 Make new index with passed location deleted
4503
4504 Returns
4505 -------
4506 new_index : MultiIndex
4507 """
4508 new_codes = [np.delete(level_codes, loc) for level_codes in self.codes]
4509 return MultiIndex(
4510 levels=self.levels,
4511 codes=new_codes,
4512 names=self.names,
4513 verify_integrity=False,
4514 )
4515
4516 def isin(self, values, level=None) -> npt.NDArray[np.bool_]:
4517 """
4518 Return a boolean array where the index values are in `values`.
4519
4520 Compute boolean array of whether each index value is found in the
4521 passed set of values. The length of the returned boolean array matches
4522 the length of the index.
4523
4524 Parameters
4525 ----------
4526 values : set or list-like
4527 Sought values.
4528 level : str or int, optional
4529 Name or position of the index level to use (if the index is a
4530 `MultiIndex`).
4531
4532 Returns
4533 -------
4534 np.ndarray[bool]
4535 NumPy array of boolean values.
4536
4537 See Also
4538 --------
4539 Series.isin : Same for Series.
4540 DataFrame.isin : Same method for DataFrames.
4541
4542 Notes
4543 -----
4544 In the case of `MultiIndex` you must either specify `values` as a
4545 list-like object containing tuples that are the same length as the
4546 number of levels, or specify `level`. Otherwise it will raise a
4547 ``ValueError``.
4548
4549 If `level` is specified:
4550
4551 - if it is the name of one *and only one* index level, use that level;
4552 - otherwise it should be a number indicating level position.
4553
4554 Examples
4555 --------
4556 >>> idx = pd.Index([1, 2, 3])
4557 >>> idx
4558 Index([1, 2, 3], dtype='int64')
4559
4560 Check whether each index value in a list of values.
4561
4562 >>> idx.isin([1, 4])
4563 array([ True, False, False])
4564
4565 >>> mi = pd.MultiIndex.from_arrays(
4566 ... [[1, 2, 3], ["red", "blue", "green"]], names=["number", "color"]
4567 ... )
4568 >>> mi
4569 MultiIndex([(1, 'red'),
4570 (2, 'blue'),
4571 (3, 'green')],
4572 names=['number', 'color'])
4573
4574 Check whether the strings in the 'color' level of the MultiIndex
4575 are in a list of colors.
4576
4577 >>> mi.isin(["red", "orange", "yellow"], level="color")
4578 array([ True, False, False])
4579
4580 To check across the levels of a MultiIndex, pass a list of tuples:
4581
4582 >>> mi.isin([(1, "red"), (3, "red")])
4583 array([ True, False, False])
4584 """
4585 if isinstance(values, Generator):
4586 values = list(values)
4587
4588 if level is None:
4589 if len(values) == 0:
4590 return np.zeros((len(self),), dtype=np.bool_)
4591 if not isinstance(values, MultiIndex):
4592 values = MultiIndex.from_tuples(values)
4593 return values.unique().get_indexer_for(self) != -1
4594 else:
4595 num = self._get_level_number(level)
4596 levs = self.get_level_values(num)
4597
4598 if levs.size == 0:
4599 return np.zeros(len(levs), dtype=np.bool_)
4600 return levs.isin(values)
4601
4602 # error: Incompatible types in assignment (expression has type overloaded function,
4603 # base class "Index" defined the type as "Callable[[Index, Any, bool], Any]")
4604 rename = Index.set_names # type: ignore[assignment]
4605
4606 # ---------------------------------------------------------------
4607 # Arithmetic/Numeric Methods - Disabled
4608
4609 __add__ = make_invalid_op("__add__")
4610 __radd__ = make_invalid_op("__radd__")
4611 __iadd__ = make_invalid_op("__iadd__")
4612 __sub__ = make_invalid_op("__sub__")
4613 __rsub__ = make_invalid_op("__rsub__")
4614 __isub__ = make_invalid_op("__isub__")
4615 __pow__ = make_invalid_op("__pow__")
4616 __rpow__ = make_invalid_op("__rpow__")
4617 __mul__ = make_invalid_op("__mul__")
4618 __rmul__ = make_invalid_op("__rmul__")
4619 __floordiv__ = make_invalid_op("__floordiv__")
4620 __rfloordiv__ = make_invalid_op("__rfloordiv__")
4621 __truediv__ = make_invalid_op("__truediv__")
4622 __rtruediv__ = make_invalid_op("__rtruediv__")
4623 __mod__ = make_invalid_op("__mod__")
4624 __rmod__ = make_invalid_op("__rmod__")
4625 __divmod__ = make_invalid_op("__divmod__")
4626 __rdivmod__ = make_invalid_op("__rdivmod__")
4627 # Unary methods disabled
4628 __neg__ = make_invalid_op("__neg__")
4629 __pos__ = make_invalid_op("__pos__")
4630 __abs__ = make_invalid_op("__abs__")
4631 __invert__ = make_invalid_op("__invert__")
4632
4633
4634def _lexsort_depth(codes: list[np.ndarray], nlevels: int) -> int:
4635 """Count depth (up to a maximum of `nlevels`) with which codes are lexsorted."""
4636 int64_codes = [ensure_int64(level_codes) for level_codes in codes]
4637 for k in range(nlevels, 0, -1):
4638 if libalgos.is_lexsorted(int64_codes[:k]):
4639 return k
4640 return 0
4641
4642
4643def sparsify_labels(label_list, start: int = 0, sentinel: object = ""):
4644 pivoted = list(zip(*label_list, strict=True))
4645 k = len(label_list)
4646
4647 result = pivoted[: start + 1]
4648 prev = pivoted[start]
4649
4650 for cur in pivoted[start + 1 :]:
4651 sparse_cur = []
4652
4653 for i, (p, t) in enumerate(zip(prev, cur, strict=True)):
4654 if i == k - 1:
4655 sparse_cur.append(t)
4656 result.append(sparse_cur) # type: ignore[arg-type]
4657 break
4658
4659 if p == t:
4660 sparse_cur.append(sentinel)
4661 else:
4662 sparse_cur.extend(cur[i:])
4663 result.append(sparse_cur) # type: ignore[arg-type]
4664 break
4665
4666 prev = cur
4667
4668 return list(zip(*result, strict=True))
4669
4670
4671def _get_na_rep(dtype: DtypeObj) -> str:
4672 if isinstance(dtype, ExtensionDtype):
4673 return f"{dtype.na_value}"
4674 else:
4675 dtype_type = dtype.type
4676
4677 return {np.datetime64: "NaT", np.timedelta64: "NaT"}.get(dtype_type, "NaN")
4678
4679
4680def maybe_droplevels(index: Index, key) -> Index:
4681 """
4682 Attempt to drop level or levels from the given index.
4683
4684 Parameters
4685 ----------
4686 index: Index
4687 key : scalar or tuple
4688
4689 Returns
4690 -------
4691 Index
4692 """
4693 # drop levels
4694 original_index = index
4695 if isinstance(key, tuple):
4696 # Caller is responsible for ensuring the key is not an entry in the first
4697 # level of the MultiIndex.
4698 for _ in key:
4699 try:
4700 index = index._drop_level_numbers([0])
4701 except ValueError:
4702 # we have dropped too much, so back out
4703 return original_index
4704 else:
4705 try:
4706 index = index._drop_level_numbers([0])
4707 except ValueError:
4708 pass
4709
4710 return index
4711
4712
4713def _coerce_indexer_frozen(array_like, categories, copy: bool = False) -> np.ndarray:
4714 """
4715 Coerce the array-like indexer to the smallest integer dtype that can encode all
4716 of the given categories.
4717
4718 Parameters
4719 ----------
4720 array_like : array-like
4721 categories : array-like
4722 copy : bool
4723
4724 Returns
4725 -------
4726 np.ndarray
4727 Non-writeable.
4728 """
4729 array_like = coerce_indexer_dtype(array_like, categories)
4730 if copy:
4731 array_like = array_like.copy()
4732 array_like.flags.writeable = False
4733 return array_like
4734
4735
4736def _require_listlike(level, arr, arrname: str):
4737 """
4738 Ensure that level is either None or listlike, and arr is list-of-listlike.
4739 """
4740 if level is not None and not is_list_like(level):
4741 if not is_list_like(arr):
4742 raise TypeError(f"{arrname} must be list-like")
4743 if len(arr) > 0 and is_list_like(arr[0]):
4744 raise TypeError(f"{arrname} must be list-like")
4745 level = [level]
4746 arr = [arr]
4747 elif level is None or is_list_like(level):
4748 if not is_list_like(arr) or not is_list_like(arr[0]):
4749 raise TypeError(f"{arrname} must be list of lists-like")
4750 return level, arr
4751
4752
4753def cartesian_product(X: list[np.ndarray]) -> list[np.ndarray]:
4754 """
4755 Numpy version of itertools.product.
4756 Sometimes faster (for large inputs)...
4757
4758 Parameters
4759 ----------
4760 X : list-like of list-likes
4761
4762 Returns
4763 -------
4764 product : list of ndarrays
4765
4766 Examples
4767 --------
4768 >>> cartesian_product([list("ABC"), [1, 2]])
4769 [array(['A', 'A', 'B', 'B', 'C', 'C'], dtype='<U1'), array([1, 2, 1, 2, 1, 2])]
4770
4771 See Also
4772 --------
4773 itertools.product : Cartesian product of input iterables. Equivalent to
4774 nested for-loops.
4775 """
4776 msg = "Input must be a list-like of list-likes"
4777 if not is_list_like(X):
4778 raise TypeError(msg)
4779 for x in X:
4780 if not is_list_like(x):
4781 raise TypeError(msg)
4782
4783 if len(X) == 0:
4784 return []
4785
4786 lenX = np.fromiter((len(x) for x in X), dtype=np.intp)
4787 cumprodX = np.cumprod(lenX)
4788
4789 if np.any(cumprodX < 0):
4790 raise ValueError("Product space too large to allocate arrays!")
4791
4792 a = np.roll(cumprodX, 1)
4793 a[0] = 1
4794
4795 if cumprodX[-1] != 0:
4796 b = cumprodX[-1] / cumprodX
4797 else:
4798 # if any factor is empty, the cartesian product is empty
4799 b = np.zeros_like(cumprodX)
4800
4801 return [
4802 np.tile(
4803 np.repeat(x, b[i]),
4804 np.prod(a[i]),
4805 )
4806 for i, x in enumerate(X)
4807 ]