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