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

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

1423 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 Self, 

19 cast, 

20) 

21import warnings 

22 

23import numpy as np 

24 

25from pandas._config import get_option 

26 

27from pandas._libs import ( 

28 algos as libalgos, 

29 index as libindex, 

30 lib, 

31) 

32from pandas._libs.hashtable import duplicated 

33from pandas._typing import ( 

34 AnyAll, 

35 AnyArrayLike, 

36 Axis, 

37 DropKeep, 

38 DtypeObj, 

39 F, 

40 IgnoreRaise, 

41 IndexLabel, 

42 IndexT, 

43 NaPosition, 

44 Scalar, 

45 Shape, 

46 npt, 

47) 

48from pandas.compat.numpy import function as nv 

49from pandas.errors import ( 

50 InvalidIndexError, 

51 PerformanceWarning, 

52 UnsortedIndexError, 

53) 

54from pandas.util._decorators import ( 

55 cache_readonly, 

56 set_module, 

57) 

58from pandas.util._exceptions import find_stack_level 

59 

60from pandas.core.dtypes.cast import ( 

61 coerce_indexer_dtype, 

62 maybe_unbox_numpy_scalar, 

63) 

64from pandas.core.dtypes.common import ( 

65 ensure_int64, 

66 ensure_platform_int, 

67 is_hashable, 

68 is_integer, 

69 is_iterator, 

70 is_list_like, 

71 is_object_dtype, 

72 is_scalar, 

73 is_string_dtype, 

74 pandas_dtype, 

75) 

76from pandas.core.dtypes.dtypes import ( 

77 CategoricalDtype, 

78 ExtensionDtype, 

79) 

80from pandas.core.dtypes.generic import ( 

81 ABCDataFrame, 

82 ABCSeries, 

83) 

84from pandas.core.dtypes.inference import is_array_like 

85from pandas.core.dtypes.missing import ( 

86 array_equivalent, 

87 isna, 

88) 

89 

90import pandas.core.algorithms as algos 

91from pandas.core.array_algos.putmask import validate_putmask 

92from pandas.core.arrays import ( 

93 Categorical, 

94 ExtensionArray, 

95) 

96from pandas.core.arrays.categorical import ( 

97 factorize_from_iterables, 

98 recode_for_categories, 

99) 

100import pandas.core.common as com 

101from pandas.core.construction import sanitize_array 

102import pandas.core.indexes.base as ibase 

103from pandas.core.indexes.base import ( 

104 Index, 

105 ensure_index, 

106 get_unanimous_names, 

107) 

108from pandas.core.indexes.frozen import FrozenList 

109from pandas.core.ops.invalid import make_invalid_op 

110from pandas.core.sorting import ( 

111 get_group_index, 

112 lexsort_indexer, 

113) 

114 

115from pandas.io.formats.printing import pprint_thing 

116 

117if TYPE_CHECKING: 

118 from pandas import ( 

119 CategoricalIndex, 

120 DataFrame, 

121 Series, 

122 ) 

123 

124 

125class MultiIndexUInt64Engine(libindex.BaseMultiIndexCodesEngine, libindex.UInt64Engine): 

126 """Manages a MultiIndex by mapping label combinations to positive integers. 

127 

128 The number of possible label combinations must not overflow the 64 bits integers. 

129 """ 

130 

131 _base = libindex.UInt64Engine 

132 _codes_dtype = "uint64" 

133 

134 

135class MultiIndexUInt32Engine(libindex.BaseMultiIndexCodesEngine, libindex.UInt32Engine): 

136 """Manages a MultiIndex by mapping label combinations to positive integers. 

137 

138 The number of possible label combinations must not overflow the 32 bits integers. 

139 """ 

140 

141 _base = libindex.UInt32Engine 

142 _codes_dtype = "uint32" 

143 

144 

145class MultiIndexUInt16Engine(libindex.BaseMultiIndexCodesEngine, libindex.UInt16Engine): 

146 """Manages a MultiIndex by mapping label combinations to positive integers. 

147 

148 The number of possible label combinations must not overflow the 16 bits integers. 

149 """ 

150 

151 _base = libindex.UInt16Engine 

152 _codes_dtype = "uint16" 

153 

154 

155class MultiIndexUInt8Engine(libindex.BaseMultiIndexCodesEngine, libindex.UInt8Engine): 

156 """Manages a MultiIndex by mapping label combinations to positive integers. 

157 

158 The number of possible label combinations must not overflow the 8 bits integers. 

159 """ 

160 

161 _base = libindex.UInt8Engine 

162 _codes_dtype = "uint8" 

163 

164 

165class MultiIndexPyIntEngine(libindex.BaseMultiIndexCodesEngine, libindex.ObjectEngine): 

166 """Manages a MultiIndex by mapping label combinations to positive integers. 

167 

168 This class manages those (extreme) cases in which the number of possible 

169 label combinations overflows the 64 bits integers, and uses an ObjectEngine 

170 containing Python integers. 

171 """ 

172 

173 _base = libindex.ObjectEngine 

174 _codes_dtype = "object" 

175 

176 

177def names_compat(meth: F) -> F: 

178 """ 

179 A decorator to allow either `name` or `names` keyword but not both. 

180 

181 This makes it easier to share code with base class. 

182 """ 

183 

184 @wraps(meth) 

185 def new_meth(self_or_cls, *args, **kwargs): 

186 if "name" in kwargs and "names" in kwargs: 

187 raise TypeError("Can only provide one of `names` and `name`") 

188 if "name" in kwargs: 

189 kwargs["names"] = kwargs.pop("name") 

190 

191 return meth(self_or_cls, *args, **kwargs) 

192 

193 return cast(F, new_meth) 

194 

195 

196@set_module("pandas") 

197class MultiIndex(Index): 

198 """ 

199 A multi-level, or hierarchical, index object for pandas objects. 

200 

201 Parameters 

202 ---------- 

203 levels : sequence of arrays 

204 The unique labels for each level. 

205 codes : sequence of arrays 

206 Integers for each level designating which label at each location. 

207 sortorder : optional int 

208 Level of sortedness (must be lexicographically sorted by that 

209 level). 

210 names : optional sequence of objects 

211 Names for each of the index levels. (name is accepted for compat). 

212 copy : bool, default False 

213 Copy the meta-data. 

214 name : Label 

215 Kept for compatibility with 1-dimensional Index. Should not be used. 

216 verify_integrity : bool, default True 

217 Check that the levels/codes are consistent and valid. 

218 

219 Attributes 

220 ---------- 

221 names 

222 levels 

223 codes 

224 nlevels 

225 levshape 

226 dtypes 

227 

228 Methods 

229 ------- 

230 from_arrays 

231 from_tuples 

232 from_product 

233 from_frame 

234 set_levels 

235 set_codes 

236 to_frame 

237 to_flat_index 

238 sortlevel 

239 droplevel 

240 swaplevel 

241 reorder_levels 

242 remove_unused_levels 

243 get_level_values 

244 get_indexer 

245 get_loc 

246 get_locs 

247 get_loc_level 

248 drop 

249 

250 See Also 

251 -------- 

252 MultiIndex.from_arrays : Convert list of arrays to MultiIndex. 

253 MultiIndex.from_product : Create a MultiIndex from the cartesian product 

254 of iterables. 

255 MultiIndex.from_tuples : Convert list of tuples to a MultiIndex. 

256 MultiIndex.from_frame : Make a MultiIndex from a DataFrame. 

257 Index : The base pandas Index type. 

258 

259 Notes 

260 ----- 

261 See the `user guide 

262 <https://pandas.pydata.org/pandas-docs/stable/user_guide/advanced.html>`__ 

263 for more. 

264 

265 Examples 

266 -------- 

267 A new ``MultiIndex`` is typically constructed using one of the helper 

268 methods :meth:`MultiIndex.from_arrays`, :meth:`MultiIndex.from_product` 

269 and :meth:`MultiIndex.from_tuples`. For example (using ``.from_arrays``): 

270 

271 >>> arrays = [[1, 1, 2, 2], ["red", "blue", "red", "blue"]] 

272 >>> pd.MultiIndex.from_arrays(arrays, names=("number", "color")) 

273 MultiIndex([(1, 'red'), 

274 (1, 'blue'), 

275 (2, 'red'), 

276 (2, 'blue')], 

277 names=['number', 'color']) 

278 

279 See further examples for how to construct a MultiIndex in the doc strings 

280 of the mentioned helper methods. 

281 """ 

282 

283 _hidden_attrs = Index._hidden_attrs | frozenset() 

284 

285 # initialize to zero-length tuples to make everything work 

286 _typ = "multiindex" 

287 _names: list[Hashable | None] = [] 

288 _levels = FrozenList() 

289 _codes = FrozenList() 

290 _comparables = ["names"] 

291 

292 sortorder: int | None 

293 

294 # -------------------------------------------------------------------- 

295 # Constructors 

296 

297 def __new__( 

298 cls, 

299 levels=None, 

300 codes=None, 

301 sortorder=None, 

302 names=None, 

303 copy: bool = False, 

304 name=None, 

305 verify_integrity: bool = True, 

306 ) -> Self: 

307 # compat with Index 

308 if name is not None: 

309 names = name 

310 if levels is None or codes is None: 

311 raise TypeError("Must pass both levels and codes") 

312 if len(levels) != len(codes): 

313 raise ValueError("Length of levels and codes must be the same.") 

314 if len(levels) == 0: 

315 raise ValueError("Must pass non-zero number of levels/codes") 

316 

317 result = object.__new__(cls) 

318 result._cache = {} 

319 

320 # we've already validated levels and codes, so shortcut here 

321 result._set_levels(levels, copy=copy, validate=False) 

322 result._set_codes(codes, copy=copy, validate=False) 

323 

324 result._names = [None] * len(levels) 

325 if names is not None: 

326 # handles name validation 

327 result._set_names(names) 

328 

329 if sortorder is not None: 

330 result.sortorder = int(sortorder) 

331 else: 

332 result.sortorder = sortorder 

333 

334 if verify_integrity: 

335 new_codes = result._verify_integrity() 

336 result._codes = new_codes 

337 

338 result._reset_identity() 

339 result._references = None 

340 

341 return result 

342 

343 def _validate_codes(self, level: Index, code: np.ndarray) -> np.ndarray: 

344 """ 

345 Reassign code values as -1 if their corresponding levels are NaN. 

346 

347 Parameters 

348 ---------- 

349 code : Index 

350 Code to reassign. 

351 level : np.ndarray 

352 Level to check for missing values (NaN, NaT, None). 

353 

354 Returns 

355 ------- 

356 new code where code value = -1 if it corresponds 

357 to a level with missing values (NaN, NaT, None). 

358 """ 

359 null_mask = isna(level) 

360 if np.any(null_mask): 

361 code = np.where(null_mask[code], -1, code) 

362 return code 

363 

364 def _verify_integrity( 

365 self, 

366 codes: list | None = None, 

367 levels: list | None = None, 

368 levels_to_verify: list[int] | range | None = None, 

369 ) -> FrozenList: 

370 """ 

371 Parameters 

372 ---------- 

373 codes : optional list 

374 Codes to check for validity. Defaults to current codes. 

375 levels : optional list 

376 Levels to check for validity. Defaults to current levels. 

377 levels_to_validate: optional list 

378 Specifies the levels to verify. 

379 

380 Raises 

381 ------ 

382 ValueError 

383 If length of levels and codes don't match, if the codes for any 

384 level would exceed level bounds, or there are any duplicate levels. 

385 

386 Returns 

387 ------- 

388 new codes where code value = -1 if it corresponds to a 

389 NaN level. 

390 """ 

391 # NOTE: Currently does not check, among other things, that cached 

392 # nlevels matches nor that sortorder matches actually sortorder. 

393 codes = codes or self.codes 

394 levels = levels or self.levels 

395 if levels_to_verify is None: 

396 levels_to_verify = range(len(levels)) 

397 

398 if len(levels) != len(codes): 

399 raise ValueError( 

400 "Length of levels and codes must match. NOTE: " 

401 "this index is in an inconsistent state." 

402 ) 

403 codes_length = len(codes[0]) 

404 for i in levels_to_verify: 

405 level = levels[i] 

406 level_codes = codes[i] 

407 

408 if len(level_codes) != codes_length: 

409 raise ValueError( 

410 f"Unequal code lengths: {[len(code_) for code_ in codes]}" 

411 ) 

412 if len(level_codes) and level_codes.max() >= len(level): 

413 raise ValueError( 

414 f"On level {i}, code max ({level_codes.max()}) >= length of " 

415 f"level ({len(level)}). NOTE: this index is in an " 

416 "inconsistent state" 

417 ) 

418 if len(level_codes) and level_codes.min() < -1: 

419 raise ValueError(f"On level {i}, code value ({level_codes.min()}) < -1") 

420 if not level.is_unique: 

421 raise ValueError( 

422 f"Level values must be unique: {list(level)} on level {i}" 

423 ) 

424 if self.sortorder is not None: 

425 if self.sortorder > _lexsort_depth(self.codes, self.nlevels): 

426 raise ValueError( 

427 "Value for sortorder must be inferior or equal to actual " 

428 f"lexsort_depth: sortorder {self.sortorder} " 

429 f"with lexsort_depth {_lexsort_depth(self.codes, self.nlevels)}" 

430 ) 

431 

432 result_codes = [] 

433 for i in range(len(levels)): 

434 if i in levels_to_verify: 

435 result_codes.append(self._validate_codes(levels[i], codes[i])) 

436 else: 

437 result_codes.append(codes[i]) 

438 

439 new_codes = FrozenList(result_codes) 

440 return new_codes 

441 

442 @classmethod 

443 def from_arrays( 

444 cls, 

445 arrays, 

446 sortorder: int | None = None, 

447 names: Sequence[Hashable] | Hashable | lib.NoDefault = lib.no_default, 

448 ) -> MultiIndex: 

449 """ 

450 Convert arrays to MultiIndex. 

451 

452 Parameters 

453 ---------- 

454 arrays : list / sequence of array-likes 

455 Each array-like gives one level's value for each data point. 

456 len(arrays) is the number of levels. 

457 sortorder : int or None 

458 Level of sortedness (must be lexicographically sorted by that 

459 level). 

460 names : list / sequence of str, optional 

461 Names for the levels in the index. 

462 

463 Returns 

464 ------- 

465 MultiIndex 

466 

467 See Also 

468 -------- 

469 MultiIndex.from_tuples : Convert list of tuples to MultiIndex. 

470 MultiIndex.from_product : Make a MultiIndex from cartesian product 

471 of iterables. 

472 MultiIndex.from_frame : Make a MultiIndex from a DataFrame. 

473 

474 Examples 

475 -------- 

476 >>> arrays = [[1, 1, 2, 2], ["red", "blue", "red", "blue"]] 

477 >>> pd.MultiIndex.from_arrays(arrays, names=("number", "color")) 

478 MultiIndex([(1, 'red'), 

479 (1, 'blue'), 

480 (2, 'red'), 

481 (2, 'blue')], 

482 names=['number', 'color']) 

483 """ 

484 error_msg = "Input must be a list / sequence of array-likes." 

485 if not is_list_like(arrays): 

486 raise TypeError(error_msg) 

487 if is_iterator(arrays): 

488 arrays = list(arrays) 

489 

490 # Check if elements of array are list-like 

491 for array in arrays: 

492 if not is_list_like(array): 

493 raise TypeError(error_msg) 

494 

495 # Check if lengths of all arrays are equal or not, 

496 # raise ValueError, if not 

497 for i in range(1, len(arrays)): 

498 if len(arrays[i]) != len(arrays[i - 1]): 

499 raise ValueError("all arrays must be same length") 

500 

501 codes, levels = factorize_from_iterables(arrays) 

502 if names is lib.no_default: 

503 names = [getattr(arr, "name", None) for arr in arrays] 

504 

505 return cls( 

506 levels=levels, 

507 codes=codes, 

508 sortorder=sortorder, 

509 names=names, 

510 verify_integrity=False, 

511 ) 

512 

513 @classmethod 

514 @names_compat 

515 def from_tuples( 

516 cls, 

517 tuples: Iterable[tuple[Hashable, ...]], 

518 sortorder: int | None = None, 

519 names: Sequence[Hashable] | Hashable | None = None, 

520 ) -> MultiIndex: 

521 """ 

522 Convert list of tuples to MultiIndex. 

523 

524 Parameters 

525 ---------- 

526 tuples : list / sequence of tuple-likes 

527 Each tuple is the index of one row/column. 

528 sortorder : int or None 

529 Level of sortedness (must be lexicographically sorted by that 

530 level). 

531 names : list / sequence of str, optional 

532 Names for the levels in the index. 

533 

534 Returns 

535 ------- 

536 MultiIndex 

537 

538 See Also 

539 -------- 

540 MultiIndex.from_arrays : Convert list of arrays to MultiIndex. 

541 MultiIndex.from_product : Make a MultiIndex from cartesian product 

542 of iterables. 

543 MultiIndex.from_frame : Make a MultiIndex from a DataFrame. 

544 

545 Examples 

546 -------- 

547 >>> tuples = [(1, "red"), (1, "blue"), (2, "red"), (2, "blue")] 

548 >>> pd.MultiIndex.from_tuples(tuples, names=("number", "color")) 

549 MultiIndex([(1, 'red'), 

550 (1, 'blue'), 

551 (2, 'red'), 

552 (2, 'blue')], 

553 names=['number', 'color']) 

554 """ 

555 if not is_list_like(tuples): 

556 raise TypeError("Input must be a list / sequence of tuple-likes.") 

557 if is_iterator(tuples): 

558 tuples = list(tuples) 

559 tuples = cast(Collection[tuple[Hashable, ...]], tuples) 

560 

561 # handling the empty tuple cases 

562 if len(tuples) and all(isinstance(e, tuple) and not e for e in tuples): 

563 codes = [np.zeros(len(tuples))] 

564 levels = [Index(com.asarray_tuplesafe(tuples, dtype=np.dtype("object")))] 

565 return cls( 

566 levels=levels, 

567 codes=codes, 

568 sortorder=sortorder, 

569 names=names, 

570 verify_integrity=False, 

571 ) 

572 

573 arrays: list[Sequence[Hashable]] 

574 if len(tuples) == 0: 

575 if names is None: 

576 raise TypeError("Cannot infer number of levels from empty list") 

577 # error: Argument 1 to "len" has incompatible type "Hashable"; 

578 # expected "Sized" 

579 arrays = [[]] * len(names) # type: ignore[arg-type] 

580 elif isinstance(tuples, (np.ndarray, Index)): 

581 if isinstance(tuples, Index): 

582 tuples = np.asarray(tuples._values) 

583 

584 arrays = list(lib.tuples_to_object_array(tuples).T) 

585 elif isinstance(tuples, list): 

586 arrays = list(lib.to_object_array_tuples(tuples).T) 

587 else: 

588 arrs = zip_longest(*tuples, fillvalue=np.nan) 

589 arrays = cast(list[Sequence[Hashable]], arrs) 

590 

591 return cls.from_arrays(arrays, sortorder=sortorder, names=names) 

592 

593 @classmethod 

594 def from_product( 

595 cls, 

596 iterables: Sequence[Iterable[Hashable]], 

597 sortorder: int | None = None, 

598 names: Sequence[Hashable] | Hashable | lib.NoDefault = lib.no_default, 

599 ) -> MultiIndex: 

600 """ 

601 Make a MultiIndex from the cartesian product of multiple iterables. 

602 

603 Parameters 

604 ---------- 

605 iterables : list / sequence of iterables 

606 Each iterable has unique labels for each level of the index. 

607 sortorder : int or None 

608 Level of sortedness (must be lexicographically sorted by that 

609 level). 

610 names : list / sequence of str, optional 

611 Names for the levels in the index. 

612 If not explicitly provided, names will be inferred from the 

613 elements of iterables if an element has a name attribute. 

614 

615 Returns 

616 ------- 

617 MultiIndex 

618 

619 See Also 

620 -------- 

621 MultiIndex.from_arrays : Convert list of arrays to MultiIndex. 

622 MultiIndex.from_tuples : Convert list of tuples to MultiIndex. 

623 MultiIndex.from_frame : Make a MultiIndex from a DataFrame. 

624 

625 Examples 

626 -------- 

627 >>> numbers = [0, 1, 2] 

628 >>> colors = ["green", "purple"] 

629 >>> pd.MultiIndex.from_product([numbers, colors], names=["number", "color"]) 

630 MultiIndex([(0, 'green'), 

631 (0, 'purple'), 

632 (1, 'green'), 

633 (1, 'purple'), 

634 (2, 'green'), 

635 (2, 'purple')], 

636 names=['number', 'color']) 

637 """ 

638 

639 if not is_list_like(iterables): 

640 raise TypeError("Input must be a list / sequence of iterables.") 

641 if is_iterator(iterables): 

642 iterables = list(iterables) 

643 

644 codes, levels = factorize_from_iterables(iterables) 

645 if names is lib.no_default: 

646 names = [getattr(it, "name", None) for it in iterables] 

647 

648 # codes are all ndarrays, so cartesian_product is lossless 

649 codes = cartesian_product(codes) 

650 return cls(levels, codes, sortorder=sortorder, names=names) 

651 

652 @classmethod 

653 def from_frame( 

654 cls, 

655 df: DataFrame, 

656 sortorder: int | None = None, 

657 names: Sequence[Hashable] | Hashable | None = None, 

658 ) -> MultiIndex: 

659 """ 

660 Make a MultiIndex from a DataFrame. 

661 

662 Parameters 

663 ---------- 

664 df : DataFrame 

665 DataFrame to be converted to MultiIndex. 

666 sortorder : int, optional 

667 Level of sortedness (must be lexicographically sorted by that 

668 level). 

669 names : list-like, optional 

670 If no names are provided, use the column names, or tuple of column 

671 names if the columns is a MultiIndex. If a sequence, overwrite 

672 names with the given sequence. 

673 

674 Returns 

675 ------- 

676 MultiIndex 

677 The MultiIndex representation of the given DataFrame. 

678 

679 See Also 

680 -------- 

681 MultiIndex.from_arrays : Convert list of arrays to MultiIndex. 

682 MultiIndex.from_tuples : Convert list of tuples to MultiIndex. 

683 MultiIndex.from_product : Make a MultiIndex from cartesian product 

684 of iterables. 

685 

686 Examples 

687 -------- 

688 >>> df = pd.DataFrame( 

689 ... [["HI", "Temp"], ["HI", "Precip"], ["NJ", "Temp"], ["NJ", "Precip"]], 

690 ... columns=["a", "b"], 

691 ... ) 

692 >>> df 

693 a b 

694 0 HI Temp 

695 1 HI Precip 

696 2 NJ Temp 

697 3 NJ Precip 

698 

699 >>> pd.MultiIndex.from_frame(df) 

700 MultiIndex([('HI', 'Temp'), 

701 ('HI', 'Precip'), 

702 ('NJ', 'Temp'), 

703 ('NJ', 'Precip')], 

704 names=['a', 'b']) 

705 

706 Using explicit names, instead of the column names 

707 

708 >>> pd.MultiIndex.from_frame(df, names=["state", "observation"]) 

709 MultiIndex([('HI', 'Temp'), 

710 ('HI', 'Precip'), 

711 ('NJ', 'Temp'), 

712 ('NJ', 'Precip')], 

713 names=['state', 'observation']) 

714 """ 

715 if not isinstance(df, ABCDataFrame): 

716 raise TypeError("Input must be a DataFrame") 

717 

718 column_names, columns = zip(*df.items(), strict=True) 

719 names = column_names if names is None else names 

720 return cls.from_arrays(columns, sortorder=sortorder, names=names) 

721 

722 # -------------------------------------------------------------------- 

723 

724 @cache_readonly 

725 def _values(self) -> np.ndarray: 

726 # We override here, since our parent uses _data, which we don't use. 

727 values = [] 

728 

729 for i in range(self.nlevels): 

730 index = self.levels[i] 

731 codes = self.codes[i] 

732 

733 vals = index 

734 if isinstance(vals.dtype, CategoricalDtype): 

735 vals = cast("CategoricalIndex", vals) 

736 vals = vals._data._internal_get_values() 

737 

738 if isinstance(vals.dtype, ExtensionDtype) or lib.is_np_dtype( 

739 vals.dtype, "mM" 

740 ): 

741 vals = vals.astype(object) 

742 

743 array_vals = np.asarray(vals) 

744 array_vals = algos.take_nd(array_vals, codes, fill_value=index._na_value) 

745 values.append(array_vals) 

746 

747 arr = lib.fast_zip(values) 

748 return arr 

749 

750 @property 

751 def values(self) -> np.ndarray: 

752 return self._values 

753 

754 @property 

755 def array(self): 

756 """ 

757 Raises a ValueError for `MultiIndex` because there's no single 

758 array backing a MultiIndex. 

759 

760 Raises 

761 ------ 

762 ValueError 

763 """ 

764 raise ValueError( 

765 "MultiIndex has no single backing array. Use " 

766 "'MultiIndex.to_numpy()' to get a NumPy array of tuples." 

767 ) 

768 

769 @cache_readonly 

770 def dtypes(self) -> Series: 

771 """ 

772 Return the dtypes as a Series for the underlying MultiIndex. 

773 

774 See Also 

775 -------- 

776 Index.dtype : Return the dtype object of the underlying data. 

777 Series.dtypes : Return the data type of the underlying Series. 

778 

779 Examples 

780 -------- 

781 >>> idx = pd.MultiIndex.from_product( 

782 ... [(0, 1, 2), ("green", "purple")], names=["number", "color"] 

783 ... ) 

784 >>> idx 

785 MultiIndex([(0, 'green'), 

786 (0, 'purple'), 

787 (1, 'green'), 

788 (1, 'purple'), 

789 (2, 'green'), 

790 (2, 'purple')], 

791 names=['number', 'color']) 

792 >>> idx.dtypes 

793 number int64 

794 color object 

795 dtype: object 

796 """ 

797 from pandas import Series 

798 

799 names = com.fill_missing_names(self.names) 

800 return Series([level.dtype for level in self.levels], index=Index(names)) 

801 

802 def __len__(self) -> int: 

803 return len(self.codes[0]) 

804 

805 @property 

806 def size(self) -> int: 

807 """ 

808 Return the number of elements in the underlying data. 

809 """ 

810 # override Index.size to avoid materializing _values 

811 return len(self) 

812 

813 # -------------------------------------------------------------------- 

814 # Levels Methods 

815 

816 @cache_readonly 

817 def levels(self) -> FrozenList: 

818 """ 

819 Levels of the MultiIndex. 

820 

821 Levels refer to the different hierarchical levels or layers in a MultiIndex. 

822 In a MultiIndex, each level represents a distinct dimension or category of 

823 the index. 

824 

825 To access the levels, you can use the levels attribute of the MultiIndex, 

826 which returns a tuple of Index objects. Each Index object represents a 

827 level in the MultiIndex and contains the unique values found in that 

828 specific level. 

829 

830 If a MultiIndex is created with levels A, B, C, and the DataFrame using 

831 it filters out all rows of the level C, MultiIndex.levels will still 

832 return A, B, C. 

833 

834 See Also 

835 -------- 

836 MultiIndex.codes : The codes of the levels in the MultiIndex. 

837 MultiIndex.get_level_values : Return vector of label values for requested 

838 level. 

839 

840 Examples 

841 -------- 

842 >>> index = pd.MultiIndex.from_product( 

843 ... [["mammal"], ("goat", "human", "cat", "dog")], 

844 ... names=["Category", "Animals"], 

845 ... ) 

846 >>> leg_num = pd.DataFrame(data=(4, 2, 4, 4), index=index, columns=["Legs"]) 

847 >>> leg_num 

848 Legs 

849 Category Animals 

850 mammal goat 4 

851 human 2 

852 cat 4 

853 dog 4 

854 

855 >>> leg_num.index.levels 

856 FrozenList([['mammal'], ['cat', 'dog', 'goat', 'human']]) 

857 

858 MultiIndex levels will not change even if the DataFrame using the MultiIndex 

859 does not contain all them anymore. 

860 See how "human" is not in the DataFrame, but it is still in levels: 

861 

862 >>> large_leg_num = leg_num[leg_num.Legs > 2] 

863 >>> large_leg_num 

864 Legs 

865 Category Animals 

866 mammal goat 4 

867 cat 4 

868 dog 4 

869 

870 >>> large_leg_num.index.levels 

871 FrozenList([['mammal'], ['cat', 'dog', 'goat', 'human']]) 

872 """ 

873 # Use cache_readonly to ensure that self.get_locs doesn't repeatedly 

874 # create new IndexEngine 

875 # https://github.com/pandas-dev/pandas/issues/31648 

876 result = [ 

877 x._rename(name=name) 

878 for x, name in zip(self._levels, self._names, strict=True) 

879 ] 

880 for level in result: 

881 # disallow midx.levels[0].name = "foo" 

882 level._no_setting_name = True 

883 return FrozenList(result) 

884 

885 def _set_levels( 

886 self, 

887 levels, 

888 *, 

889 level=None, 

890 copy: bool = False, 

891 validate: bool = True, 

892 verify_integrity: bool = False, 

893 ) -> None: 

894 # This is NOT part of the levels property because it should be 

895 # externally not allowed to set levels. User beware if you change 

896 # _levels directly 

897 if validate: 

898 if len(levels) == 0: 

899 raise ValueError("Must set non-zero number of levels.") 

900 if level is None and len(levels) != self.nlevels: 

901 raise ValueError("Length of levels must match number of levels.") 

902 if level is not None and len(levels) != len(level): 

903 raise ValueError("Length of levels must match length of level.") 

904 

905 if level is None: 

906 new_levels = FrozenList( 

907 ensure_index(lev, copy=copy)._view() for lev in levels 

908 ) 

909 level_numbers: range | list[int] = range(len(new_levels)) 

910 else: 

911 level_numbers = [self._get_level_number(lev) for lev in level] 

912 new_levels_list = list(self._levels) 

913 for lev_num, lev in zip(level_numbers, levels, strict=True): 

914 new_levels_list[lev_num] = ensure_index(lev, copy=copy)._view() 

915 new_levels = FrozenList(new_levels_list) 

916 

917 if verify_integrity: 

918 new_codes = self._verify_integrity( 

919 levels=new_levels, levels_to_verify=level_numbers 

920 ) 

921 self._codes = new_codes 

922 

923 names = self.names 

924 self._levels = new_levels 

925 if any(names): 

926 self._set_names(names) 

927 

928 self._reset_cache() 

929 

930 def set_levels( 

931 self, levels, *, level=None, verify_integrity: bool = True 

932 ) -> MultiIndex: 

933 """ 

934 Set new levels on MultiIndex. Defaults to returning new index. 

935 

936 The `set_levels` method provides a flexible way to change the levels of a 

937 `MultiIndex`. This is particularly useful when you need to update the 

938 index structure of your DataFrame without altering the data. The method 

939 returns a new `MultiIndex` unless the operation is performed in-place, 

940 ensuring that the original index remains unchanged unless explicitly 

941 modified. 

942 

943 The method checks the integrity of the new levels against the existing 

944 codes by default, but this can be disabled if you are confident that 

945 your levels are consistent with the underlying data. This can be useful 

946 when you want to perform optimizations or make specific adjustments to 

947 the index levels that do not strictly adhere to the original structure. 

948 

949 Parameters 

950 ---------- 

951 levels : sequence or list of sequence 

952 New level(s) to apply. 

953 level : int, level name, or sequence of int/level names (default None) 

954 Level(s) to set (None for all levels). 

955 verify_integrity : bool, default True 

956 If True, checks that levels and codes are compatible. 

957 

958 Returns 

959 ------- 

960 MultiIndex 

961 A new `MultiIndex` with the updated levels. 

962 

963 See Also 

964 -------- 

965 MultiIndex.set_codes : Set new codes on the existing `MultiIndex`. 

966 MultiIndex.remove_unused_levels : Create new MultiIndex from current that 

967 removes unused levels. 

968 Index.set_names : Set Index or MultiIndex name. 

969 

970 Examples 

971 -------- 

972 >>> idx = pd.MultiIndex.from_tuples( 

973 ... [ 

974 ... (1, "one"), 

975 ... (1, "two"), 

976 ... (2, "one"), 

977 ... (2, "two"), 

978 ... (3, "one"), 

979 ... (3, "two"), 

980 ... ], 

981 ... names=["foo", "bar"], 

982 ... ) 

983 >>> idx 

984 MultiIndex([(1, 'one'), 

985 (1, 'two'), 

986 (2, 'one'), 

987 (2, 'two'), 

988 (3, 'one'), 

989 (3, 'two')], 

990 names=['foo', 'bar']) 

991 

992 >>> idx.set_levels([["a", "b", "c"], [1, 2]]) 

993 MultiIndex([('a', 1), 

994 ('a', 2), 

995 ('b', 1), 

996 ('b', 2), 

997 ('c', 1), 

998 ('c', 2)], 

999 names=['foo', 'bar']) 

1000 >>> idx.set_levels(["a", "b", "c"], level=0) 

1001 MultiIndex([('a', 'one'), 

1002 ('a', 'two'), 

1003 ('b', 'one'), 

1004 ('b', 'two'), 

1005 ('c', 'one'), 

1006 ('c', 'two')], 

1007 names=['foo', 'bar']) 

1008 >>> idx.set_levels(["a", "b"], level="bar") 

1009 MultiIndex([(1, 'a'), 

1010 (1, 'b'), 

1011 (2, 'a'), 

1012 (2, 'b'), 

1013 (3, 'a'), 

1014 (3, 'b')], 

1015 names=['foo', 'bar']) 

1016 

1017 If any of the levels passed to ``set_levels()`` exceeds the 

1018 existing length, all of the values from that argument will 

1019 be stored in the MultiIndex levels, though the values will 

1020 be truncated in the MultiIndex output. 

1021 

1022 >>> idx.set_levels([["a", "b", "c"], [1, 2, 3, 4]], level=[0, 1]) 

1023 MultiIndex([('a', 1), 

1024 ('a', 2), 

1025 ('b', 1), 

1026 ('b', 2), 

1027 ('c', 1), 

1028 ('c', 2)], 

1029 names=['foo', 'bar']) 

1030 >>> idx.set_levels([["a", "b", "c"], [1, 2, 3, 4]], level=[0, 1]).levels 

1031 FrozenList([['a', 'b', 'c'], [1, 2, 3, 4]]) 

1032 """ 

1033 

1034 if isinstance(levels, Index): 

1035 pass 

1036 elif is_array_like(levels): 

1037 levels = Index(levels) 

1038 elif is_list_like(levels): 

1039 levels = list(levels) 

1040 

1041 level, levels = _require_listlike(level, levels, "Levels") 

1042 idx = self._view() 

1043 idx._reset_identity() 

1044 idx._set_levels( 

1045 levels, level=level, validate=True, verify_integrity=verify_integrity 

1046 ) 

1047 return idx 

1048 

1049 @property 

1050 def nlevels(self) -> int: 

1051 """ 

1052 Integer number of levels in this MultiIndex. 

1053 

1054 See Also 

1055 -------- 

1056 MultiIndex.levels : Get the levels of the MultiIndex. 

1057 MultiIndex.codes : Get the codes of the MultiIndex. 

1058 MultiIndex.from_arrays : Convert arrays to MultiIndex. 

1059 MultiIndex.from_tuples : Convert list of tuples to MultiIndex. 

1060 

1061 Examples 

1062 -------- 

1063 >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"], ["c"]]) 

1064 >>> mi 

1065 MultiIndex([('a', 'b', 'c')], 

1066 ) 

1067 >>> mi.nlevels 

1068 3 

1069 """ 

1070 return len(self._levels) 

1071 

1072 @property 

1073 def levshape(self) -> Shape: 

1074 """ 

1075 A tuple representing the length of each level in the MultiIndex. 

1076 

1077 In a `MultiIndex`, each level can contain multiple unique values. The 

1078 `levshape` property provides a quick way to assess the size of each 

1079 level by returning a tuple where each entry represents the number of 

1080 unique values in that specific level. This is particularly useful in 

1081 scenarios where you need to understand the structure and distribution 

1082 of your index levels, such as when working with multidimensional data. 

1083 

1084 See Also 

1085 -------- 

1086 MultiIndex.shape : Return a tuple of the shape of the MultiIndex. 

1087 MultiIndex.levels : Returns the levels of the MultiIndex. 

1088 

1089 Examples 

1090 -------- 

1091 >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"], ["c"]]) 

1092 >>> mi 

1093 MultiIndex([('a', 'b', 'c')], 

1094 ) 

1095 >>> mi.levshape 

1096 (1, 1, 1) 

1097 """ 

1098 return tuple(len(x) for x in self.levels) 

1099 

1100 # -------------------------------------------------------------------- 

1101 # Codes Methods 

1102 

1103 @property 

1104 def codes(self) -> FrozenList: 

1105 """ 

1106 Codes of the MultiIndex. 

1107 

1108 Codes are the position of the index value in the list of level values 

1109 for each level. 

1110 

1111 Returns 

1112 ------- 

1113 tuple of numpy.ndarray 

1114 The codes of the MultiIndex. Each array in the tuple corresponds 

1115 to a level in the MultiIndex. 

1116 

1117 See Also 

1118 -------- 

1119 MultiIndex.set_codes : Set new codes on MultiIndex. 

1120 

1121 Examples 

1122 -------- 

1123 >>> arrays = [[1, 1, 2, 2], ["red", "blue", "red", "blue"]] 

1124 >>> mi = pd.MultiIndex.from_arrays(arrays, names=("number", "color")) 

1125 >>> mi.codes 

1126 FrozenList([[0, 0, 1, 1], [1, 0, 1, 0]]) 

1127 """ 

1128 return self._codes 

1129 

1130 def _set_codes( 

1131 self, 

1132 codes, 

1133 *, 

1134 level=None, 

1135 copy: bool = False, 

1136 validate: bool = True, 

1137 verify_integrity: bool = False, 

1138 ) -> None: 

1139 if validate: 

1140 if level is None and len(codes) != self.nlevels: 

1141 raise ValueError("Length of codes must match number of levels") 

1142 if level is not None and len(codes) != len(level): 

1143 raise ValueError("Length of codes must match length of levels.") 

1144 

1145 level_numbers: list[int] | range 

1146 if level is None: 

1147 new_codes = FrozenList( 

1148 _coerce_indexer_frozen(level_codes, lev, copy=copy).view() 

1149 for lev, level_codes in zip(self._levels, codes, strict=True) 

1150 ) 

1151 level_numbers = range(len(new_codes)) 

1152 else: 

1153 level_numbers = [self._get_level_number(lev) for lev in level] 

1154 new_codes_list = list(self._codes) 

1155 for lev_num, level_codes in zip(level_numbers, codes, strict=True): 

1156 lev = self.levels[lev_num] 

1157 new_codes_list[lev_num] = _coerce_indexer_frozen( 

1158 level_codes, lev, copy=copy 

1159 ) 

1160 new_codes = FrozenList(new_codes_list) 

1161 

1162 if verify_integrity: 

1163 new_codes = self._verify_integrity( 

1164 codes=new_codes, levels_to_verify=level_numbers 

1165 ) 

1166 

1167 self._codes = new_codes 

1168 

1169 self._reset_cache() 

1170 

1171 def set_codes( 

1172 self, codes, *, level=None, verify_integrity: bool = True 

1173 ) -> MultiIndex: 

1174 """ 

1175 Set new codes on MultiIndex. Defaults to returning new index. 

1176 

1177 Parameters 

1178 ---------- 

1179 codes : sequence or list of sequence 

1180 New codes to apply. 

1181 level : int, level name, or sequence of int/level names (default None) 

1182 Level(s) to set (None for all levels). 

1183 verify_integrity : bool, default True 

1184 If True, checks that levels and codes are compatible. 

1185 

1186 Returns 

1187 ------- 

1188 new index (of same type and class...etc) or None 

1189 The same type as the caller or None if ``inplace=True``. 

1190 

1191 See Also 

1192 -------- 

1193 MultiIndex.set_levels : Set new levels on MultiIndex. 

1194 MultiIndex.codes : Get the codes of the levels in the MultiIndex. 

1195 MultiIndex.levels : Get the levels of the MultiIndex. 

1196 

1197 Examples 

1198 -------- 

1199 >>> idx = pd.MultiIndex.from_tuples( 

1200 ... [(1, "one"), (1, "two"), (2, "one"), (2, "two")], names=["foo", "bar"] 

1201 ... ) 

1202 >>> idx 

1203 MultiIndex([(1, 'one'), 

1204 (1, 'two'), 

1205 (2, 'one'), 

1206 (2, 'two')], 

1207 names=['foo', 'bar']) 

1208 

1209 >>> idx.set_codes([[1, 0, 1, 0], [0, 0, 1, 1]]) 

1210 MultiIndex([(2, 'one'), 

1211 (1, 'one'), 

1212 (2, 'two'), 

1213 (1, 'two')], 

1214 names=['foo', 'bar']) 

1215 >>> idx.set_codes([1, 0, 1, 0], level=0) 

1216 MultiIndex([(2, 'one'), 

1217 (1, 'two'), 

1218 (2, 'one'), 

1219 (1, 'two')], 

1220 names=['foo', 'bar']) 

1221 >>> idx.set_codes([0, 0, 1, 1], level="bar") 

1222 MultiIndex([(1, 'one'), 

1223 (1, 'one'), 

1224 (2, 'two'), 

1225 (2, 'two')], 

1226 names=['foo', 'bar']) 

1227 >>> idx.set_codes([[1, 0, 1, 0], [0, 0, 1, 1]], level=[0, 1]) 

1228 MultiIndex([(2, 'one'), 

1229 (1, 'one'), 

1230 (2, 'two'), 

1231 (1, 'two')], 

1232 names=['foo', 'bar']) 

1233 """ 

1234 

1235 level, codes = _require_listlike(level, codes, "Codes") 

1236 idx = self._view() 

1237 idx._reset_identity() 

1238 idx._set_codes(codes, level=level, verify_integrity=verify_integrity) 

1239 return idx 

1240 

1241 # -------------------------------------------------------------------- 

1242 # Index Internals 

1243 

1244 @cache_readonly 

1245 def _engine(self): 

1246 # Calculate the number of bits needed to represent labels in each 

1247 # level, as log2 of their sizes: 

1248 # NaN values are shifted to 1 and missing values in other while 

1249 # calculating the indexer are shifted to 0 

1250 sizes = np.ceil( 

1251 np.log2( 

1252 [len(level) + libindex.multiindex_nulls_shift for level in self.levels] 

1253 ) 

1254 ) 

1255 

1256 # Sum bit counts, starting from the _right_.... 

1257 lev_bits = np.cumsum(sizes[::-1])[::-1] 

1258 

1259 # ... in order to obtain offsets such that sorting the combination of 

1260 # shifted codes (one for each level, resulting in a unique integer) is 

1261 # equivalent to sorting lexicographically the codes themselves. Notice 

1262 # that each level needs to be shifted by the number of bits needed to 

1263 # represent the _previous_ ones: 

1264 offsets = np.concatenate([lev_bits[1:], [0]]) 

1265 # Downcast the type if possible, to prevent upcasting when shifting codes: 

1266 offsets = offsets.astype(np.min_scalar_type(int(offsets[0]))) 

1267 

1268 # Check the total number of bits needed for our representation: 

1269 if lev_bits[0] > 64: 

1270 # The levels would overflow a 64 bit uint - use Python integers: 

1271 return MultiIndexPyIntEngine(self.levels, self.codes, offsets) 

1272 if lev_bits[0] > 32: 

1273 # The levels would overflow a 32 bit uint - use uint64 

1274 return MultiIndexUInt64Engine(self.levels, self.codes, offsets) 

1275 if lev_bits[0] > 16: 

1276 # The levels would overflow a 16 bit uint - use uint8 

1277 return MultiIndexUInt32Engine(self.levels, self.codes, offsets) 

1278 if lev_bits[0] > 8: 

1279 # The levels would overflow a 8 bit uint - use uint16 

1280 return MultiIndexUInt16Engine(self.levels, self.codes, offsets) 

1281 # The levels fit in an 8 bit uint - use uint8 

1282 return MultiIndexUInt8Engine(self.levels, self.codes, offsets) 

1283 

1284 # Return type "Callable[..., MultiIndex]" of "_constructor" incompatible with return 

1285 # type "Type[MultiIndex]" in supertype "Index" 

1286 @property 

1287 def _constructor(self) -> Callable[..., MultiIndex]: # type: ignore[override] 

1288 return type(self).from_tuples 

1289 

1290 def _shallow_copy(self, values: np.ndarray, name=lib.no_default) -> MultiIndex: 

1291 """ 

1292 Create a new Index with the same class as the caller, don't copy the 

1293 data, use the same object attributes with passed in attributes taking 

1294 precedence. 

1295 

1296 *this is an internal non-public method* 

1297 

1298 Parameters 

1299 ---------- 

1300 values : the values to create the new Index, optional 

1301 name : Label, defaults to self.name 

1302 """ 

1303 names = name if name is not lib.no_default else self.names 

1304 

1305 return type(self).from_tuples(values, sortorder=None, names=names) 

1306 

1307 def _view(self) -> MultiIndex: 

1308 result = type(self)( 

1309 levels=self.levels, 

1310 codes=self.codes, 

1311 sortorder=self.sortorder, 

1312 names=self.names, 

1313 verify_integrity=False, 

1314 ) 

1315 result._cache = self._cache.copy() 

1316 result._reset_cache("levels") # GH32669 

1317 return result 

1318 

1319 # -------------------------------------------------------------------- 

1320 

1321 # error: Signature of "copy" incompatible with supertype "Index" 

1322 def copy( # type: ignore[override] 

1323 self, 

1324 names=None, 

1325 deep: bool = False, 

1326 name=None, 

1327 ) -> Self: 

1328 """ 

1329 Make a copy of this object. Names, dtype, levels and codes can be passed and \ 

1330 will be set on new copy. 

1331 

1332 The `copy` method provides a mechanism to create a duplicate of an 

1333 existing MultiIndex object. This is particularly useful in scenarios where 

1334 modifications are required on an index, but the original MultiIndex should 

1335 remain unchanged. By specifying the `deep` parameter, users can control 

1336 whether the copy should be a deep or shallow copy, providing flexibility 

1337 depending on the size and complexity of the MultiIndex. 

1338 

1339 Parameters 

1340 ---------- 

1341 names : sequence, optional 

1342 Names to set on the new MultiIndex object. 

1343 deep : bool, default False 

1344 If False, the new object will be a shallow copy. If True, a deep copy 

1345 will be attempted. Deep copying can be potentially expensive for large 

1346 MultiIndex objects. 

1347 name : Label 

1348 Kept for compatibility with 1-dimensional Index. Should not be used. 

1349 

1350 Returns 

1351 ------- 

1352 MultiIndex 

1353 A new MultiIndex object with the specified modifications. 

1354 

1355 See Also 

1356 -------- 

1357 MultiIndex.from_arrays : Convert arrays to MultiIndex. 

1358 MultiIndex.from_tuples : Convert list of tuples to MultiIndex. 

1359 MultiIndex.from_frame : Convert DataFrame to MultiIndex. 

1360 

1361 Notes 

1362 ----- 

1363 In most cases, there should be no functional difference from using 

1364 ``deep``, but if ``deep`` is passed it will attempt to deepcopy. 

1365 This could be potentially expensive on large MultiIndex objects. 

1366 

1367 Examples 

1368 -------- 

1369 >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"], ["c"]]) 

1370 >>> mi 

1371 MultiIndex([('a', 'b', 'c')], 

1372 ) 

1373 >>> mi.copy() 

1374 MultiIndex([('a', 'b', 'c')], 

1375 ) 

1376 """ 

1377 names = self._validate_names(name=name, names=names, deep=deep) 

1378 keep_id = not deep 

1379 levels, codes = None, None 

1380 

1381 if deep: 

1382 from copy import deepcopy 

1383 

1384 levels = deepcopy(self.levels) 

1385 codes = deepcopy(self.codes) 

1386 

1387 levels = levels if levels is not None else self.levels 

1388 codes = codes if codes is not None else self.codes 

1389 

1390 new_index = type(self)( 

1391 levels=levels, 

1392 codes=codes, 

1393 sortorder=self.sortorder, 

1394 names=names, 

1395 verify_integrity=False, 

1396 ) 

1397 new_index._cache = self._cache.copy() 

1398 new_index._reset_cache("levels") # GH32669 

1399 if keep_id: 

1400 new_index._id = self._id 

1401 return new_index 

1402 

1403 def __array__(self, dtype=None, copy=None) -> np.ndarray: 

1404 """the array interface, return my values""" 

1405 if copy is False: 

1406 # self.values is always a newly construct array, so raise. 

1407 raise ValueError( 

1408 "Unable to avoid copy while creating an array as requested." 

1409 ) 

1410 if copy is True: 

1411 # explicit np.array call to ensure a copy is made and unique objects 

1412 # are returned, because self.values is cached 

1413 return np.array(self.values, dtype=dtype) 

1414 return self.values 

1415 

1416 def view(self, cls=None) -> Self: 

1417 """this is defined as a copy with the same identity""" 

1418 result = self.copy() 

1419 result._id = self._id 

1420 return result 

1421 

1422 def __contains__(self, key: Any) -> bool: 

1423 """ 

1424 Return a boolean indicating whether the provided key is in the index. 

1425 

1426 Parameters 

1427 ---------- 

1428 key : label 

1429 The key to check if it is present in the index. 

1430 

1431 Returns 

1432 ------- 

1433 bool 

1434 Whether the key search is in the index. 

1435 

1436 Raises 

1437 ------ 

1438 TypeError 

1439 If the key is not hashable. 

1440 

1441 See Also 

1442 -------- 

1443 Index.isin : Returns an ndarray of boolean dtype indicating whether the 

1444 list-like key is in the index. 

1445 

1446 Examples 

1447 -------- 

1448 >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"], ["c"]]) 

1449 >>> mi 

1450 MultiIndex([('a', 'b', 'c')], 

1451 ) 

1452 

1453 >>> "a" in mi 

1454 True 

1455 >>> "x" in mi 

1456 False 

1457 """ 

1458 hash(key) 

1459 try: 

1460 self.get_loc(key) 

1461 return True 

1462 except (LookupError, TypeError, ValueError): 

1463 return False 

1464 

1465 @cache_readonly 

1466 def dtype(self) -> np.dtype: 

1467 return np.dtype("O") 

1468 

1469 @cache_readonly 

1470 def _is_memory_usage_qualified(self) -> bool: 

1471 """return a boolean if we need a qualified .info display""" 

1472 

1473 def f(dtype) -> bool: 

1474 return is_object_dtype(dtype) or ( 

1475 is_string_dtype(dtype) and dtype.storage == "python" 

1476 ) 

1477 

1478 return any(f(level.dtype) for level in self.levels) 

1479 

1480 # Cannot determine type of "memory_usage" 

1481 def memory_usage(self, deep: bool = False) -> int: 

1482 """ 

1483 Memory usage of the values. 

1484 

1485 Parameters 

1486 ---------- 

1487 deep : bool, default False 

1488 Introspect the data deeply, interrogate 

1489 `object` dtypes for system-level memory consumption. 

1490 

1491 Returns 

1492 ------- 

1493 bytes used 

1494 Returns memory usage of the values in the Index in bytes. 

1495 

1496 See Also 

1497 -------- 

1498 numpy.ndarray.nbytes : Total bytes consumed by the elements of the 

1499 array. 

1500 

1501 Notes 

1502 ----- 

1503 Memory usage does not include memory consumed by elements that 

1504 are not components of the array if deep=False or if used on PyPy 

1505 

1506 Examples 

1507 -------- 

1508 >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"], ["c"]]) 

1509 >>> mi.memory_usage() 

1510 81 

1511 """ 

1512 # we are overwriting our base class to avoid 

1513 # computing .values here which could materialize 

1514 # a tuple representation unnecessarily 

1515 return self._nbytes(deep) 

1516 

1517 @cache_readonly 

1518 def nbytes(self) -> int: 

1519 """return the number of bytes in the underlying data""" 

1520 return self._nbytes(False) 

1521 

1522 def _nbytes(self, deep: bool = False) -> int: 

1523 """ 

1524 return the number of bytes in the underlying data 

1525 deeply introspect the level data if deep=True 

1526 

1527 include the engine hashtable 

1528 

1529 *this is in internal routine* 

1530 

1531 """ 

1532 # for implementations with no useful getsizeof (PyPy) 

1533 objsize = 24 

1534 

1535 level_nbytes = sum(i.memory_usage(deep=deep) for i in self.levels) 

1536 label_nbytes = sum(i.nbytes for i in self.codes) 

1537 names_nbytes = sum(getsizeof(i, objsize) for i in self.names) 

1538 result = level_nbytes + label_nbytes + names_nbytes 

1539 

1540 # include our engine hashtable, only if it's already cached 

1541 if "_engine" in self._cache: 

1542 result += self._engine.sizeof(deep=deep) 

1543 return result 

1544 

1545 # -------------------------------------------------------------------- 

1546 # Rendering Methods 

1547 

1548 def _formatter_func(self, tup): 

1549 """ 

1550 Formats each item in tup according to its level's formatter function. 

1551 """ 

1552 formatter_funcs = (level._formatter_func for level in self.levels) 

1553 return tuple(func(val) for func, val in zip(formatter_funcs, tup, strict=True)) 

1554 

1555 def _get_values_for_csv( 

1556 self, *, na_rep: str = "nan", **kwargs 

1557 ) -> npt.NDArray[np.object_]: 

1558 new_levels = [] 

1559 new_codes = [] 

1560 

1561 # go through the levels and format them 

1562 for level, level_codes in zip(self.levels, self.codes, strict=True): 

1563 level_strs = level._get_values_for_csv(na_rep=na_rep, **kwargs) 

1564 # add nan values, if there are any 

1565 mask = level_codes == -1 

1566 if mask.any(): 

1567 nan_index = len(level_strs) 

1568 # numpy 1.21 deprecated implicit string casting 

1569 level_strs = level_strs.astype(str) 

1570 level_strs = np.append(level_strs, na_rep) 

1571 assert not level_codes.flags.writeable # i.e. copy is needed 

1572 level_codes = level_codes.copy() # make writeable 

1573 level_codes[mask] = nan_index 

1574 new_levels.append(level_strs) 

1575 new_codes.append(level_codes) 

1576 

1577 if len(new_levels) == 1: 

1578 # a single-level multi-index 

1579 return Index( 

1580 new_levels[0].take(new_codes[0]), copy=False 

1581 )._get_values_for_csv() 

1582 else: 

1583 # reconstruct the multi-index 

1584 mi = MultiIndex( 

1585 levels=new_levels, 

1586 codes=new_codes, 

1587 names=self.names, 

1588 sortorder=self.sortorder, 

1589 verify_integrity=False, 

1590 ) 

1591 return mi._values 

1592 

1593 def _format_multi( 

1594 self, 

1595 *, 

1596 include_names: bool, 

1597 sparsify: bool | None | lib.NoDefault, 

1598 formatter: Callable | None = None, 

1599 ) -> list: 

1600 if len(self) == 0: 

1601 return [] 

1602 

1603 stringified_levels = [] 

1604 for lev, level_codes in zip(self.levels, self.codes, strict=True): 

1605 na = _get_na_rep(lev.dtype) 

1606 

1607 if len(lev) > 0: 

1608 taken = formatted = lev.take(level_codes) 

1609 formatted = taken._format_flat(include_name=False, formatter=formatter) 

1610 

1611 # we have some NA 

1612 mask = level_codes == -1 

1613 if mask.any(): 

1614 formatted = np.array(formatted, dtype=object) 

1615 formatted[mask] = na 

1616 formatted = formatted.tolist() 

1617 

1618 else: 

1619 # weird all NA case 

1620 formatted = [ 

1621 pprint_thing(na if isna(x) else x, escape_chars=("\t", "\r", "\n")) 

1622 for x in algos.take_nd(lev._values, level_codes) 

1623 ] 

1624 stringified_levels.append(formatted) 

1625 

1626 result_levels = [] 

1627 for lev, lev_name in zip(stringified_levels, self.names, strict=True): 

1628 level = [] 

1629 

1630 if include_names: 

1631 level.append( 

1632 pprint_thing(lev_name, escape_chars=("\t", "\r", "\n")) 

1633 if lev_name is not None 

1634 else "" 

1635 ) 

1636 

1637 level.extend(np.array(lev, dtype=object)) 

1638 result_levels.append(level) 

1639 

1640 if sparsify is None: 

1641 sparsify = get_option("display.multi_sparse") 

1642 

1643 if sparsify: 

1644 sentinel: Literal[""] | bool | lib.NoDefault = "" 

1645 # GH3547 use value of sparsify as sentinel if it's "Falsey" 

1646 assert isinstance(sparsify, bool) or sparsify is lib.no_default 

1647 if sparsify is lib.no_default: 

1648 sentinel = sparsify 

1649 # little bit of a kludge job for #1217 

1650 result_levels = sparsify_labels( 

1651 result_levels, start=int(include_names), sentinel=sentinel 

1652 ) 

1653 

1654 return result_levels 

1655 

1656 # -------------------------------------------------------------------- 

1657 # Names Methods 

1658 

1659 def _get_names(self) -> FrozenList: 

1660 return FrozenList(self._names) 

1661 

1662 def _set_names(self, names, *, level=None) -> None: 

1663 """ 

1664 Set new names on index. Each name has to be a hashable type. 

1665 

1666 Parameters 

1667 ---------- 

1668 values : str or sequence 

1669 name(s) to set 

1670 level : int, level name, or sequence of int/level names (default None) 

1671 If the index is a MultiIndex (hierarchical), level(s) to set (None 

1672 for all levels). Otherwise level must be None 

1673 

1674 Raises 

1675 ------ 

1676 TypeError if each name is not hashable. 

1677 

1678 Notes 

1679 ----- 

1680 sets names on levels. WARNING: mutates! 

1681 

1682 Note that you generally want to set this *after* changing levels, so 

1683 that it only acts on copies 

1684 """ 

1685 # GH 15110 

1686 # Don't allow a single string for names in a MultiIndex 

1687 if names is not None and not is_list_like(names): 

1688 raise ValueError("Names should be list-like for a MultiIndex") 

1689 names = list(names) 

1690 

1691 if level is not None and len(names) != len(level): 

1692 raise ValueError("Length of names must match length of level.") 

1693 if level is None and len(names) != self.nlevels: 

1694 raise ValueError( 

1695 "Length of names must match number of levels in MultiIndex." 

1696 ) 

1697 

1698 if level is None: 

1699 level = range(self.nlevels) 

1700 else: 

1701 level = (self._get_level_number(lev) for lev in level) 

1702 

1703 # set the name 

1704 for lev, name in zip(level, names, strict=True): 

1705 if name is not None: 

1706 # GH 20527 

1707 # All items in 'names' need to be hashable: 

1708 if not is_hashable(name): 

1709 raise TypeError( 

1710 f"{type(self).__name__}.name must be a hashable type" 

1711 ) 

1712 self._names[lev] = name 

1713 

1714 # If .levels has been accessed, the .name of each level in our cache 

1715 # will be stale. 

1716 self._reset_cache("levels") 

1717 

1718 names = property( 

1719 fset=_set_names, 

1720 fget=_get_names, 

1721 doc=""" 

1722 Names of levels in MultiIndex. 

1723 

1724 This attribute provides access to the names of the levels in a `MultiIndex`. 

1725 The names are stored as a `FrozenList`, which is an immutable list-like 

1726 container. Each name corresponds to a level in the `MultiIndex`, and can be 

1727 used to identify or manipulate the levels individually. 

1728 

1729 See Also 

1730 -------- 

1731 MultiIndex.set_names : Set Index or MultiIndex name. 

1732 MultiIndex.rename : Rename specific levels in a MultiIndex. 

1733 Index.names : Get names on index. 

1734 

1735 Examples 

1736 -------- 

1737 >>> mi = pd.MultiIndex.from_arrays( 

1738 ... [[1, 2], [3, 4], [5, 6]], names=['x', 'y', 'z'] 

1739 ... ) 

1740 >>> mi 

1741 MultiIndex([(1, 3, 5), 

1742 (2, 4, 6)], 

1743 names=['x', 'y', 'z']) 

1744 >>> mi.names 

1745 FrozenList(['x', 'y', 'z']) 

1746 """, 

1747 ) 

1748 

1749 # -------------------------------------------------------------------- 

1750 

1751 @cache_readonly 

1752 def inferred_type(self) -> str: 

1753 return "mixed" 

1754 

1755 def _get_level_number(self, level) -> int: 

1756 count = self.names.count(level) 

1757 if (count > 1) and not is_integer(level): 

1758 raise ValueError( 

1759 f"The name {level} occurs multiple times, use a level number" 

1760 ) 

1761 try: 

1762 level = self.names.index(level) 

1763 except ValueError as err: 

1764 if not is_integer(level): 

1765 raise KeyError(f"Level {level} not found") from err 

1766 if level < 0: 

1767 level += self.nlevels 

1768 if level < 0: 

1769 orig_level = level - self.nlevels 

1770 raise IndexError( 

1771 f"Too many levels: Index has only {self.nlevels} levels, " 

1772 f"{orig_level} is not a valid level number" 

1773 ) from err 

1774 # Note: levels are zero-based 

1775 elif level >= self.nlevels: 

1776 raise IndexError( 

1777 f"Too many levels: Index has only {self.nlevels} levels, " 

1778 f"not {level + 1}" 

1779 ) from err 

1780 return level 

1781 

1782 @cache_readonly 

1783 def is_monotonic_increasing(self) -> bool: 

1784 """ 

1785 Return a boolean if the values are equal or increasing. 

1786 """ 

1787 if any(-1 in code for code in self.codes): 

1788 return False 

1789 

1790 if all(level.is_monotonic_increasing for level in self.levels): 

1791 # If each level is sorted, we can operate on the codes directly. GH27495 

1792 return libalgos.is_lexsorted( 

1793 [x.astype("int64", copy=False) for x in self.codes] 

1794 ) 

1795 

1796 # reversed() because lexsort() wants the most significant key last. 

1797 values = [ 

1798 self._get_level_values(i)._values for i in reversed(range(len(self.levels))) 

1799 ] 

1800 try: 

1801 # error: Argument 1 to "lexsort" has incompatible type 

1802 # "List[Union[ExtensionArray, ndarray[Any, Any]]]"; 

1803 # expected "Union[_SupportsArray[dtype[Any]], 

1804 # _NestedSequence[_SupportsArray[dtype[Any]]], bool, 

1805 # int, float, complex, str, bytes, _NestedSequence[Union 

1806 # [bool, int, float, complex, str, bytes]]]" 

1807 sort_order = np.lexsort(values) # type: ignore[arg-type] 

1808 return Index(sort_order, copy=False).is_monotonic_increasing 

1809 except TypeError: 

1810 # we have mixed types and np.lexsort is not happy 

1811 return Index(self._values, copy=False).is_monotonic_increasing 

1812 

1813 @cache_readonly 

1814 def is_monotonic_decreasing(self) -> bool: 

1815 """ 

1816 Return a boolean if the values are equal or decreasing. 

1817 """ 

1818 # monotonic decreasing if and only if reverse is monotonic increasing 

1819 return self[::-1].is_monotonic_increasing 

1820 

1821 def duplicated(self, keep: DropKeep = "first") -> npt.NDArray[np.bool_]: 

1822 """ 

1823 Indicate duplicate index values. 

1824 

1825 Duplicated values are indicated as ``True`` values in the resulting 

1826 array. Either all duplicates, all except the first, or all except the 

1827 last occurrence of duplicates can be indicated. 

1828 

1829 Parameters 

1830 ---------- 

1831 keep : {'first', 'last', False}, default 'first' 

1832 The value or values in a set of duplicates to mark as missing. 

1833 

1834 - 'first' : Mark duplicates as ``True`` except for the first 

1835 occurrence. 

1836 - 'last' : Mark duplicates as ``True`` except for the last 

1837 occurrence. 

1838 - ``False`` : Mark all duplicates as ``True``. 

1839 

1840 Returns 

1841 ------- 

1842 np.ndarray[bool] 

1843 A numpy array of boolean values indicating duplicate index values. 

1844 

1845 See Also 

1846 -------- 

1847 Series.duplicated : Equivalent method on pandas.Series. 

1848 DataFrame.duplicated : Equivalent method on pandas.DataFrame. 

1849 Index.drop_duplicates : Remove duplicate values from Index. 

1850 

1851 Examples 

1852 -------- 

1853 By default, for each set of duplicated values, the first occurrence is 

1854 set to False and all others to True: 

1855 

1856 >>> mi = pd.MultiIndex.from_arrays((list("abca"), list("defd"))) 

1857 >>> mi.duplicated() 

1858 array([False, False, False, True]) 

1859 

1860 which is equivalent to 

1861 

1862 >>> mi.duplicated(keep="first") 

1863 array([False, False, False, True]) 

1864 

1865 By using 'last', the last occurrence of each set of duplicated values 

1866 is set on False and all others on True: 

1867 

1868 >>> mi.duplicated(keep="last") 

1869 array([ True, False, False, False]) 

1870 

1871 By setting keep on ``False``, all duplicates are True: 

1872 

1873 >>> mi.duplicated(keep=False) 

1874 array([ True, False, False, True]) 

1875 """ 

1876 shape = tuple(len(lev) for lev in self.levels) 

1877 ids = get_group_index(self.codes, shape, sort=False, xnull=False) 

1878 

1879 return duplicated(ids, keep) 

1880 

1881 # error: Cannot override final attribute "_duplicated" 

1882 # (previously declared in base class "IndexOpsMixin") 

1883 _duplicated = duplicated # type: ignore[misc] 

1884 

1885 def fillna(self, value): 

1886 """ 

1887 fillna is not implemented for MultiIndex 

1888 """ 

1889 raise NotImplementedError("fillna is not defined for MultiIndex") 

1890 

1891 def dropna(self, how: AnyAll = "any") -> MultiIndex: 

1892 """ 

1893 Return MultiIndex without NA/NaN values. 

1894 

1895 Parameters 

1896 ---------- 

1897 how : {'any', 'all'}, default 'any' 

1898 Drop the value when any or all levels are NaN. 

1899 

1900 Returns 

1901 ------- 

1902 Index 

1903 Returns an MultiIndex object after removing NA/NaN values. 

1904 

1905 See Also 

1906 -------- 

1907 Index.fillna : Fill NA/NaN values with the specified value. 

1908 Index.isna : Detect missing values. 

1909 

1910 Examples 

1911 -------- 

1912 >>> mi = pd.MultiIndex.from_arrays(([np.nan, np.nan, 2.0], [3.0, np.nan, 4.0])) 

1913 >>> mi.dropna() 

1914 MultiIndex([(2.0, 4.0)], 

1915 ) 

1916 >>> mi.dropna(how="all") 

1917 MultiIndex([(nan, 3.0), 

1918 (2.0, 4.0)], 

1919 ) 

1920 """ 

1921 nans = [level_codes == -1 for level_codes in self.codes] 

1922 if how == "any": 

1923 indexer = np.any(nans, axis=0) 

1924 elif how == "all": 

1925 indexer = np.all(nans, axis=0) 

1926 else: 

1927 raise ValueError(f"invalid how option: {how}") 

1928 

1929 new_codes = [level_codes[~indexer] for level_codes in self.codes] 

1930 return self.set_codes(codes=new_codes) 

1931 

1932 def _get_level_values(self, level: int, unique: bool = False) -> Index: 

1933 """ 

1934 Return vector of label values for requested level, 

1935 equal to the length of the index 

1936 

1937 **this is an internal method** 

1938 

1939 Parameters 

1940 ---------- 

1941 level : int 

1942 unique : bool, default False 

1943 if True, drop duplicated values 

1944 

1945 Returns 

1946 ------- 

1947 Index 

1948 """ 

1949 lev = self.levels[level] 

1950 level_codes = self.codes[level] 

1951 name = self._names[level] 

1952 if unique: 

1953 level_codes = algos.unique(level_codes) 

1954 filled = algos.take_nd(lev._values, level_codes, fill_value=lev._na_value) 

1955 return lev._shallow_copy(filled, name=name) 

1956 

1957 def get_level_values(self, level) -> Index: 

1958 """ 

1959 Return vector of label values for requested level. 

1960 

1961 Length of returned vector is equal to the length of the index. 

1962 The `get_level_values` method is a crucial utility for extracting 

1963 specific level values from a `MultiIndex`. This function is particularly 

1964 useful when working with multi-level data, allowing you to isolate 

1965 and manipulate individual levels without having to deal with the 

1966 complexity of the entire `MultiIndex` structure. It seamlessly handles 

1967 both integer and string-based level access, providing flexibility in 

1968 how you can interact with the data. Additionally, this method ensures 

1969 that the returned `Index` maintains the integrity of the original data, 

1970 even when missing values are present, by appropriately casting the 

1971 result to a suitable data type. 

1972 

1973 Parameters 

1974 ---------- 

1975 level : int or str 

1976 ``level`` is either the integer position of the level in the 

1977 MultiIndex, or the name of the level. 

1978 

1979 Returns 

1980 ------- 

1981 Index 

1982 Values is a level of this MultiIndex converted to 

1983 a single :class:`Index` (or subclass thereof). 

1984 

1985 See Also 

1986 -------- 

1987 MultiIndex : A multi-level, or hierarchical, index object for pandas objects. 

1988 Index : Immutable sequence used for indexing and alignment. 

1989 MultiIndex.remove_unused_levels : Create new MultiIndex from current that 

1990 removes unused levels. 

1991 

1992 Notes 

1993 ----- 

1994 If the level contains missing values, the result may be casted to 

1995 ``float`` with missing values specified as ``NaN``. This is because 

1996 the level is converted to a regular ``Index``. 

1997 

1998 Examples 

1999 -------- 

2000 Create a MultiIndex: 

2001 

2002 >>> mi = pd.MultiIndex.from_arrays((list("abc"), list("def"))) 

2003 >>> mi.names = ["level_1", "level_2"] 

2004 

2005 Get level values by supplying level as either integer or name: 

2006 

2007 >>> mi.get_level_values(0) 

2008 Index(['a', 'b', 'c'], dtype='str', name='level_1') 

2009 >>> mi.get_level_values("level_2") 

2010 Index(['d', 'e', 'f'], dtype='str', name='level_2') 

2011 

2012 If a level contains missing values, the return type of the level 

2013 may be cast to ``float``. 

2014 

2015 >>> pd.MultiIndex.from_arrays([[1, None, 2], [3, 4, 5]]).dtypes 

2016 level_0 int64 

2017 level_1 int64 

2018 dtype: object 

2019 >>> pd.MultiIndex.from_arrays([[1, None, 2], [3, 4, 5]]).get_level_values(0) 

2020 Index([1.0, nan, 2.0], dtype='float64') 

2021 """ 

2022 level = self._get_level_number(level) 

2023 values = self._get_level_values(level) 

2024 return values 

2025 

2026 def unique(self, level=None): 

2027 """ 

2028 Return unique values in the index. 

2029 

2030 Unique values are returned in order of appearance, this does NOT sort. 

2031 

2032 Parameters 

2033 ---------- 

2034 level : int or hashable, optional 

2035 Only return values from specified level (for MultiIndex). 

2036 If int, gets the level by integer position, else by level name. 

2037 

2038 Returns 

2039 ------- 

2040 MultiIndex 

2041 Unique values in the MultiIndex. 

2042 

2043 See Also 

2044 -------- 

2045 unique : Numpy array of unique values in that column. 

2046 Series.unique : Return unique values of Series object. 

2047 

2048 Examples 

2049 -------- 

2050 >>> mi = pd.MultiIndex.from_arrays((list("abca"), list("defd"))) 

2051 >>> mi 

2052 MultiIndex([('a', 'd'), 

2053 ('b', 'e'), 

2054 ('c', 'f'), 

2055 ('a', 'd')], 

2056 ) 

2057 >>> mi.unique() 

2058 MultiIndex([('a', 'd'), 

2059 ('b', 'e'), 

2060 ('c', 'f')], 

2061 ) 

2062 """ 

2063 if level is None: 

2064 return self.drop_duplicates() 

2065 else: 

2066 level = self._get_level_number(level) 

2067 return self._get_level_values(level=level, unique=True) 

2068 

2069 def to_frame( 

2070 self, 

2071 index: bool = True, 

2072 name=lib.no_default, 

2073 allow_duplicates: bool = False, 

2074 ) -> DataFrame: 

2075 """ 

2076 Create a DataFrame with the levels of the MultiIndex as columns. 

2077 

2078 Column ordering is determined by the DataFrame constructor with data as 

2079 a dict. 

2080 

2081 Parameters 

2082 ---------- 

2083 index : bool, default True 

2084 Set the index of the returned DataFrame as the original MultiIndex. 

2085 

2086 name : list / sequence of str, optional 

2087 The passed names should substitute index level names. 

2088 

2089 allow_duplicates : bool, optional default False 

2090 Allow duplicate column labels to be created. 

2091 

2092 Returns 

2093 ------- 

2094 DataFrame 

2095 DataFrame representation of the MultiIndex, with levels as columns. 

2096 

2097 See Also 

2098 -------- 

2099 DataFrame : Two-dimensional, size-mutable, potentially heterogeneous 

2100 tabular data. 

2101 

2102 Examples 

2103 -------- 

2104 >>> mi = pd.MultiIndex.from_arrays([["a", "b"], ["c", "d"]]) 

2105 >>> mi 

2106 MultiIndex([('a', 'c'), 

2107 ('b', 'd')], 

2108 ) 

2109 

2110 >>> df = mi.to_frame() 

2111 >>> df 

2112 0 1 

2113 a c a c 

2114 b d b d 

2115 

2116 >>> df = mi.to_frame(index=False) 

2117 >>> df 

2118 0 1 

2119 0 a c 

2120 1 b d 

2121 

2122 >>> df = mi.to_frame(name=["x", "y"]) 

2123 >>> df 

2124 x y 

2125 a c a c 

2126 b d b d 

2127 """ 

2128 from pandas import DataFrame 

2129 

2130 if name is not lib.no_default: 

2131 if not is_list_like(name): 

2132 raise TypeError("'name' must be a list / sequence of column names.") 

2133 

2134 if len(name) != len(self.levels): 

2135 raise ValueError( 

2136 "'name' should have same length as number of levels on index." 

2137 ) 

2138 idx_names = name 

2139 else: 

2140 idx_names = self._get_level_names() 

2141 

2142 if not allow_duplicates and len(set(idx_names)) != len(idx_names): 

2143 raise ValueError( 

2144 "Cannot create duplicate column labels if allow_duplicates is False" 

2145 ) 

2146 

2147 # Guarantee resulting column order - PY36+ dict maintains insertion order 

2148 result = DataFrame( 

2149 {level: self._get_level_values(level) for level in range(len(self.levels))}, 

2150 copy=False, 

2151 ) 

2152 result.columns = idx_names 

2153 

2154 if index: 

2155 result.index = self 

2156 return result 

2157 

2158 # error: Return type "Index" of "to_flat_index" incompatible with return type 

2159 # "MultiIndex" in supertype "Index" 

2160 def to_flat_index(self) -> Index: # type: ignore[override] 

2161 """ 

2162 Convert a MultiIndex to an Index of Tuples containing the level values. 

2163 

2164 Returns 

2165 ------- 

2166 pd.Index 

2167 Index with the MultiIndex data represented in Tuples. 

2168 

2169 See Also 

2170 -------- 

2171 MultiIndex.from_tuples : Convert flat index back to MultiIndex. 

2172 

2173 Notes 

2174 ----- 

2175 This method will simply return the caller if called by anything other 

2176 than a MultiIndex. 

2177 

2178 Examples 

2179 -------- 

2180 >>> index = pd.MultiIndex.from_product( 

2181 ... [["foo", "bar"], ["baz", "qux"]], names=["a", "b"] 

2182 ... ) 

2183 >>> index.to_flat_index() 

2184 Index([('foo', 'baz'), ('foo', 'qux'), 

2185 ('bar', 'baz'), ('bar', 'qux')], 

2186 dtype='object') 

2187 """ 

2188 return Index(self._values, tupleize_cols=False, copy=False) 

2189 

2190 def _is_lexsorted(self) -> bool: 

2191 """ 

2192 Return True if the codes are lexicographically sorted. 

2193 

2194 Returns 

2195 ------- 

2196 bool 

2197 

2198 Examples 

2199 -------- 

2200 In the below examples, the first level of the MultiIndex is sorted because 

2201 a<b<c, so there is no need to look at the next level. 

2202 

2203 >>> pd.MultiIndex.from_arrays( 

2204 ... [["a", "b", "c"], ["d", "e", "f"]] 

2205 ... )._is_lexsorted() 

2206 True 

2207 >>> pd.MultiIndex.from_arrays( 

2208 ... [["a", "b", "c"], ["d", "f", "e"]] 

2209 ... )._is_lexsorted() 

2210 True 

2211 

2212 In case there is a tie, the lexicographical sorting looks 

2213 at the next level of the MultiIndex. 

2214 

2215 >>> pd.MultiIndex.from_arrays([[0, 1, 1], ["a", "b", "c"]])._is_lexsorted() 

2216 True 

2217 >>> pd.MultiIndex.from_arrays([[0, 1, 1], ["a", "c", "b"]])._is_lexsorted() 

2218 False 

2219 >>> pd.MultiIndex.from_arrays( 

2220 ... [["a", "a", "b", "b"], ["aa", "bb", "aa", "bb"]] 

2221 ... )._is_lexsorted() 

2222 True 

2223 >>> pd.MultiIndex.from_arrays( 

2224 ... [["a", "a", "b", "b"], ["bb", "aa", "aa", "bb"]] 

2225 ... )._is_lexsorted() 

2226 False 

2227 """ 

2228 return self._lexsort_depth == self.nlevels 

2229 

2230 @cache_readonly 

2231 def _lexsort_depth(self) -> int: 

2232 """ 

2233 Compute and return the lexsort_depth, the number of levels of the 

2234 MultiIndex that are sorted lexically 

2235 

2236 Returns 

2237 ------- 

2238 int 

2239 """ 

2240 if self.sortorder is not None: 

2241 return self.sortorder 

2242 return _lexsort_depth(self.codes, self.nlevels) 

2243 

2244 def _sort_levels_monotonic(self, raise_if_incomparable: bool = False) -> MultiIndex: 

2245 """ 

2246 This is an *internal* function. 

2247 

2248 Create a new MultiIndex from the current to monotonically sorted 

2249 items IN the levels. This does not actually make the entire MultiIndex 

2250 monotonic, JUST the levels. 

2251 

2252 The resulting MultiIndex will have the same outward 

2253 appearance, meaning the same .values and ordering. It will also 

2254 be .equals() to the original. 

2255 

2256 Returns 

2257 ------- 

2258 MultiIndex 

2259 

2260 Examples 

2261 -------- 

2262 >>> mi = pd.MultiIndex( 

2263 ... levels=[["a", "b"], ["bb", "aa"]], codes=[[0, 0, 1, 1], [0, 1, 0, 1]] 

2264 ... ) 

2265 >>> mi 

2266 MultiIndex([('a', 'bb'), 

2267 ('a', 'aa'), 

2268 ('b', 'bb'), 

2269 ('b', 'aa')], 

2270 ) 

2271 

2272 >>> mi.sort_values() 

2273 MultiIndex([('a', 'aa'), 

2274 ('a', 'bb'), 

2275 ('b', 'aa'), 

2276 ('b', 'bb')], 

2277 ) 

2278 """ 

2279 if self._is_lexsorted() and self.is_monotonic_increasing: 

2280 return self 

2281 

2282 new_levels = [] 

2283 new_codes = [] 

2284 

2285 for lev, level_codes in zip(self.levels, self.codes, strict=True): 

2286 if not lev.is_monotonic_increasing: 

2287 try: 

2288 # indexer to reorder the levels 

2289 indexer = lev.argsort() 

2290 except TypeError: 

2291 if raise_if_incomparable: 

2292 raise 

2293 else: 

2294 lev = lev.take(indexer) 

2295 

2296 # indexer to reorder the level codes 

2297 indexer = ensure_platform_int(indexer) 

2298 ri = lib.get_reverse_indexer(indexer, len(indexer)) 

2299 level_codes = algos.take_nd(ri, level_codes, fill_value=-1) 

2300 

2301 new_levels.append(lev) 

2302 new_codes.append(level_codes) 

2303 

2304 return MultiIndex( 

2305 new_levels, 

2306 new_codes, 

2307 names=self.names, 

2308 sortorder=self.sortorder, 

2309 verify_integrity=False, 

2310 ) 

2311 

2312 def remove_unused_levels(self) -> MultiIndex: 

2313 """ 

2314 Create new MultiIndex from current that removes unused levels. 

2315 

2316 Unused level(s) means levels that are not expressed in the 

2317 labels. The resulting MultiIndex will have the same outward 

2318 appearance, meaning the same .values and ordering. It will 

2319 also be .equals() to the original. 

2320 

2321 The `remove_unused_levels` method is useful in cases where you have a 

2322 MultiIndex with hierarchical levels, but some of these levels are no 

2323 longer needed due to filtering or subsetting operations. By removing 

2324 the unused levels, the resulting MultiIndex becomes more compact and 

2325 efficient, which can improve performance in subsequent operations. 

2326 

2327 Returns 

2328 ------- 

2329 MultiIndex 

2330 A new MultiIndex with unused levels removed. 

2331 

2332 See Also 

2333 -------- 

2334 MultiIndex.droplevel : Remove specified levels from a MultiIndex. 

2335 MultiIndex.reorder_levels : Rearrange levels of a MultiIndex. 

2336 MultiIndex.set_levels : Set new levels on a MultiIndex. 

2337 

2338 Examples 

2339 -------- 

2340 >>> mi = pd.MultiIndex.from_product([range(2), list("ab")]) 

2341 >>> mi 

2342 MultiIndex([(0, 'a'), 

2343 (0, 'b'), 

2344 (1, 'a'), 

2345 (1, 'b')], 

2346 ) 

2347 

2348 >>> mi[2:] 

2349 MultiIndex([(1, 'a'), 

2350 (1, 'b')], 

2351 ) 

2352 

2353 The 0 from the first level is not represented 

2354 and can be removed 

2355 

2356 >>> mi2 = mi[2:].remove_unused_levels() 

2357 >>> mi2.levels 

2358 FrozenList([[1], ['a', 'b']]) 

2359 """ 

2360 new_levels = [] 

2361 new_codes = [] 

2362 

2363 changed = False 

2364 for lev, level_codes in zip(self.levels, self.codes, strict=True): 

2365 # Since few levels are typically unused, bincount() is more 

2366 # efficient than unique() - however it only accepts positive values 

2367 # (and drops order): 

2368 uniques = np.where(np.bincount(level_codes + 1) > 0)[0] - 1 

2369 has_na = int(len(uniques) and (uniques[0] == -1)) 

2370 

2371 if len(uniques) != len(lev) + has_na: 

2372 if lev.isna().any() and len(uniques) == len(lev): 

2373 break 

2374 # We have unused levels 

2375 changed = True 

2376 

2377 # Recalculate uniques, now preserving order. 

2378 # Can easily be cythonized by exploiting the already existing 

2379 # "uniques" and stop parsing "level_codes" when all items 

2380 # are found: 

2381 uniques = algos.unique(level_codes) 

2382 if has_na: 

2383 na_idx = np.where(uniques == -1)[0] 

2384 # Just ensure that -1 is in first position: 

2385 uniques[[0, na_idx[0]]] = uniques[[na_idx[0], 0]] 

2386 

2387 # codes get mapped from uniques to 0:len(uniques) 

2388 # -1 (if present) is mapped to last position 

2389 code_mapping = np.zeros(len(lev) + has_na) 

2390 # ... and reassigned value -1: 

2391 code_mapping[uniques] = np.arange(len(uniques)) - has_na 

2392 

2393 level_codes = code_mapping[level_codes] 

2394 

2395 # new levels are simple 

2396 lev = lev.take(uniques[has_na:]) 

2397 

2398 new_levels.append(lev) 

2399 new_codes.append(level_codes) 

2400 

2401 result = self.view() 

2402 

2403 if changed: 

2404 result._reset_identity() 

2405 result._set_levels(new_levels, validate=False) 

2406 result._set_codes(new_codes, validate=False) 

2407 

2408 return result 

2409 

2410 # -------------------------------------------------------------------- 

2411 # Pickling Methods 

2412 

2413 def __reduce__(self): 

2414 """Necessary for making this object picklable""" 

2415 d = { 

2416 "levels": list(self.levels), 

2417 "codes": list(self.codes), 

2418 "sortorder": self.sortorder, 

2419 "names": list(self.names), 

2420 } 

2421 return ibase._new_Index, (type(self), d), None 

2422 

2423 # -------------------------------------------------------------------- 

2424 

2425 def __getitem__(self, key): 

2426 key = lib.item_from_zerodim(key) 

2427 if is_scalar(key): 

2428 key = com.cast_scalar_indexer(key) 

2429 

2430 retval = [] 

2431 for lev, level_codes in zip(self.levels, self.codes, strict=True): 

2432 if level_codes[key] == -1: 

2433 retval.append(np.nan) 

2434 else: 

2435 retval.append(lev[level_codes[key]]) 

2436 

2437 return tuple(retval) 

2438 else: 

2439 # in general cannot be sure whether the result will be sorted 

2440 sortorder = None 

2441 if com.is_bool_indexer(key): 

2442 key = np.asarray(key, dtype=bool) 

2443 sortorder = self.sortorder 

2444 elif isinstance(key, slice): 

2445 if key.step is None or key.step > 0: 

2446 sortorder = self.sortorder 

2447 elif isinstance(key, Index): 

2448 key = np.asarray(key) 

2449 

2450 new_codes = [level_codes[key] for level_codes in self.codes] 

2451 

2452 return MultiIndex( 

2453 levels=self.levels, 

2454 codes=new_codes, 

2455 names=self.names, 

2456 sortorder=sortorder, 

2457 verify_integrity=False, 

2458 ) 

2459 

2460 def _getitem_slice(self: MultiIndex, slobj: slice) -> MultiIndex: 

2461 """ 

2462 Fastpath for __getitem__ when we know we have a slice. 

2463 """ 

2464 sortorder = None 

2465 if slobj.step is None or slobj.step > 0: 

2466 sortorder = self.sortorder 

2467 

2468 new_codes = [level_codes[slobj] for level_codes in self.codes] 

2469 

2470 return type(self)( 

2471 levels=self.levels, 

2472 codes=new_codes, 

2473 names=self._names, 

2474 sortorder=sortorder, 

2475 verify_integrity=False, 

2476 ) 

2477 

2478 def take( 

2479 self: MultiIndex, 

2480 indices, 

2481 axis: Axis = 0, 

2482 allow_fill: bool = True, 

2483 fill_value=None, 

2484 **kwargs, 

2485 ) -> MultiIndex: 

2486 """ 

2487 Return a new MultiIndex of the values selected by the indices. 

2488 

2489 For internal compatibility with numpy arrays. 

2490 

2491 Parameters 

2492 ---------- 

2493 indices : array-like 

2494 Indices to be taken. 

2495 axis : {0 or 'index'}, optional 

2496 The axis over which to select values, always 0 or 'index'. 

2497 allow_fill : bool, default True 

2498 How to handle negative values in `indices`. 

2499 

2500 * False: negative values in `indices` indicate positional indices 

2501 from the right (the default). This is similar to 

2502 :func:`numpy.take`. 

2503 

2504 * True: negative values in `indices` indicate 

2505 missing values. These values are set to `fill_value`. Any other 

2506 other negative values raise a ``ValueError``. 

2507 

2508 fill_value : scalar, default None 

2509 If allow_fill=True and fill_value is not None, indices specified by 

2510 -1 are regarded as NA. If Index doesn't hold NA, raise ValueError. 

2511 **kwargs 

2512 Required for compatibility with numpy. 

2513 

2514 Returns 

2515 ------- 

2516 Index 

2517 An index formed of elements at the given indices. Will be the same 

2518 type as self, except for RangeIndex. 

2519 

2520 See Also 

2521 -------- 

2522 numpy.ndarray.take: Return an array formed from the 

2523 elements of a at the given indices. 

2524 

2525 Examples 

2526 -------- 

2527 >>> idx = pd.MultiIndex.from_arrays([["a", "b", "c"], [1, 2, 3]]) 

2528 >>> idx 

2529 MultiIndex([('a', 1), 

2530 ('b', 2), 

2531 ('c', 3)], 

2532 ) 

2533 >>> idx.take([2, 2, 1, 0]) 

2534 MultiIndex([('c', 3), 

2535 ('c', 3), 

2536 ('b', 2), 

2537 ('a', 1)], 

2538 ) 

2539 """ 

2540 nv.validate_take((), kwargs) 

2541 indices = ensure_platform_int(indices) 

2542 

2543 # only fill if we are passing a non-None fill_value 

2544 allow_fill = self._maybe_disallow_fill(allow_fill, fill_value, indices) 

2545 

2546 if indices.ndim == 1 and lib.is_range_indexer(indices, len(self)): 

2547 return self.copy() 

2548 

2549 na_value = -1 

2550 

2551 taken = [lab.take(indices) for lab in self.codes] 

2552 if allow_fill: 

2553 mask = indices == -1 

2554 if mask.any(): 

2555 masked = [] 

2556 for new_label in taken: 

2557 label_values = new_label 

2558 label_values[mask] = na_value 

2559 masked.append(np.asarray(label_values)) 

2560 taken = masked 

2561 

2562 return MultiIndex( 

2563 levels=self.levels, codes=taken, names=self.names, verify_integrity=False 

2564 ) 

2565 

2566 def append(self, other): 

2567 """ 

2568 Append a collection of Index options together. 

2569 

2570 The `append` method is used to combine multiple `Index` objects into a single 

2571 `Index`. This is particularly useful when dealing with multi-level indexing 

2572 (MultiIndex) where you might need to concatenate different levels of indices. 

2573 The method handles the alignment of the levels and codes of the indices being 

2574 appended to ensure consistency in the resulting `MultiIndex`. 

2575 

2576 Parameters 

2577 ---------- 

2578 other : Index or list/tuple of indices 

2579 Index or list/tuple of Index objects to be appended. 

2580 

2581 Returns 

2582 ------- 

2583 Index 

2584 The combined index. 

2585 

2586 See Also 

2587 -------- 

2588 MultiIndex: A multi-level, or hierarchical, index object for pandas objects. 

2589 Index.append : Append a collection of Index options together. 

2590 concat : Concatenate pandas objects along a particular axis. 

2591 

2592 Examples 

2593 -------- 

2594 >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"]]) 

2595 >>> mi 

2596 MultiIndex([('a', 'b')], 

2597 ) 

2598 >>> mi.append(mi) 

2599 MultiIndex([('a', 'b'), ('a', 'b')], 

2600 ) 

2601 """ 

2602 if not isinstance(other, (list, tuple)): 

2603 other = [other] 

2604 

2605 if all( 

2606 (isinstance(o, MultiIndex) and o.nlevels >= self.nlevels) for o in other 

2607 ): 

2608 codes = [] 

2609 levels = [] 

2610 names = [] 

2611 for i in range(self.nlevels): 

2612 level_values = self.levels[i] 

2613 for mi in other: 

2614 level_values = level_values.union(mi.levels[i]) 

2615 level_codes = [ 

2616 recode_for_categories( 

2617 mi.codes[i], mi.levels[i], level_values, copy=False 

2618 ) 

2619 for mi in ([self, *other]) 

2620 ] 

2621 level_name = self.names[i] 

2622 if any(mi.names[i] != level_name for mi in other): 

2623 level_name = None 

2624 codes.append(np.concatenate(level_codes)) 

2625 levels.append(level_values) 

2626 names.append(level_name) 

2627 return MultiIndex( 

2628 codes=codes, levels=levels, names=names, verify_integrity=False 

2629 ) 

2630 

2631 to_concat = (self._values, *tuple(k._values for k in other)) 

2632 new_tuples = np.concatenate(to_concat) 

2633 

2634 # if all(isinstance(x, MultiIndex) for x in other): 

2635 try: 

2636 # We only get here if other contains at least one index with tuples, 

2637 # setting names to None automatically 

2638 return MultiIndex.from_tuples(new_tuples) 

2639 except (TypeError, IndexError): 

2640 return Index(new_tuples, copy=False) 

2641 

2642 def argsort( 

2643 self, *args, na_position: NaPosition = "last", **kwargs 

2644 ) -> npt.NDArray[np.intp]: 

2645 """ 

2646 Return the integer indices that would sort the index. 

2647 

2648 Parameters 

2649 ---------- 

2650 *args 

2651 Passed to `numpy.ndarray.argsort`. 

2652 na_position : {'first' or 'last'}, default 'last' 

2653 Argument 'first' puts NaNs at the beginning, 'last' puts NaNs at 

2654 the end. 

2655 **kwargs 

2656 Passed to `numpy.ndarray.argsort`. 

2657 

2658 Returns 

2659 ------- 

2660 np.ndarray[np.intp] 

2661 Integer indices that would sort the index if used as 

2662 an indexer. 

2663 

2664 See Also 

2665 -------- 

2666 numpy.argsort : Similar method for NumPy arrays. 

2667 Index.argsort : Similar method for Index. 

2668 

2669 Examples 

2670 -------- 

2671 >>> midx = pd.MultiIndex.from_arrays([[3, 2], ["e", "c"]]) 

2672 >>> midx 

2673 MultiIndex([(3, 'e'), 

2674 (2, 'c')], 

2675 ) 

2676 

2677 >>> order = midx.argsort() 

2678 >>> order 

2679 array([1, 0]) 

2680 

2681 >>> midx[order] 

2682 MultiIndex([(2, 'c'), 

2683 (3, 'e')], 

2684 ) 

2685 

2686 >>> midx = pd.MultiIndex.from_arrays([[2, 2], [np.nan, 0]]) 

2687 >>> midx.argsort(na_position="first") 

2688 array([0, 1]) 

2689 

2690 >>> midx.argsort() 

2691 array([1, 0]) 

2692 """ 

2693 target = self._sort_levels_monotonic(raise_if_incomparable=True) 

2694 keys = [lev.codes for lev in target._get_codes_for_sorting()] 

2695 return lexsort_indexer(keys, na_position=na_position, codes_given=True) 

2696 

2697 def repeat(self, repeats: int, axis=None) -> MultiIndex: 

2698 """ 

2699 Repeat elements of a MultiIndex. 

2700 

2701 Returns a new MultiIndex where each element of the current MultiIndex 

2702 is repeated consecutively a given number of times. 

2703 

2704 Parameters 

2705 ---------- 

2706 repeats : int or array of ints 

2707 The number of repetitions for each element. This should be a 

2708 non-negative integer. Repeating 0 times will return an empty 

2709 MultiIndex. 

2710 axis : None 

2711 Must be ``None``. Has no effect but is accepted for compatibility 

2712 with numpy. 

2713 

2714 Returns 

2715 ------- 

2716 MultiIndex 

2717 Newly created MultiIndex with repeated elements. 

2718 

2719 See Also 

2720 -------- 

2721 Series.repeat : Equivalent function for Series. 

2722 numpy.repeat : Similar method for :class:`numpy.ndarray`. 

2723 

2724 Examples 

2725 -------- 

2726 >>> idx = pd.MultiIndex.from_arrays([["a", "b", "c"], [1, 2, 3]]) 

2727 >>> idx 

2728 MultiIndex([('a', 1), 

2729 ('b', 2), 

2730 ('c', 3)], 

2731 ) 

2732 >>> idx.repeat(2) 

2733 MultiIndex([('a', 1), 

2734 ('a', 1), 

2735 ('b', 2), 

2736 ('b', 2), 

2737 ('c', 3), 

2738 ('c', 3)], 

2739 ) 

2740 >>> idx.repeat([1, 2, 3]) 

2741 MultiIndex([('a', 1), 

2742 ('b', 2), 

2743 ('b', 2), 

2744 ('c', 3), 

2745 ('c', 3), 

2746 ('c', 3)], 

2747 ) 

2748 """ 

2749 nv.validate_repeat((), {"axis": axis}) 

2750 # error: Incompatible types in assignment (expression has type "ndarray", 

2751 # variable has type "int") 

2752 repeats = ensure_platform_int(repeats) # type: ignore[assignment] 

2753 return MultiIndex( 

2754 levels=self.levels, 

2755 codes=[ 

2756 level_codes.view(np.ndarray).astype(np.intp, copy=False).repeat(repeats) 

2757 for level_codes in self.codes 

2758 ], 

2759 names=self.names, 

2760 sortorder=self.sortorder, 

2761 verify_integrity=False, 

2762 ) 

2763 

2764 # error: Signature of "drop" incompatible with supertype "Index" 

2765 def drop( # type: ignore[override] 

2766 self, 

2767 codes, 

2768 level: Index | np.ndarray | Iterable[Hashable] | None = None, 

2769 errors: IgnoreRaise = "raise", 

2770 ) -> MultiIndex: 

2771 """ 

2772 Make a new :class:`pandas.MultiIndex` with the passed list of codes deleted. 

2773 

2774 This method allows for the removal of specified labels from a MultiIndex. 

2775 The labels to be removed can be provided as a list of tuples if no level 

2776 is specified, or as a list of labels from a specific level if the level 

2777 parameter is provided. This can be useful for refining the structure of a 

2778 MultiIndex to fit specific requirements. 

2779 

2780 Parameters 

2781 ---------- 

2782 codes : array-like 

2783 Must be a list of tuples when ``level`` is not specified. 

2784 level : int or level name, default None 

2785 Level from which the labels will be dropped. 

2786 errors : str, default 'raise' 

2787 If 'ignore', suppress error and existing labels are dropped. 

2788 

2789 Returns 

2790 ------- 

2791 MultiIndex 

2792 A new MultiIndex with the specified labels removed. 

2793 

2794 See Also 

2795 -------- 

2796 MultiIndex.remove_unused_levels : Create new MultiIndex from current that 

2797 removes unused levels. 

2798 MultiIndex.reorder_levels : Rearrange levels using input order. 

2799 MultiIndex.rename : Rename levels in a MultiIndex. 

2800 

2801 Examples 

2802 -------- 

2803 >>> idx = pd.MultiIndex.from_product( 

2804 ... [(0, 1, 2), ("green", "purple")], names=["number", "color"] 

2805 ... ) 

2806 >>> idx 

2807 MultiIndex([(0, 'green'), 

2808 (0, 'purple'), 

2809 (1, 'green'), 

2810 (1, 'purple'), 

2811 (2, 'green'), 

2812 (2, 'purple')], 

2813 names=['number', 'color']) 

2814 >>> idx.drop([(1, "green"), (2, "purple")]) 

2815 MultiIndex([(0, 'green'), 

2816 (0, 'purple'), 

2817 (1, 'purple'), 

2818 (2, 'green')], 

2819 names=['number', 'color']) 

2820 

2821 We can also drop from a specific level. 

2822 

2823 >>> idx.drop("green", level="color") 

2824 MultiIndex([(0, 'purple'), 

2825 (1, 'purple'), 

2826 (2, 'purple')], 

2827 names=['number', 'color']) 

2828 

2829 >>> idx.drop([1, 2], level=0) 

2830 MultiIndex([(0, 'green'), 

2831 (0, 'purple')], 

2832 names=['number', 'color']) 

2833 """ 

2834 if level is not None: 

2835 return self._drop_from_level(codes, level, errors) 

2836 

2837 if not isinstance(codes, (np.ndarray, Index)): 

2838 try: 

2839 codes = com.index_labels_to_array(codes, dtype=np.dtype("object")) 

2840 except ValueError: 

2841 pass 

2842 

2843 inds = [] 

2844 for level_codes in codes: 

2845 try: 

2846 loc = self.get_loc(level_codes) 

2847 # get_loc returns either an integer, a slice, or a boolean 

2848 # mask 

2849 if isinstance(loc, int): 

2850 inds.append(loc) 

2851 elif isinstance(loc, slice): 

2852 step = loc.step if loc.step is not None else 1 

2853 inds.extend(range(loc.start, loc.stop, step)) 

2854 elif com.is_bool_indexer(loc): 

2855 if get_option("performance_warnings") and self._lexsort_depth == 0: 

2856 warnings.warn( 

2857 "dropping on a non-lexsorted multi-index " 

2858 "without a level parameter may impact performance.", 

2859 PerformanceWarning, 

2860 stacklevel=find_stack_level(), 

2861 ) 

2862 loc = loc.nonzero()[0] 

2863 inds.extend(loc) 

2864 else: 

2865 msg = f"unsupported indexer of type {type(loc)}" 

2866 raise AssertionError(msg) 

2867 except KeyError: 

2868 if errors != "ignore": 

2869 raise 

2870 

2871 return self.delete(inds) 

2872 

2873 def _drop_from_level( 

2874 self, codes, level, errors: IgnoreRaise = "raise" 

2875 ) -> MultiIndex: 

2876 codes = com.index_labels_to_array(codes) 

2877 i = self._get_level_number(level) 

2878 index = self.levels[i] 

2879 values = index.get_indexer(codes) 

2880 # If nan should be dropped it will equal -1 here. We have to check which values 

2881 # are not nan and equal -1, this means they are missing in the index 

2882 nan_codes = isna(codes) 

2883 values[(np.equal(nan_codes, False)) & (values == -1)] = -2 

2884 if index.shape[0] == self.shape[0]: 

2885 values[np.equal(nan_codes, True)] = -2 

2886 

2887 not_found = codes[values == -2] 

2888 if len(not_found) != 0 and errors != "ignore": 

2889 raise KeyError(f"labels {not_found} not found in level") 

2890 mask = ~algos.isin(self.codes[i], values) 

2891 

2892 return self[mask] 

2893 

2894 def swaplevel(self, i=-2, j=-1) -> MultiIndex: 

2895 """ 

2896 Swap level i with level j. 

2897 

2898 Calling this method does not change the ordering of the values. 

2899 

2900 Default is to swap the last two levels of the MultiIndex. 

2901 

2902 Parameters 

2903 ---------- 

2904 i : int, str, default -2 

2905 First level of index to be swapped. Can pass level name as string. 

2906 Type of parameters can be mixed. If i is a negative int, the first 

2907 level is indexed relative to the end of the MultiIndex. 

2908 j : int, str, default -1 

2909 Second level of index to be swapped. Can pass level name as string. 

2910 Type of parameters can be mixed. If j is a negative int, the second 

2911 level is indexed relative to the end of the MultiIndex. 

2912 

2913 Returns 

2914 ------- 

2915 MultiIndex 

2916 A new MultiIndex. 

2917 

2918 See Also 

2919 -------- 

2920 Series.swaplevel : Swap levels i and j in a MultiIndex. 

2921 DataFrame.swaplevel : Swap levels i and j in a MultiIndex on a 

2922 particular axis. 

2923 

2924 Examples 

2925 -------- 

2926 >>> mi = pd.MultiIndex( 

2927 ... levels=[["a", "b"], ["bb", "aa"], ["aaa", "bbb"]], 

2928 ... codes=[[0, 0, 1, 1], [0, 1, 0, 1], [1, 0, 1, 0]], 

2929 ... ) 

2930 >>> mi 

2931 MultiIndex([('a', 'bb', 'bbb'), 

2932 ('a', 'aa', 'aaa'), 

2933 ('b', 'bb', 'bbb'), 

2934 ('b', 'aa', 'aaa')], 

2935 ) 

2936 >>> mi.swaplevel() 

2937 MultiIndex([('a', 'bbb', 'bb'), 

2938 ('a', 'aaa', 'aa'), 

2939 ('b', 'bbb', 'bb'), 

2940 ('b', 'aaa', 'aa')], 

2941 ) 

2942 >>> mi.swaplevel(0) 

2943 MultiIndex([('bbb', 'bb', 'a'), 

2944 ('aaa', 'aa', 'a'), 

2945 ('bbb', 'bb', 'b'), 

2946 ('aaa', 'aa', 'b')], 

2947 ) 

2948 >>> mi.swaplevel(0, 1) 

2949 MultiIndex([('bb', 'a', 'bbb'), 

2950 ('aa', 'a', 'aaa'), 

2951 ('bb', 'b', 'bbb'), 

2952 ('aa', 'b', 'aaa')], 

2953 ) 

2954 """ 

2955 new_levels = list(self.levels) 

2956 new_codes = list(self.codes) 

2957 new_names = list(self.names) 

2958 

2959 i = self._get_level_number(i) 

2960 j = self._get_level_number(j) 

2961 

2962 new_levels[i], new_levels[j] = new_levels[j], new_levels[i] 

2963 new_codes[i], new_codes[j] = new_codes[j], new_codes[i] 

2964 new_names[i], new_names[j] = new_names[j], new_names[i] 

2965 

2966 return MultiIndex( 

2967 levels=new_levels, codes=new_codes, names=new_names, verify_integrity=False 

2968 ) 

2969 

2970 def reorder_levels(self, order) -> MultiIndex: 

2971 """ 

2972 Rearrange levels using input order. May not drop or duplicate levels. 

2973 

2974 `reorder_levels` is useful when you need to change the order of levels in 

2975 a MultiIndex, such as when reordering levels for hierarchical indexing. It 

2976 maintains the integrity of the MultiIndex, ensuring that all existing levels 

2977 are present and no levels are duplicated. This method is helpful for aligning 

2978 the index structure with other data structures or for optimizing the order 

2979 for specific data operations. 

2980 

2981 Parameters 

2982 ---------- 

2983 order : list of int or list of str 

2984 List representing new level order. Reference level by number 

2985 (position) or by key (label). 

2986 

2987 Returns 

2988 ------- 

2989 MultiIndex 

2990 A new MultiIndex with levels rearranged according to the specified order. 

2991 

2992 See Also 

2993 -------- 

2994 MultiIndex.swaplevel : Swap two levels of the MultiIndex. 

2995 MultiIndex.set_names : Set names for the MultiIndex levels. 

2996 DataFrame.reorder_levels : Reorder levels in a DataFrame with a MultiIndex. 

2997 

2998 Examples 

2999 -------- 

3000 >>> mi = pd.MultiIndex.from_arrays([[1, 2], [3, 4]], names=["x", "y"]) 

3001 >>> mi 

3002 MultiIndex([(1, 3), 

3003 (2, 4)], 

3004 names=['x', 'y']) 

3005 

3006 >>> mi.reorder_levels(order=[1, 0]) 

3007 MultiIndex([(3, 1), 

3008 (4, 2)], 

3009 names=['y', 'x']) 

3010 

3011 >>> mi.reorder_levels(order=["y", "x"]) 

3012 MultiIndex([(3, 1), 

3013 (4, 2)], 

3014 names=['y', 'x']) 

3015 """ 

3016 order = [self._get_level_number(i) for i in order] 

3017 result = self._reorder_ilevels(order) 

3018 return result 

3019 

3020 def _reorder_ilevels(self, order) -> MultiIndex: 

3021 if len(order) != self.nlevels: 

3022 raise AssertionError( 

3023 f"Length of order must be same as number of levels ({self.nlevels}), " 

3024 f"got {len(order)}" 

3025 ) 

3026 new_levels = [self.levels[i] for i in order] 

3027 new_codes = [self.codes[i] for i in order] 

3028 new_names = [self.names[i] for i in order] 

3029 

3030 return MultiIndex( 

3031 levels=new_levels, codes=new_codes, names=new_names, verify_integrity=False 

3032 ) 

3033 

3034 def _recode_for_new_levels( 

3035 self, new_levels, *, copy: bool 

3036 ) -> Generator[np.ndarray]: 

3037 if len(new_levels) > self.nlevels: 

3038 raise AssertionError( 

3039 f"Length of new_levels ({len(new_levels)}) " 

3040 f"must be <= self.nlevels ({self.nlevels})" 

3041 ) 

3042 for i in range(len(new_levels)): 

3043 yield recode_for_categories( 

3044 self.codes[i], self.levels[i], new_levels[i], copy=copy 

3045 ) 

3046 

3047 def _get_codes_for_sorting(self) -> list[Categorical]: 

3048 """ 

3049 we are categorizing our codes by using the 

3050 available categories (all, not just observed) 

3051 excluding any missing ones (-1); this is in preparation 

3052 for sorting, where we need to disambiguate that -1 is not 

3053 a valid valid 

3054 """ 

3055 

3056 def cats(level_codes: np.ndarray) -> np.ndarray: 

3057 return np.arange( 

3058 level_codes.max() + 1 if len(level_codes) else 0, 

3059 dtype=level_codes.dtype, 

3060 ) 

3061 

3062 return [ 

3063 Categorical.from_codes(level_codes, cats(level_codes), True, validate=False) 

3064 for level_codes in self.codes 

3065 ] 

3066 

3067 def sortlevel( 

3068 self, 

3069 level: IndexLabel = 0, 

3070 ascending: bool | list[bool] = True, 

3071 sort_remaining: bool = True, 

3072 na_position: str = "first", 

3073 ) -> tuple[MultiIndex, npt.NDArray[np.intp]]: 

3074 """ 

3075 Sort MultiIndex at the requested level. 

3076 

3077 This method is useful when dealing with MultiIndex objects, allowing for 

3078 sorting at a specific level of the index. The function preserves the 

3079 relative ordering of data within the same level while sorting 

3080 the overall MultiIndex. The method provides flexibility with the `ascending` 

3081 parameter to define the sort order and with the `sort_remaining` parameter to 

3082 control whether the remaining levels should also be sorted. Sorting a 

3083 MultiIndex can be crucial when performing operations that require ordered 

3084 indices, such as grouping or merging datasets. The `na_position` argument is 

3085 important in handling missing values consistently across different levels. 

3086 

3087 Parameters 

3088 ---------- 

3089 level : list-like, int or str, default 0 

3090 If a string is given, must be a name of the level. 

3091 If list-like must be names or ints of levels. 

3092 ascending : bool, default True 

3093 False to sort in descending order. 

3094 Can also be a list to specify a directed ordering. 

3095 sort_remaining : bool, default True 

3096 If True, sorts by the remaining levels after sorting by the specified 

3097 `level`. 

3098 na_position : {'first' or 'last'}, default 'first' 

3099 Argument 'first' puts NaNs at the beginning, 'last' puts NaNs at 

3100 the end. 

3101 

3102 .. versionadded:: 2.1.0 

3103 

3104 Returns 

3105 ------- 

3106 sorted_index : pd.MultiIndex 

3107 Resulting index. 

3108 indexer : np.ndarray[np.intp] 

3109 Indices of output values in original index. 

3110 

3111 See Also 

3112 -------- 

3113 MultiIndex : A multi-level, or hierarchical, index object for pandas objects. 

3114 Index.sort_values : Sort Index values. 

3115 DataFrame.sort_index : Sort DataFrame by the index. 

3116 Series.sort_index : Sort Series by the index. 

3117 

3118 Examples 

3119 -------- 

3120 >>> mi = pd.MultiIndex.from_arrays([[0, 0], [2, 1]]) 

3121 >>> mi 

3122 MultiIndex([(0, 2), 

3123 (0, 1)], 

3124 ) 

3125 

3126 >>> mi.sortlevel() 

3127 (MultiIndex([(0, 1), 

3128 (0, 2)], 

3129 ), array([1, 0])) 

3130 

3131 >>> mi.sortlevel(sort_remaining=False) 

3132 (MultiIndex([(0, 2), 

3133 (0, 1)], 

3134 ), array([0, 1])) 

3135 

3136 >>> mi.sortlevel(1) 

3137 (MultiIndex([(0, 1), 

3138 (0, 2)], 

3139 ), array([1, 0])) 

3140 

3141 >>> mi.sortlevel(1, ascending=False) 

3142 (MultiIndex([(0, 2), 

3143 (0, 1)], 

3144 ), array([0, 1])) 

3145 """ 

3146 if not is_list_like(level): 

3147 level = [level] 

3148 # error: Item "Hashable" of "Union[Hashable, Sequence[Hashable]]" has 

3149 # no attribute "__iter__" (not iterable) 

3150 level = [ 

3151 self._get_level_number(lev) 

3152 for lev in level # type: ignore[union-attr] 

3153 ] 

3154 sortorder = None 

3155 

3156 codes = [self.codes[lev] for lev in level] 

3157 # we have a directed ordering via ascending 

3158 if isinstance(ascending, list): 

3159 if not len(level) == len(ascending): 

3160 raise ValueError("level must have same length as ascending") 

3161 elif sort_remaining: 

3162 codes.extend( 

3163 [self.codes[lev] for lev in range(len(self.levels)) if lev not in level] 

3164 ) 

3165 else: 

3166 sortorder = level[0] 

3167 

3168 indexer = lexsort_indexer( 

3169 codes, orders=ascending, na_position=na_position, codes_given=True 

3170 ) 

3171 

3172 indexer = ensure_platform_int(indexer) 

3173 new_codes = [level_codes.take(indexer) for level_codes in self.codes] 

3174 

3175 new_index = MultiIndex( 

3176 codes=new_codes, 

3177 levels=self.levels, 

3178 names=self.names, 

3179 sortorder=sortorder, 

3180 verify_integrity=False, 

3181 ) 

3182 

3183 return new_index, indexer 

3184 

3185 def _wrap_reindex_result(self, target, indexer, preserve_names: bool): 

3186 if not isinstance(target, MultiIndex): 

3187 if indexer is None: 

3188 target = self 

3189 elif (indexer >= 0).all(): 

3190 target = self.take(indexer) 

3191 else: 

3192 try: 

3193 target = MultiIndex.from_tuples(target) 

3194 except TypeError: 

3195 # not all tuples, see test_constructor_dict_multiindex_reindex_flat 

3196 return target 

3197 

3198 target = self._maybe_preserve_names(target, preserve_names) 

3199 return target 

3200 

3201 def _maybe_preserve_names(self, target: IndexT, preserve_names: bool) -> IndexT: 

3202 if ( 

3203 preserve_names 

3204 and target.nlevels == self.nlevels 

3205 and target.names != self.names 

3206 ): 

3207 target = target.copy(deep=False) 

3208 target.names = self.names 

3209 return target 

3210 

3211 # -------------------------------------------------------------------- 

3212 # Indexing Methods 

3213 

3214 def _check_indexing_error(self, key) -> None: 

3215 if not is_hashable(key) or is_iterator(key): 

3216 # We allow tuples if they are hashable, whereas other Index 

3217 # subclasses require scalar. 

3218 # We have to explicitly exclude generators, as these are hashable. 

3219 raise InvalidIndexError(key) 

3220 

3221 @cache_readonly 

3222 def _should_fallback_to_positional(self) -> bool: 

3223 """ 

3224 Should integer key(s) be treated as positional? 

3225 """ 

3226 # GH#33355 

3227 return self.levels[0]._should_fallback_to_positional 

3228 

3229 def _get_indexer_strict( 

3230 self, key, axis_name: str 

3231 ) -> tuple[Index, npt.NDArray[np.intp]]: 

3232 keyarr = key 

3233 if not isinstance(keyarr, Index): 

3234 keyarr = com.asarray_tuplesafe(keyarr) 

3235 

3236 if len(keyarr) and not isinstance(keyarr[0], tuple): 

3237 indexer = self._get_indexer_level_0(keyarr) 

3238 

3239 self._raise_if_missing(key, indexer, axis_name) 

3240 return self[indexer], indexer 

3241 

3242 return super()._get_indexer_strict(key, axis_name) 

3243 

3244 def _raise_if_missing(self, key, indexer, axis_name: str) -> None: 

3245 keyarr = key 

3246 if not isinstance(key, Index): 

3247 keyarr = com.asarray_tuplesafe(key) 

3248 

3249 if len(keyarr) and not isinstance(keyarr[0], tuple): 

3250 # i.e. same condition for special case in MultiIndex._get_indexer_strict 

3251 

3252 mask = indexer == -1 

3253 if mask.any(): 

3254 check = self.levels[0].get_indexer(keyarr) 

3255 cmask = check == -1 

3256 if cmask.any(): 

3257 raise KeyError(f"{keyarr[cmask]} not in index") 

3258 # We get here when levels still contain values which are not 

3259 # actually in Index anymore 

3260 raise KeyError(f"{keyarr} not in index") 

3261 else: 

3262 return super()._raise_if_missing(key, indexer, axis_name) 

3263 

3264 def _get_indexer_level_0(self, target) -> npt.NDArray[np.intp]: 

3265 """ 

3266 Optimized equivalent to `self.get_level_values(0).get_indexer_for(target)`. 

3267 """ 

3268 lev = self.levels[0] 

3269 codes = self._codes[0] 

3270 cat = Categorical.from_codes(codes=codes, categories=lev, validate=False) 

3271 ci = Index(cat, copy=False) 

3272 return ci.get_indexer_for(target) 

3273 

3274 def get_slice_bound( 

3275 self, 

3276 label: Hashable | Sequence[Hashable], 

3277 side: Literal["left", "right"], 

3278 ) -> int: 

3279 """ 

3280 For an ordered MultiIndex, compute slice bound 

3281 that corresponds to given label. 

3282 

3283 Returns leftmost (one-past-the-rightmost if `side=='right') position 

3284 of given label. 

3285 

3286 Parameters 

3287 ---------- 

3288 label : object or tuple of objects 

3289 side : {'left', 'right'} 

3290 

3291 Returns 

3292 ------- 

3293 int 

3294 Index of label. 

3295 

3296 Notes 

3297 ----- 

3298 This method only works if level 0 index of the MultiIndex is lexsorted. 

3299 

3300 Examples 

3301 -------- 

3302 >>> mi = pd.MultiIndex.from_arrays([list("abbc"), list("gefd")]) 

3303 

3304 Get the locations from the leftmost 'b' in the first level 

3305 until the end of the multiindex: 

3306 

3307 >>> mi.get_slice_bound("b", side="left") 

3308 1 

3309 

3310 Like above, but if you get the locations from the rightmost 

3311 'b' in the first level and 'f' in the second level: 

3312 

3313 >>> mi.get_slice_bound(("b", "f"), side="right") 

3314 3 

3315 

3316 See Also 

3317 -------- 

3318 MultiIndex.get_loc : Get location for a label or a tuple of labels. 

3319 MultiIndex.get_locs : Get location for a label/slice/list/mask or a 

3320 sequence of such. 

3321 """ 

3322 if not isinstance(label, tuple): 

3323 label = (label,) 

3324 result = self._partial_tup_index(label, side=side) 

3325 result = maybe_unbox_numpy_scalar(result) 

3326 return result 

3327 

3328 def slice_locs(self, start=None, end=None, step=None) -> tuple[int, int]: 

3329 """ 

3330 For an ordered MultiIndex, compute the slice locations for input 

3331 labels. 

3332 

3333 The input labels can be tuples representing partial levels, e.g. for a 

3334 MultiIndex with 3 levels, you can pass a single value (corresponding to 

3335 the first level), or a 1-, 2-, or 3-tuple. 

3336 

3337 Parameters 

3338 ---------- 

3339 start : label or tuple, default None 

3340 If None, defaults to the beginning 

3341 end : label or tuple 

3342 If None, defaults to the end 

3343 step : int or None 

3344 Slice step 

3345 

3346 Returns 

3347 ------- 

3348 (start, end) : (int, int) 

3349 

3350 Notes 

3351 ----- 

3352 This method only works if the MultiIndex is properly lexsorted. So, 

3353 if only the first 2 levels of a 3-level MultiIndex are lexsorted, 

3354 you can only pass two levels to ``.slice_locs``. 

3355 

3356 Examples 

3357 -------- 

3358 >>> mi = pd.MultiIndex.from_arrays( 

3359 ... [list("abbd"), list("deff")], names=["A", "B"] 

3360 ... ) 

3361 

3362 Get the slice locations from the beginning of 'b' in the first level 

3363 until the end of the multiindex: 

3364 

3365 >>> mi.slice_locs(start="b") 

3366 (1, 4) 

3367 

3368 Like above, but stop at the end of 'b' in the first level and 'f' in 

3369 the second level: 

3370 

3371 >>> mi.slice_locs(start="b", end=("b", "f")) 

3372 (1, 3) 

3373 

3374 See Also 

3375 -------- 

3376 MultiIndex.get_loc : Get location for a label or a tuple of labels. 

3377 MultiIndex.get_locs : Get location for a label/slice/list/mask or a 

3378 sequence of such. 

3379 """ 

3380 # This function adds nothing to its parent implementation (the magic 

3381 # happens in get_slice_bound method), but it adds meaningful doc. 

3382 return super().slice_locs(start, end, step) 

3383 

3384 def _partial_tup_index(self, tup: tuple, side: Literal["left", "right"] = "left"): 

3385 if len(tup) > self._lexsort_depth: 

3386 raise UnsortedIndexError( 

3387 f"Key length ({len(tup)}) was greater than MultiIndex lexsort depth " 

3388 f"({self._lexsort_depth})" 

3389 ) 

3390 

3391 n = len(tup) 

3392 start, end = 0, len(self) 

3393 zipped = zip(tup, self.levels, self.codes, strict=True) 

3394 for k, (lab, lev, level_codes) in enumerate(zipped): 

3395 section = level_codes[start:end] 

3396 

3397 loc: npt.NDArray[np.intp] | np.intp | int 

3398 if lab not in lev and not isna(lab): 

3399 # short circuit 

3400 try: 

3401 loc = algos.searchsorted(lev, lab, side=side) 

3402 except TypeError as err: 

3403 # non-comparable e.g. test_slice_locs_with_type_mismatch 

3404 raise TypeError(f"Level type mismatch: {lab}") from err 

3405 if not is_integer(loc): 

3406 # non-comparable level, e.g. test_groupby_example 

3407 raise TypeError(f"Level type mismatch: {lab}") 

3408 if side == "right" and loc >= 0: 

3409 loc -= 1 

3410 return start + algos.searchsorted(section, loc, side=side) 

3411 

3412 idx = self._get_loc_single_level_index(lev, lab) 

3413 if isinstance(idx, slice) and k < n - 1: 

3414 # Get start and end value from slice, necessary when a non-integer 

3415 # interval is given as input GH#37707 

3416 start = idx.start 

3417 end = idx.stop 

3418 elif k < n - 1: 

3419 # error: Incompatible types in assignment (expression has type 

3420 # "Union[ndarray[Any, dtype[signedinteger[Any]]] 

3421 end = start + algos.searchsorted( # type: ignore[assignment] 

3422 section, idx, side="right" 

3423 ) 

3424 # error: Incompatible types in assignment (expression has type 

3425 # "Union[ndarray[Any, dtype[signedinteger[Any]]] 

3426 start = start + algos.searchsorted( # type: ignore[assignment] 

3427 section, idx, side="left" 

3428 ) 

3429 elif isinstance(idx, slice): 

3430 idx = idx.start 

3431 return start + algos.searchsorted(section, idx, side=side) 

3432 else: 

3433 return start + algos.searchsorted(section, idx, side=side) 

3434 

3435 def _get_loc_single_level_index(self, level_index: Index, key: Hashable) -> int: 

3436 """ 

3437 If key is NA value, location of index unify as -1. 

3438 

3439 Parameters 

3440 ---------- 

3441 level_index: Index 

3442 key : label 

3443 

3444 Returns 

3445 ------- 

3446 loc : int 

3447 If key is NA value, loc is -1 

3448 Else, location of key in index. 

3449 

3450 See Also 

3451 -------- 

3452 Index.get_loc : The get_loc method for (single-level) index. 

3453 """ 

3454 if is_scalar(key) and isna(key): 

3455 # TODO: need is_valid_na_for_dtype(key, level_index.dtype) 

3456 return -1 

3457 else: 

3458 return level_index.get_loc(key) 

3459 

3460 def get_loc(self, key): 

3461 """ 

3462 Get location for a label or a tuple of labels. The location is returned \ 

3463 as an integer/slice or boolean mask. 

3464 

3465 This method returns the integer location, slice object, or boolean mask 

3466 corresponding to the specified key, which can be a single label or a tuple 

3467 of labels. The key represents a position in the MultiIndex, and the location 

3468 indicates where the key is found within the index. 

3469 

3470 Parameters 

3471 ---------- 

3472 key : label or tuple of labels (one for each level) 

3473 A label or tuple of labels that correspond to the levels of the MultiIndex. 

3474 The key must match the structure of the MultiIndex. 

3475 

3476 Returns 

3477 ------- 

3478 int, slice object or boolean mask 

3479 If the key is past the lexsort depth, the return may be a 

3480 boolean mask array, otherwise it is always a slice or int. 

3481 

3482 See Also 

3483 -------- 

3484 Index.get_loc : The get_loc method for (single-level) index. 

3485 MultiIndex.slice_locs : Get slice location given start label(s) and 

3486 end label(s). 

3487 MultiIndex.get_locs : Get location for a label/slice/list/mask or a 

3488 sequence of such. 

3489 

3490 Notes 

3491 ----- 

3492 The key cannot be a slice, list of same-level labels, a boolean mask, 

3493 or a sequence of such. If you want to use those, use 

3494 :meth:`MultiIndex.get_locs` instead. 

3495 

3496 Examples 

3497 -------- 

3498 >>> mi = pd.MultiIndex.from_arrays([list("abb"), list("def")]) 

3499 

3500 >>> mi.get_loc("b") 

3501 slice(1, 3, None) 

3502 

3503 >>> mi.get_loc(("b", "e")) 

3504 1 

3505 """ 

3506 self._check_indexing_error(key) 

3507 

3508 def _maybe_to_slice(loc): 

3509 """convert integer indexer to boolean mask or slice if possible""" 

3510 if not isinstance(loc, np.ndarray) or loc.dtype != np.intp: 

3511 return loc 

3512 

3513 loc = lib.maybe_indices_to_slice(loc, len(self)) 

3514 if isinstance(loc, slice): 

3515 return loc 

3516 

3517 mask = np.empty(len(self), dtype="bool") 

3518 mask.fill(False) 

3519 mask[loc] = True 

3520 return mask 

3521 

3522 if not isinstance(key, tuple): 

3523 loc = self._get_level_indexer(key, level=0) 

3524 return _maybe_to_slice(loc) 

3525 

3526 keylen = len(key) 

3527 if self.nlevels < keylen: 

3528 raise KeyError( 

3529 f"Key length ({keylen}) exceeds index depth ({self.nlevels})" 

3530 ) 

3531 

3532 if keylen == self.nlevels and self.is_unique: 

3533 # TODO: what if we have an IntervalIndex level? 

3534 # i.e. do we need _index_as_unique on that level? 

3535 try: 

3536 return self._engine.get_loc(key) 

3537 except KeyError as err: 

3538 raise KeyError(key) from err 

3539 except TypeError: 

3540 # e.g. test_partial_slicing_with_multiindex partial string slicing 

3541 loc, _ = self.get_loc_level(key, range(self.nlevels)) 

3542 return loc 

3543 

3544 # -- partial selection or non-unique index 

3545 # break the key into 2 parts based on the lexsort_depth of the index; 

3546 # the first part returns a continuous slice of the index; the 2nd part 

3547 # needs linear search within the slice 

3548 i = self._lexsort_depth 

3549 lead_key, follow_key = key[:i], key[i:] 

3550 

3551 if not lead_key: 

3552 start = 0 

3553 stop = len(self) 

3554 else: 

3555 try: 

3556 start, stop = self.slice_locs(lead_key, lead_key) 

3557 except TypeError as err: 

3558 # e.g. test_groupby_example key = ((0, 0, 1, 2), "new_col") 

3559 # when self has 5 integer levels 

3560 raise KeyError(key) from err 

3561 

3562 if start == stop: 

3563 raise KeyError(key) 

3564 

3565 if not follow_key: 

3566 return slice(start, stop) 

3567 

3568 if get_option("performance_warnings"): 

3569 warnings.warn( 

3570 "indexing past lexsort depth may impact performance.", 

3571 PerformanceWarning, 

3572 stacklevel=find_stack_level(), 

3573 ) 

3574 

3575 loc = np.arange(start, stop, dtype=np.intp) 

3576 

3577 for i, k in enumerate(follow_key, len(lead_key)): 

3578 mask = self.codes[i][loc] == self._get_loc_single_level_index( 

3579 self.levels[i], k 

3580 ) 

3581 if not mask.all(): 

3582 loc = loc[mask] 

3583 if not len(loc): 

3584 raise KeyError(key) 

3585 

3586 return _maybe_to_slice(loc) if len(loc) != stop - start else slice(start, stop) 

3587 

3588 def get_loc_level(self, key, level: IndexLabel = 0, drop_level: bool = True): 

3589 """ 

3590 Get location and sliced index for requested label(s)/level(s). 

3591 

3592 The `get_loc_level` method is a more advanced form of `get_loc`, allowing 

3593 users to specify not just a label or sequence of labels, but also the level(s) 

3594 in which to search. This method is useful when you need to isolate particular 

3595 sections of a MultiIndex, either for further analysis or for slicing and 

3596 dicing the data. The method provides flexibility in terms of maintaining 

3597 or dropping levels from the resulting index based on the `drop_level` 

3598 parameter. 

3599 

3600 Parameters 

3601 ---------- 

3602 key : label or sequence of labels 

3603 The label(s) for which to get the location. 

3604 level : int/level name or list thereof, optional 

3605 The level(s) in the MultiIndex to consider. If not provided, defaults 

3606 to the first level. 

3607 drop_level : bool, default True 

3608 If ``False``, the resulting index will not drop any level. 

3609 

3610 Returns 

3611 ------- 

3612 tuple 

3613 A 2-tuple where the elements : 

3614 

3615 Element 0: int, slice object or boolean array. 

3616 

3617 Element 1: The resulting sliced multiindex/index. If the key 

3618 contains all levels, this will be ``None``. 

3619 

3620 See Also 

3621 -------- 

3622 MultiIndex.get_loc : Get location for a label or a tuple of labels. 

3623 MultiIndex.get_locs : Get location for a label/slice/list/mask or a 

3624 sequence of such. 

3625 

3626 Examples 

3627 -------- 

3628 >>> mi = pd.MultiIndex.from_arrays([list("abb"), list("def")], names=["A", "B"]) 

3629 

3630 >>> mi.get_loc_level("b") 

3631 (slice(1, 3, None), Index(['e', 'f'], dtype='str', name='B')) 

3632 

3633 >>> mi.get_loc_level("e", level="B") 

3634 (array([False, True, False]), Index(['b'], dtype='str', name='A')) 

3635 

3636 >>> mi.get_loc_level(["b", "e"]) 

3637 (1, None) 

3638 """ 

3639 if not isinstance(level, (range, list, tuple)): 

3640 level = self._get_level_number(level) 

3641 else: 

3642 level = [self._get_level_number(lev) for lev in level] 

3643 

3644 loc, mi = self._get_loc_level(key, level=level) 

3645 if not drop_level: 

3646 if lib.is_integer(loc): 

3647 # Slice index must be an integer or None 

3648 mi = self[loc : loc + 1] 

3649 else: 

3650 mi = self[loc] 

3651 return loc, mi 

3652 

3653 def _get_loc_level(self, key, level: int | list[int] = 0): 

3654 """ 

3655 get_loc_level but with `level` known to be positional, not name-based. 

3656 """ 

3657 

3658 # different name to distinguish from maybe_droplevels 

3659 def maybe_mi_droplevels(indexer, levels): 

3660 """ 

3661 If level does not exist or all levels were dropped, the exception 

3662 has to be handled outside. 

3663 """ 

3664 new_index = self[indexer] 

3665 

3666 for i in sorted(levels, reverse=True): 

3667 new_index = new_index._drop_level_numbers([i]) 

3668 

3669 return new_index 

3670 

3671 if isinstance(level, (tuple, list)): 

3672 if len(key) != len(level): 

3673 raise AssertionError( 

3674 "Key for location must have same length as number of levels" 

3675 ) 

3676 result = None 

3677 for lev, k in zip(level, key, strict=True): 

3678 loc, new_index = self._get_loc_level(k, level=lev) 

3679 if isinstance(loc, slice): 

3680 mask = np.zeros(len(self), dtype=bool) 

3681 mask[loc] = True 

3682 loc = mask 

3683 result = loc if result is None else result & loc 

3684 

3685 try: 

3686 # FIXME: we should be only dropping levels on which we are 

3687 # scalar-indexing 

3688 mi = maybe_mi_droplevels(result, level) 

3689 except ValueError: 

3690 # droplevel failed because we tried to drop all levels, 

3691 # i.e. len(level) == self.nlevels 

3692 mi = self[result] 

3693 

3694 return result, mi 

3695 

3696 # kludge for #1796 

3697 if isinstance(key, list): 

3698 key = tuple(key) 

3699 

3700 if isinstance(key, tuple) and level == 0: 

3701 try: 

3702 # Check if this tuple is a single key in our first level 

3703 if key in self.levels[0]: 

3704 indexer = self._get_level_indexer(key, level=level) 

3705 new_index = maybe_mi_droplevels(indexer, [0]) 

3706 return indexer, new_index 

3707 except (TypeError, InvalidIndexError): 

3708 pass 

3709 

3710 if not any(isinstance(k, slice) for k in key): 

3711 if len(key) == self.nlevels and self.is_unique: 

3712 # Complete key in unique index -> standard get_loc 

3713 try: 

3714 return (self._engine.get_loc(key), None) 

3715 except KeyError as err: 

3716 raise KeyError(key) from err 

3717 except TypeError: 

3718 # e.g. partial string indexing 

3719 # test_partial_string_timestamp_multiindex 

3720 pass 

3721 

3722 # partial selection 

3723 indexer = self.get_loc(key) 

3724 ilevels = [i for i in range(len(key)) if key[i] != slice(None, None)] 

3725 if len(ilevels) == self.nlevels: 

3726 if is_integer(indexer): 

3727 # we are dropping all levels 

3728 return indexer, None 

3729 

3730 # TODO: in some cases we still need to drop some levels, 

3731 # e.g. test_multiindex_perf_warn 

3732 # test_partial_string_timestamp_multiindex 

3733 ilevels = [ 

3734 i 

3735 for i in range(len(key)) 

3736 if ( 

3737 not isinstance(key[i], str) 

3738 or not self.levels[i]._supports_partial_string_indexing 

3739 ) 

3740 and key[i] != slice(None, None) 

3741 ] 

3742 if len(ilevels) == self.nlevels: 

3743 # TODO: why? 

3744 ilevels = [] 

3745 return indexer, maybe_mi_droplevels(indexer, ilevels) 

3746 

3747 else: 

3748 indexer = None 

3749 for i, k in enumerate(key): 

3750 if not isinstance(k, slice): 

3751 loc_level = self._get_level_indexer(k, level=i) 

3752 if isinstance(loc_level, slice): 

3753 if com.is_null_slice(loc_level) or com.is_full_slice( 

3754 loc_level, len(self) 

3755 ): 

3756 # everything 

3757 continue 

3758 

3759 # e.g. test_xs_IndexSlice_argument_not_implemented 

3760 k_index = np.zeros(len(self), dtype=bool) 

3761 k_index[loc_level] = True 

3762 

3763 else: 

3764 k_index = loc_level 

3765 

3766 elif com.is_null_slice(k): 

3767 # taking everything, does not affect `indexer` below 

3768 continue 

3769 

3770 else: 

3771 # FIXME: this message can be inaccurate, e.g. 

3772 # test_series_varied_multiindex_alignment 

3773 raise TypeError(f"Expected label or tuple of labels, got {key}") 

3774 

3775 if indexer is None: 

3776 indexer = k_index 

3777 else: 

3778 indexer &= k_index 

3779 if indexer is None: 

3780 indexer = slice(None, None) 

3781 ilevels = [i for i in range(len(key)) if key[i] != slice(None, None)] 

3782 return indexer, maybe_mi_droplevels(indexer, ilevels) 

3783 else: 

3784 indexer = self._get_level_indexer(key, level=level) 

3785 if ( 

3786 isinstance(key, str) 

3787 and self.levels[level]._supports_partial_string_indexing 

3788 ): 

3789 # check to see if we did an exact lookup vs sliced 

3790 check = self.levels[level].get_loc(key) 

3791 if not is_integer(check): 

3792 # e.g. test_partial_string_timestamp_multiindex 

3793 return indexer, self[indexer] 

3794 

3795 try: 

3796 result_index = maybe_mi_droplevels(indexer, [level]) 

3797 except ValueError: 

3798 result_index = self[indexer] 

3799 

3800 return indexer, result_index 

3801 

3802 def _get_level_indexer( 

3803 self, key, level: int = 0, indexer: npt.NDArray[np.bool_] | None = None 

3804 ): 

3805 # `level` kwarg is _always_ positional, never name 

3806 # return a boolean array or slice showing where the key is 

3807 # in the totality of values 

3808 # if the indexer is provided, then use this 

3809 

3810 level_index = self.levels[level] 

3811 level_codes = self.codes[level] 

3812 

3813 def convert_indexer(start, stop, step, indexer=indexer, codes=level_codes): 

3814 # Compute a bool indexer to identify the positions to take. 

3815 # If we have an existing indexer, we only need to examine the 

3816 # subset of positions where the existing indexer is True. 

3817 if indexer is not None: 

3818 # we only need to look at the subset of codes where the 

3819 # existing indexer equals True 

3820 codes = codes[indexer] 

3821 

3822 if step is None or step == 1: 

3823 new_indexer = (codes >= start) & (codes < stop) 

3824 else: 

3825 r = np.arange(start, stop, step, dtype=codes.dtype) 

3826 new_indexer = algos.isin(codes, r) 

3827 

3828 if indexer is None: 

3829 return new_indexer 

3830 

3831 indexer = indexer.copy() 

3832 indexer[indexer] = new_indexer 

3833 return indexer 

3834 

3835 if isinstance(key, slice): 

3836 # handle a slice, returning a slice if we can 

3837 # otherwise a boolean indexer 

3838 step = key.step 

3839 is_negative_step = step is not None and step < 0 

3840 

3841 try: 

3842 if key.start is not None: 

3843 start = level_index.get_loc(key.start) 

3844 elif is_negative_step: 

3845 start = len(level_index) - 1 

3846 else: 

3847 start = 0 

3848 

3849 if key.stop is not None: 

3850 stop = level_index.get_loc(key.stop) 

3851 elif is_negative_step: 

3852 stop = 0 

3853 elif isinstance(start, slice): 

3854 stop = len(level_index) 

3855 else: 

3856 stop = len(level_index) - 1 

3857 except KeyError: 

3858 # we have a partial slice (like looking up a partial date 

3859 # string) 

3860 start = stop = level_index.slice_indexer(key.start, key.stop, key.step) 

3861 step = start.step 

3862 

3863 if isinstance(start, slice) or isinstance(stop, slice): 

3864 # we have a slice for start and/or stop 

3865 # a partial date slicer on a DatetimeIndex generates a slice 

3866 # note that the stop ALREADY includes the stopped point (if 

3867 # it was a string sliced) 

3868 start = getattr(start, "start", start) 

3869 stop = getattr(stop, "stop", stop) 

3870 return convert_indexer(start, stop, step) 

3871 

3872 elif level > 0 or self._lexsort_depth == 0 or step is not None: 

3873 # need to have like semantics here to right 

3874 # searching as when we are using a slice 

3875 # so adjust the stop by 1 (so we include stop) 

3876 stop = (stop - 1) if is_negative_step else (stop + 1) 

3877 return convert_indexer(start, stop, step) 

3878 else: 

3879 # sorted, so can return slice object -> view 

3880 i = algos.searchsorted(level_codes, start, side="left") 

3881 j = algos.searchsorted(level_codes, stop, side="right") 

3882 return slice(i, j, step) 

3883 

3884 else: 

3885 idx = self._get_loc_single_level_index(level_index, key) 

3886 

3887 if level > 0 or self._lexsort_depth == 0: 

3888 # Desired level is not sorted 

3889 if isinstance(idx, slice): 

3890 # test_get_loc_partial_timestamp_multiindex 

3891 locs = (level_codes >= idx.start) & (level_codes < idx.stop) 

3892 return locs 

3893 

3894 locs = np.asarray(level_codes == idx, dtype=bool) 

3895 

3896 if not locs.any(): 

3897 # The label is present in self.levels[level] but unused: 

3898 raise KeyError(key) 

3899 return locs 

3900 

3901 if isinstance(idx, slice): 

3902 # e.g. test_partial_string_timestamp_multiindex 

3903 start = algos.searchsorted(level_codes, idx.start, side="left") 

3904 # NB: "left" here bc of slice semantics 

3905 end = algos.searchsorted(level_codes, idx.stop, side="left") 

3906 else: 

3907 start = algos.searchsorted(level_codes, idx, side="left") 

3908 end = algos.searchsorted(level_codes, idx, side="right") 

3909 

3910 if start == end: 

3911 # The label is present in self.levels[level] but unused: 

3912 raise KeyError(key) 

3913 return slice(maybe_unbox_numpy_scalar(start), maybe_unbox_numpy_scalar(end)) 

3914 

3915 def get_locs(self, seq) -> npt.NDArray[np.intp]: 

3916 """ 

3917 Get location for a sequence of labels. 

3918 

3919 Parameters 

3920 ---------- 

3921 seq : label, slice, list, mask or a sequence of such 

3922 You should use one of the above for each level. 

3923 If a level should not be used, set it to ``slice(None)``. 

3924 

3925 Returns 

3926 ------- 

3927 numpy.ndarray 

3928 NumPy array of integers suitable for passing to iloc. 

3929 

3930 See Also 

3931 -------- 

3932 MultiIndex.get_loc : Get location for a label or a tuple of labels. 

3933 MultiIndex.slice_locs : Get slice location given start label(s) and 

3934 end label(s). 

3935 

3936 Examples 

3937 -------- 

3938 >>> mi = pd.MultiIndex.from_arrays([list("abb"), list("def")]) 

3939 

3940 >>> mi.get_locs("b") # doctest: +SKIP 

3941 array([1, 2], dtype=int64) 

3942 

3943 >>> mi.get_locs([slice(None), ["e", "f"]]) # doctest: +SKIP 

3944 array([1, 2], dtype=int64) 

3945 

3946 >>> mi.get_locs([[True, False, True], slice("e", "f")]) # doctest: +SKIP 

3947 array([2], dtype=int64) 

3948 """ 

3949 

3950 # must be lexsorted to at least as many levels 

3951 true_slices = [i for (i, s) in enumerate(com.is_true_slices(seq)) if s] 

3952 if true_slices and true_slices[-1] >= self._lexsort_depth: 

3953 raise UnsortedIndexError( 

3954 "MultiIndex slicing requires the index to be lexsorted: slicing " 

3955 f"on levels {true_slices}, lexsort depth {self._lexsort_depth}" 

3956 ) 

3957 

3958 if any(x is Ellipsis for x in seq): 

3959 raise NotImplementedError( 

3960 "MultiIndex does not support indexing with Ellipsis" 

3961 ) 

3962 

3963 n = len(self) 

3964 

3965 def _to_bool_indexer(indexer) -> npt.NDArray[np.bool_]: 

3966 if isinstance(indexer, slice): 

3967 new_indexer = np.zeros(n, dtype=np.bool_) 

3968 new_indexer[indexer] = True 

3969 return new_indexer 

3970 return indexer 

3971 

3972 # a bool indexer for the positions we want to take 

3973 indexer: npt.NDArray[np.bool_] | None = None 

3974 

3975 for i, k in enumerate(seq): 

3976 lvl_indexer: npt.NDArray[np.bool_] | slice | None = None 

3977 

3978 if com.is_bool_indexer(k): 

3979 if len(k) != n: 

3980 raise ValueError( 

3981 "cannot index with a boolean indexer that " 

3982 "is not the same length as the index" 

3983 ) 

3984 if isinstance(k, (ABCSeries, Index)): 

3985 k = k._values 

3986 lvl_indexer = np.asarray(k) 

3987 if indexer is None: 

3988 lvl_indexer = lvl_indexer.copy() 

3989 

3990 elif is_list_like(k): 

3991 # a collection of labels to include from this level (these are or'd) 

3992 

3993 # GH#27591 check if this is a single tuple key in the level 

3994 try: 

3995 lvl_indexer = self._get_level_indexer(k, level=i, indexer=indexer) 

3996 except (InvalidIndexError, TypeError, KeyError) as err: 

3997 # InvalidIndexError e.g. non-hashable, fall back to treating 

3998 # this as a sequence of labels 

3999 # KeyError it can be ambiguous if this is a label or sequence 

4000 # of labels 

4001 # github.com/pandas-dev/pandas/issues/39424#issuecomment-871626708 

4002 for x in k: 

4003 if not is_hashable(x): 

4004 # e.g. slice 

4005 raise err 

4006 # GH 39424: Ignore not founds 

4007 # GH 42351: No longer ignore not founds & enforced in 2.0 

4008 # TODO: how to handle IntervalIndex level? (no test cases) 

4009 item_indexer = self._get_level_indexer( 

4010 x, level=i, indexer=indexer 

4011 ) 

4012 if lvl_indexer is None: 

4013 lvl_indexer = _to_bool_indexer(item_indexer) 

4014 elif isinstance(item_indexer, slice): 

4015 lvl_indexer[item_indexer] = True # type: ignore[index] 

4016 else: 

4017 lvl_indexer |= item_indexer 

4018 

4019 if lvl_indexer is None: 

4020 # no matches we are done 

4021 # test_loc_getitem_duplicates_multiindex_empty_indexer 

4022 return np.array([], dtype=np.intp) 

4023 

4024 elif com.is_null_slice(k): 

4025 # empty slice 

4026 if indexer is None and i == len(seq) - 1: 

4027 return np.arange(n, dtype=np.intp) 

4028 continue 

4029 

4030 else: 

4031 # a slice or a single label 

4032 lvl_indexer = self._get_level_indexer(k, level=i, indexer=indexer) 

4033 

4034 # update indexer 

4035 lvl_indexer = _to_bool_indexer(lvl_indexer) 

4036 if indexer is None: 

4037 indexer = lvl_indexer 

4038 else: 

4039 indexer &= lvl_indexer 

4040 if not np.any(indexer) and np.any(lvl_indexer): 

4041 raise KeyError(seq) 

4042 

4043 # empty indexer 

4044 if indexer is None: 

4045 return np.array([], dtype=np.intp) 

4046 

4047 pos_indexer = indexer.nonzero()[0] 

4048 return self._reorder_indexer(seq, pos_indexer) 

4049 

4050 # -------------------------------------------------------------------- 

4051 

4052 def _reorder_indexer( 

4053 self, 

4054 seq: tuple[Scalar | Iterable | AnyArrayLike, ...], 

4055 indexer: npt.NDArray[np.intp], 

4056 ) -> npt.NDArray[np.intp]: 

4057 """ 

4058 Reorder an indexer of a MultiIndex (self) so that the labels are in the 

4059 same order as given in seq 

4060 

4061 Parameters 

4062 ---------- 

4063 seq : label/slice/list/mask or a sequence of such 

4064 indexer: a position indexer of self 

4065 

4066 Returns 

4067 ------- 

4068 indexer : a sorted position indexer of self ordered as seq 

4069 """ 

4070 

4071 # check if sorting is necessary 

4072 need_sort = False 

4073 for i, k in enumerate(seq): 

4074 if com.is_null_slice(k) or com.is_bool_indexer(k) or is_scalar(k): 

4075 pass 

4076 elif is_list_like(k): 

4077 if len(k) <= 1: # type: ignore[arg-type] 

4078 pass 

4079 elif self._is_lexsorted(): 

4080 # If the index is lexsorted and the list_like label 

4081 # in seq are sorted then we do not need to sort 

4082 k_codes = self.levels[i].get_indexer(k) 

4083 k_codes = k_codes[k_codes >= 0] # Filter absent keys 

4084 # True if the given codes are not ordered 

4085 need_sort = (k_codes[:-1] > k_codes[1:]).any() 

4086 else: 

4087 need_sort = True 

4088 elif isinstance(k, slice): 

4089 if self._is_lexsorted(): 

4090 need_sort = k.step is not None and k.step < 0 

4091 else: 

4092 need_sort = True 

4093 else: 

4094 need_sort = True 

4095 if need_sort: 

4096 break 

4097 if not need_sort: 

4098 return indexer 

4099 

4100 n = len(self) 

4101 keys: tuple[np.ndarray, ...] = () 

4102 # For each level of the sequence in seq, map the level codes with the 

4103 # order they appears in a list-like sequence 

4104 # This mapping is then use to reorder the indexer 

4105 for i, k in enumerate(seq): 

4106 if is_scalar(k): 

4107 # GH#34603 we want to treat a scalar the same as an all equal list 

4108 k = [k] 

4109 if com.is_bool_indexer(k): 

4110 new_order = np.arange(n)[indexer] 

4111 elif is_list_like(k): 

4112 # Generate a map with all level codes as sorted initially 

4113 if not isinstance(k, (np.ndarray, ExtensionArray, Index, ABCSeries)): 

4114 k = sanitize_array(k, None) 

4115 k = algos.unique(k) 

4116 key_order_map = np.ones(len(self.levels[i]), dtype=np.uint64) * len( 

4117 self.levels[i] 

4118 ) 

4119 # Set order as given in the indexer list 

4120 level_indexer = self.levels[i].get_indexer(k) 

4121 level_indexer = level_indexer[level_indexer >= 0] # Filter absent keys 

4122 key_order_map[level_indexer] = np.arange(len(level_indexer)) 

4123 

4124 new_order = key_order_map[self.codes[i][indexer]] 

4125 elif isinstance(k, slice) and k.step is not None and k.step < 0: 

4126 # flip order for negative step 

4127 new_order = np.arange(n - 1, -1, -1)[indexer] 

4128 elif isinstance(k, slice) and k.start is None and k.stop is None: 

4129 # slice(None) should not determine order GH#31330 

4130 new_order = np.ones((n,), dtype=np.intp)[indexer] 

4131 else: 

4132 # For all other case, use the same order as the level 

4133 new_order = np.arange(n)[indexer] 

4134 keys = (new_order, *keys) 

4135 

4136 # Find the reordering using lexsort on the keys mapping 

4137 ind = np.lexsort(keys) 

4138 return indexer[ind] 

4139 

4140 def truncate(self, before=None, after=None) -> MultiIndex: 

4141 """ 

4142 Slice index between two labels / tuples, return new MultiIndex. 

4143 

4144 Parameters 

4145 ---------- 

4146 before : label or tuple, can be partial. Default None 

4147 None defaults to start. 

4148 after : label or tuple, can be partial. Default None 

4149 None defaults to end. 

4150 

4151 Returns 

4152 ------- 

4153 MultiIndex 

4154 The truncated MultiIndex. 

4155 

4156 See Also 

4157 -------- 

4158 DataFrame.truncate : Truncate a DataFrame before and after some index values. 

4159 Series.truncate : Truncate a Series before and after some index values. 

4160 

4161 Examples 

4162 -------- 

4163 >>> mi = pd.MultiIndex.from_arrays([["a", "b", "c"], ["x", "y", "z"]]) 

4164 >>> mi 

4165 MultiIndex([('a', 'x'), ('b', 'y'), ('c', 'z')], 

4166 ) 

4167 >>> mi.truncate(before="a", after="b") 

4168 MultiIndex([('a', 'x'), ('b', 'y')], 

4169 ) 

4170 """ 

4171 if after and before and after < before: 

4172 raise ValueError("after < before") 

4173 

4174 i, j = self.levels[0].slice_locs(before, after) 

4175 left, right = self.slice_locs(before, after) 

4176 

4177 new_levels = list(self.levels) 

4178 new_levels[0] = new_levels[0][i:j] 

4179 

4180 new_codes = [level_codes[left:right] for level_codes in self.codes] 

4181 new_codes[0] = new_codes[0] - i 

4182 

4183 return MultiIndex( 

4184 levels=new_levels, 

4185 codes=new_codes, 

4186 names=self._names, 

4187 verify_integrity=False, 

4188 ) 

4189 

4190 def equals(self, other: object) -> bool: 

4191 """ 

4192 Determines if two MultiIndex objects have the same labeling information 

4193 (the levels themselves do not necessarily have to be the same) 

4194 

4195 See Also 

4196 -------- 

4197 equal_levels 

4198 """ 

4199 if self.is_(other): 

4200 return True 

4201 

4202 if not isinstance(other, Index): 

4203 return False 

4204 

4205 if len(self) != len(other): 

4206 return False 

4207 

4208 if not isinstance(other, MultiIndex): 

4209 # d-level MultiIndex can equal d-tuple Index 

4210 if not self._should_compare(other): 

4211 # object Index or Categorical[object] may contain tuples 

4212 return False 

4213 return array_equivalent(self._values, other._values) 

4214 

4215 if self.nlevels != other.nlevels: 

4216 return False 

4217 

4218 for i in range(self.nlevels): 

4219 self_codes = self.codes[i] 

4220 other_codes = other.codes[i] 

4221 self_mask = self_codes == -1 

4222 other_mask = other_codes == -1 

4223 if not np.array_equal(self_mask, other_mask): 

4224 return False 

4225 self_level = self.levels[i] 

4226 other_level = other.levels[i] 

4227 new_codes = recode_for_categories( 

4228 other_codes, other_level, self_level, copy=False 

4229 ) 

4230 if not np.array_equal(self_codes, new_codes): 

4231 return False 

4232 if not self_level[:0].equals(other_level[:0]): 

4233 # e.g. Int64 != int64 

4234 return False 

4235 return True 

4236 

4237 def equal_levels(self, other: MultiIndex) -> bool: 

4238 """ 

4239 Return True if the levels of both MultiIndex objects are the same 

4240 

4241 """ 

4242 if self.nlevels != other.nlevels: 

4243 return False 

4244 

4245 for i in range(self.nlevels): 

4246 if not self.levels[i].equals(other.levels[i]): 

4247 return False 

4248 return True 

4249 

4250 # -------------------------------------------------------------------- 

4251 # Set Methods 

4252 

4253 def _union(self, other, sort) -> MultiIndex: 

4254 other, result_names = self._convert_can_do_setop(other) 

4255 if other.has_duplicates: 

4256 # This is only necessary if other has dupes, 

4257 # otherwise difference is faster 

4258 result = super(MultiIndex, self.rename(result_names))._union( 

4259 other.rename(result_names), sort 

4260 ) 

4261 

4262 if isinstance(result, MultiIndex): 

4263 return result 

4264 return MultiIndex.from_arrays( 

4265 zip(*result, strict=True), sortorder=None, names=result_names 

4266 ) 

4267 

4268 else: 

4269 right_missing = other.difference(self, sort=False) 

4270 if len(right_missing): 

4271 result = self.append(right_missing) 

4272 else: 

4273 result = self._get_reconciled_name_object(other) 

4274 

4275 if sort is not False: 

4276 try: 

4277 result = result.sort_values() 

4278 except TypeError: 

4279 if sort is True: 

4280 raise 

4281 warnings.warn( 

4282 "The values in the array are unorderable. " 

4283 "Pass `sort=False` to suppress this warning.", 

4284 RuntimeWarning, 

4285 stacklevel=find_stack_level(), 

4286 ) 

4287 return result 

4288 

4289 def _is_comparable_dtype(self, dtype: DtypeObj) -> bool: 

4290 return is_object_dtype(dtype) 

4291 

4292 def _get_reconciled_name_object(self, other) -> MultiIndex: 

4293 """ 

4294 If the result of a set operation will be self, 

4295 return a shallow copy of self. 

4296 """ 

4297 names = self._maybe_match_names(other) 

4298 if self.names != names: 

4299 return self.rename(names) 

4300 return self.copy(deep=False) 

4301 

4302 def _maybe_match_names(self, other): 

4303 """ 

4304 Try to find common names to attach to the result of an operation between 

4305 a and b. Return a consensus list of names if they match at least partly 

4306 or list of None if they have completely different names. 

4307 """ 

4308 if len(self.names) != len(other.names): 

4309 return [None] * len(self.names) 

4310 names = [] 

4311 for a_name, b_name in zip(self.names, other.names, strict=True): 

4312 if a_name == b_name: 

4313 names.append(a_name) 

4314 else: 

4315 # TODO: what if they both have np.nan for their names? 

4316 names.append(None) 

4317 return names 

4318 

4319 def _wrap_intersection_result(self, other, result) -> MultiIndex: 

4320 _, result_names = self._convert_can_do_setop(other) 

4321 return result.set_names(result_names) 

4322 

4323 def _wrap_difference_result(self, other, result: MultiIndex) -> MultiIndex: 

4324 _, result_names = self._convert_can_do_setop(other) 

4325 

4326 if len(result) == 0: 

4327 return result.remove_unused_levels().set_names(result_names) 

4328 else: 

4329 return result.set_names(result_names) 

4330 

4331 def _convert_can_do_setop(self, other): 

4332 result_names = self.names 

4333 

4334 if not isinstance(other, Index): 

4335 if len(other) == 0: 

4336 return self[:0], self.names 

4337 else: 

4338 msg = "other must be a MultiIndex or a list of tuples" 

4339 try: 

4340 other = MultiIndex.from_tuples(other, names=self.names) 

4341 except (ValueError, TypeError) as err: 

4342 # ValueError raised by tuples_to_object_array if we 

4343 # have non-object dtype 

4344 raise TypeError(msg) from err 

4345 else: 

4346 result_names = get_unanimous_names(self, other) 

4347 

4348 return other, result_names 

4349 

4350 # -------------------------------------------------------------------- 

4351 

4352 def astype(self, dtype, copy: bool = True): 

4353 """ 

4354 Create an MultiIndex with values cast to dtypes. 

4355 

4356 The class of a new Index is determined by dtype. When conversion is 

4357 impossible, a TypeError exception is raised. 

4358 

4359 Parameters 

4360 ---------- 

4361 dtype : numpy dtype or pandas type 

4362 Note that any signed integer `dtype` is treated as ``'int64'``, 

4363 and any unsigned integer `dtype` is treated as ``'uint64'``, 

4364 regardless of the size. 

4365 copy : bool, default True 

4366 By default, astype always returns a newly allocated object. 

4367 If copy is set to False and internal requirements on dtype are 

4368 satisfied, the original data is used to create a new Index 

4369 or the original Index is returned. 

4370 

4371 Returns 

4372 ------- 

4373 MultiIndex 

4374 MultiIndex with values cast to specified dtype. 

4375 

4376 See Also 

4377 -------- 

4378 Index.dtype: Return the dtype object of the underlying data. 

4379 Index.dtypes: Return the dtype object of the underlying data. 

4380 Index.convert_dtypes: Convert columns to the best possible dtypes. 

4381 

4382 Examples 

4383 -------- 

4384 >>> mi = pd.MultiIndex.from_arrays(([1, 2, 3], [4, 5, 6])) 

4385 >>> mi 

4386 MultiIndex([(1, 4), 

4387 (2, 5), 

4388 (3, 6)], 

4389 ) 

4390 >>> mi.astype("object") 

4391 MultiIndex([(1, 4), 

4392 (2, 5), 

4393 (3, 6)], 

4394 ) 

4395 """ 

4396 dtype = pandas_dtype(dtype) 

4397 if isinstance(dtype, CategoricalDtype): 

4398 msg = "> 1 ndim Categorical are not supported at this time" 

4399 raise NotImplementedError(msg) 

4400 if not is_object_dtype(dtype): 

4401 raise TypeError( 

4402 "Setting a MultiIndex dtype to anything other than object " 

4403 "is not supported" 

4404 ) 

4405 if copy is True: 

4406 return self._view() 

4407 return self 

4408 

4409 def _validate_fill_value(self, item): 

4410 if isinstance(item, MultiIndex): 

4411 # GH#43212 

4412 if item.nlevels != self.nlevels: 

4413 raise ValueError("Item must have length equal to number of levels.") 

4414 return item._values 

4415 elif not isinstance(item, tuple): 

4416 # Pad the key with empty strings if lower levels of the key 

4417 # aren't specified: 

4418 item = (item,) + ("",) * (self.nlevels - 1) 

4419 elif len(item) != self.nlevels: 

4420 raise ValueError("Item must have length equal to number of levels.") 

4421 return item 

4422 

4423 def putmask(self, mask, value: MultiIndex) -> MultiIndex: 

4424 """ 

4425 Return a new MultiIndex of the values set with the mask. 

4426 

4427 Parameters 

4428 ---------- 

4429 mask : array like 

4430 value : MultiIndex 

4431 Must either be the same length as self or length one 

4432 

4433 Returns 

4434 ------- 

4435 MultiIndex 

4436 """ 

4437 mask, noop = validate_putmask(self, mask) 

4438 if noop: 

4439 return self.copy() 

4440 

4441 if len(mask) == len(value): 

4442 subset = value[mask].remove_unused_levels() 

4443 else: 

4444 subset = value.remove_unused_levels() 

4445 

4446 new_levels = [] 

4447 new_codes = [] 

4448 

4449 for i, (value_level, level, level_codes) in enumerate( 

4450 zip(subset.levels, self.levels, self.codes, strict=True) 

4451 ): 

4452 new_level = level.union(value_level, sort=False) 

4453 value_codes = new_level.get_indexer_for(subset.get_level_values(i)) 

4454 new_code = ensure_int64(level_codes) 

4455 new_code[mask] = value_codes 

4456 new_levels.append(new_level) 

4457 new_codes.append(new_code) 

4458 

4459 return MultiIndex( 

4460 levels=new_levels, codes=new_codes, names=self.names, verify_integrity=False 

4461 ) 

4462 

4463 def insert(self, loc: int, item) -> MultiIndex: 

4464 """ 

4465 Make new MultiIndex inserting new item at location 

4466 

4467 Parameters 

4468 ---------- 

4469 loc : int 

4470 item : tuple 

4471 Must be same length as number of levels in the MultiIndex 

4472 

4473 Returns 

4474 ------- 

4475 new_index : Index 

4476 """ 

4477 item = self._validate_fill_value(item) 

4478 

4479 new_levels = [] 

4480 new_codes = [] 

4481 for k, level, level_codes in zip(item, self.levels, self.codes, strict=True): 

4482 if k not in level: 

4483 # have to insert into level 

4484 # must insert at end otherwise you have to recompute all the 

4485 # other codes 

4486 lev_loc = len(level) 

4487 level = level.insert(lev_loc, k) 

4488 if isna(level[lev_loc]): # GH 59003, 60388 

4489 lev_loc = -1 

4490 else: 

4491 lev_loc = level.get_loc(k) 

4492 

4493 new_levels.append(level) 

4494 new_codes.append(np.insert(ensure_int64(level_codes), loc, lev_loc)) 

4495 

4496 return MultiIndex( 

4497 levels=new_levels, codes=new_codes, names=self.names, verify_integrity=False 

4498 ) 

4499 

4500 def delete(self, loc) -> MultiIndex: 

4501 """ 

4502 Make new index with passed location deleted 

4503 

4504 Returns 

4505 ------- 

4506 new_index : MultiIndex 

4507 """ 

4508 new_codes = [np.delete(level_codes, loc) for level_codes in self.codes] 

4509 return MultiIndex( 

4510 levels=self.levels, 

4511 codes=new_codes, 

4512 names=self.names, 

4513 verify_integrity=False, 

4514 ) 

4515 

4516 def isin(self, values, level=None) -> npt.NDArray[np.bool_]: 

4517 """ 

4518 Return a boolean array where the index values are in `values`. 

4519 

4520 Compute boolean array of whether each index value is found in the 

4521 passed set of values. The length of the returned boolean array matches 

4522 the length of the index. 

4523 

4524 Parameters 

4525 ---------- 

4526 values : set or list-like 

4527 Sought values. 

4528 level : str or int, optional 

4529 Name or position of the index level to use (if the index is a 

4530 `MultiIndex`). 

4531 

4532 Returns 

4533 ------- 

4534 np.ndarray[bool] 

4535 NumPy array of boolean values. 

4536 

4537 See Also 

4538 -------- 

4539 Series.isin : Same for Series. 

4540 DataFrame.isin : Same method for DataFrames. 

4541 

4542 Notes 

4543 ----- 

4544 In the case of `MultiIndex` you must either specify `values` as a 

4545 list-like object containing tuples that are the same length as the 

4546 number of levels, or specify `level`. Otherwise it will raise a 

4547 ``ValueError``. 

4548 

4549 If `level` is specified: 

4550 

4551 - if it is the name of one *and only one* index level, use that level; 

4552 - otherwise it should be a number indicating level position. 

4553 

4554 Examples 

4555 -------- 

4556 >>> idx = pd.Index([1, 2, 3]) 

4557 >>> idx 

4558 Index([1, 2, 3], dtype='int64') 

4559 

4560 Check whether each index value in a list of values. 

4561 

4562 >>> idx.isin([1, 4]) 

4563 array([ True, False, False]) 

4564 

4565 >>> mi = pd.MultiIndex.from_arrays( 

4566 ... [[1, 2, 3], ["red", "blue", "green"]], names=["number", "color"] 

4567 ... ) 

4568 >>> mi 

4569 MultiIndex([(1, 'red'), 

4570 (2, 'blue'), 

4571 (3, 'green')], 

4572 names=['number', 'color']) 

4573 

4574 Check whether the strings in the 'color' level of the MultiIndex 

4575 are in a list of colors. 

4576 

4577 >>> mi.isin(["red", "orange", "yellow"], level="color") 

4578 array([ True, False, False]) 

4579 

4580 To check across the levels of a MultiIndex, pass a list of tuples: 

4581 

4582 >>> mi.isin([(1, "red"), (3, "red")]) 

4583 array([ True, False, False]) 

4584 """ 

4585 if isinstance(values, Generator): 

4586 values = list(values) 

4587 

4588 if level is None: 

4589 if len(values) == 0: 

4590 return np.zeros((len(self),), dtype=np.bool_) 

4591 if not isinstance(values, MultiIndex): 

4592 values = MultiIndex.from_tuples(values) 

4593 return values.unique().get_indexer_for(self) != -1 

4594 else: 

4595 num = self._get_level_number(level) 

4596 levs = self.get_level_values(num) 

4597 

4598 if levs.size == 0: 

4599 return np.zeros(len(levs), dtype=np.bool_) 

4600 return levs.isin(values) 

4601 

4602 # error: Incompatible types in assignment (expression has type overloaded function, 

4603 # base class "Index" defined the type as "Callable[[Index, Any, bool], Any]") 

4604 rename = Index.set_names # type: ignore[assignment] 

4605 

4606 # --------------------------------------------------------------- 

4607 # Arithmetic/Numeric Methods - Disabled 

4608 

4609 __add__ = make_invalid_op("__add__") 

4610 __radd__ = make_invalid_op("__radd__") 

4611 __iadd__ = make_invalid_op("__iadd__") 

4612 __sub__ = make_invalid_op("__sub__") 

4613 __rsub__ = make_invalid_op("__rsub__") 

4614 __isub__ = make_invalid_op("__isub__") 

4615 __pow__ = make_invalid_op("__pow__") 

4616 __rpow__ = make_invalid_op("__rpow__") 

4617 __mul__ = make_invalid_op("__mul__") 

4618 __rmul__ = make_invalid_op("__rmul__") 

4619 __floordiv__ = make_invalid_op("__floordiv__") 

4620 __rfloordiv__ = make_invalid_op("__rfloordiv__") 

4621 __truediv__ = make_invalid_op("__truediv__") 

4622 __rtruediv__ = make_invalid_op("__rtruediv__") 

4623 __mod__ = make_invalid_op("__mod__") 

4624 __rmod__ = make_invalid_op("__rmod__") 

4625 __divmod__ = make_invalid_op("__divmod__") 

4626 __rdivmod__ = make_invalid_op("__rdivmod__") 

4627 # Unary methods disabled 

4628 __neg__ = make_invalid_op("__neg__") 

4629 __pos__ = make_invalid_op("__pos__") 

4630 __abs__ = make_invalid_op("__abs__") 

4631 __invert__ = make_invalid_op("__invert__") 

4632 

4633 

4634def _lexsort_depth(codes: list[np.ndarray], nlevels: int) -> int: 

4635 """Count depth (up to a maximum of `nlevels`) with which codes are lexsorted.""" 

4636 int64_codes = [ensure_int64(level_codes) for level_codes in codes] 

4637 for k in range(nlevels, 0, -1): 

4638 if libalgos.is_lexsorted(int64_codes[:k]): 

4639 return k 

4640 return 0 

4641 

4642 

4643def sparsify_labels(label_list, start: int = 0, sentinel: object = ""): 

4644 pivoted = list(zip(*label_list, strict=True)) 

4645 k = len(label_list) 

4646 

4647 result = pivoted[: start + 1] 

4648 prev = pivoted[start] 

4649 

4650 for cur in pivoted[start + 1 :]: 

4651 sparse_cur = [] 

4652 

4653 for i, (p, t) in enumerate(zip(prev, cur, strict=True)): 

4654 if i == k - 1: 

4655 sparse_cur.append(t) 

4656 result.append(sparse_cur) # type: ignore[arg-type] 

4657 break 

4658 

4659 if p == t: 

4660 sparse_cur.append(sentinel) 

4661 else: 

4662 sparse_cur.extend(cur[i:]) 

4663 result.append(sparse_cur) # type: ignore[arg-type] 

4664 break 

4665 

4666 prev = cur 

4667 

4668 return list(zip(*result, strict=True)) 

4669 

4670 

4671def _get_na_rep(dtype: DtypeObj) -> str: 

4672 if isinstance(dtype, ExtensionDtype): 

4673 return f"{dtype.na_value}" 

4674 else: 

4675 dtype_type = dtype.type 

4676 

4677 return {np.datetime64: "NaT", np.timedelta64: "NaT"}.get(dtype_type, "NaN") 

4678 

4679 

4680def maybe_droplevels(index: Index, key) -> Index: 

4681 """ 

4682 Attempt to drop level or levels from the given index. 

4683 

4684 Parameters 

4685 ---------- 

4686 index: Index 

4687 key : scalar or tuple 

4688 

4689 Returns 

4690 ------- 

4691 Index 

4692 """ 

4693 # drop levels 

4694 original_index = index 

4695 if isinstance(key, tuple): 

4696 # Caller is responsible for ensuring the key is not an entry in the first 

4697 # level of the MultiIndex. 

4698 for _ in key: 

4699 try: 

4700 index = index._drop_level_numbers([0]) 

4701 except ValueError: 

4702 # we have dropped too much, so back out 

4703 return original_index 

4704 else: 

4705 try: 

4706 index = index._drop_level_numbers([0]) 

4707 except ValueError: 

4708 pass 

4709 

4710 return index 

4711 

4712 

4713def _coerce_indexer_frozen(array_like, categories, copy: bool = False) -> np.ndarray: 

4714 """ 

4715 Coerce the array-like indexer to the smallest integer dtype that can encode all 

4716 of the given categories. 

4717 

4718 Parameters 

4719 ---------- 

4720 array_like : array-like 

4721 categories : array-like 

4722 copy : bool 

4723 

4724 Returns 

4725 ------- 

4726 np.ndarray 

4727 Non-writeable. 

4728 """ 

4729 array_like = coerce_indexer_dtype(array_like, categories) 

4730 if copy: 

4731 array_like = array_like.copy() 

4732 array_like.flags.writeable = False 

4733 return array_like 

4734 

4735 

4736def _require_listlike(level, arr, arrname: str): 

4737 """ 

4738 Ensure that level is either None or listlike, and arr is list-of-listlike. 

4739 """ 

4740 if level is not None and not is_list_like(level): 

4741 if not is_list_like(arr): 

4742 raise TypeError(f"{arrname} must be list-like") 

4743 if len(arr) > 0 and is_list_like(arr[0]): 

4744 raise TypeError(f"{arrname} must be list-like") 

4745 level = [level] 

4746 arr = [arr] 

4747 elif level is None or is_list_like(level): 

4748 if not is_list_like(arr) or not is_list_like(arr[0]): 

4749 raise TypeError(f"{arrname} must be list of lists-like") 

4750 return level, arr 

4751 

4752 

4753def cartesian_product(X: list[np.ndarray]) -> list[np.ndarray]: 

4754 """ 

4755 Numpy version of itertools.product. 

4756 Sometimes faster (for large inputs)... 

4757 

4758 Parameters 

4759 ---------- 

4760 X : list-like of list-likes 

4761 

4762 Returns 

4763 ------- 

4764 product : list of ndarrays 

4765 

4766 Examples 

4767 -------- 

4768 >>> cartesian_product([list("ABC"), [1, 2]]) 

4769 [array(['A', 'A', 'B', 'B', 'C', 'C'], dtype='<U1'), array([1, 2, 1, 2, 1, 2])] 

4770 

4771 See Also 

4772 -------- 

4773 itertools.product : Cartesian product of input iterables. Equivalent to 

4774 nested for-loops. 

4775 """ 

4776 msg = "Input must be a list-like of list-likes" 

4777 if not is_list_like(X): 

4778 raise TypeError(msg) 

4779 for x in X: 

4780 if not is_list_like(x): 

4781 raise TypeError(msg) 

4782 

4783 if len(X) == 0: 

4784 return [] 

4785 

4786 lenX = np.fromiter((len(x) for x in X), dtype=np.intp) 

4787 cumprodX = np.cumprod(lenX) 

4788 

4789 if np.any(cumprodX < 0): 

4790 raise ValueError("Product space too large to allocate arrays!") 

4791 

4792 a = np.roll(cumprodX, 1) 

4793 a[0] = 1 

4794 

4795 if cumprodX[-1] != 0: 

4796 b = cumprodX[-1] / cumprodX 

4797 else: 

4798 # if any factor is empty, the cartesian product is empty 

4799 b = np.zeros_like(cumprodX) 

4800 

4801 return [ 

4802 np.tile( 

4803 np.repeat(x, b[i]), 

4804 np.prod(a[i]), 

4805 ) 

4806 for i, x in enumerate(X) 

4807 ]