Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/indexes/multi.py: 14%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1548 statements  

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 ]