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

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

165 statements  

1from __future__ import annotations 

2 

3from typing import ( 

4 TYPE_CHECKING, 

5 Any, 

6 Literal, 

7 Self, 

8 cast, 

9) 

10 

11import numpy as np 

12 

13from pandas._libs import index as libindex 

14from pandas.util._decorators import ( 

15 cache_readonly, 

16 set_module, 

17) 

18 

19from pandas.core.dtypes.common import is_scalar 

20from pandas.core.dtypes.dtypes import CategoricalDtype 

21from pandas.core.dtypes.missing import ( 

22 is_valid_na_for_dtype, 

23) 

24 

25from pandas.core.arrays.categorical import ( 

26 Categorical, 

27 contains, 

28) 

29from pandas.core.construction import extract_array 

30from pandas.core.indexes.base import ( 

31 Index, 

32 maybe_extract_name, 

33) 

34from pandas.core.indexes.extension import NDArrayBackedExtensionIndex 

35 

36if TYPE_CHECKING: 

37 from collections.abc import ( 

38 Callable, 

39 Hashable, 

40 ) 

41 

42 from pandas._typing import ( 

43 ArrayLike, 

44 Axes, 

45 Dtype, 

46 DtypeObj, 

47 Level, 

48 ReindexMethod, 

49 npt, 

50 ) 

51 

52 from pandas import Series 

53 from pandas.core.indexes.multi import MultiIndex 

54 

55 

56@set_module("pandas") 

57class CategoricalIndex(NDArrayBackedExtensionIndex): 

58 """ 

59 Index based on an underlying :class:`Categorical`. 

60 

61 CategoricalIndex, like Categorical, can only take on a limited, 

62 and usually fixed, number of possible values (`categories`). Also, 

63 like Categorical, it might have an order, but numerical operations 

64 (additions, divisions, ...) are not possible. 

65 

66 Parameters 

67 ---------- 

68 data : array-like (1-dimensional) 

69 The values of the categorical. If `categories` are given, values not in 

70 `categories` will be replaced with NaN. 

71 categories : index-like, optional 

72 The categories for the categorical. Items need to be unique. 

73 If the categories are not given here (and also not in `dtype`), they 

74 will be inferred from the `data`. 

75 ordered : bool, optional 

76 Whether or not this categorical is treated as an ordered 

77 categorical. If not given here or in `dtype`, the resulting 

78 categorical will be unordered. 

79 dtype : CategoricalDtype or "category", optional 

80 If :class:`CategoricalDtype`, cannot be used together with 

81 `categories` or `ordered`. 

82 copy : bool, default False 

83 Make a copy of input ndarray. 

84 name : object, optional 

85 Name to be stored in the index. 

86 

87 Attributes 

88 ---------- 

89 codes 

90 categories 

91 ordered 

92 

93 Methods 

94 ------- 

95 rename_categories 

96 reorder_categories 

97 add_categories 

98 remove_categories 

99 remove_unused_categories 

100 set_categories 

101 as_ordered 

102 as_unordered 

103 map 

104 

105 Raises 

106 ------ 

107 ValueError 

108 If the categories do not validate. 

109 TypeError 

110 If an explicit ``ordered=True`` is given but no `categories` and the 

111 `values` are not sortable. 

112 

113 See Also 

114 -------- 

115 Index : The base pandas Index type. 

116 Categorical : A categorical array. 

117 CategoricalDtype : Type for categorical data. 

118 

119 Notes 

120 ----- 

121 See the `user guide 

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

123 for more. 

124 

125 Examples 

126 -------- 

127 >>> pd.CategoricalIndex(["a", "b", "c", "a", "b", "c"]) 

128 CategoricalIndex(['a', 'b', 'c', 'a', 'b', 'c'], 

129 categories=['a', 'b', 'c'], ordered=False, dtype='category') 

130 

131 ``CategoricalIndex`` can also be instantiated from a ``Categorical``: 

132 

133 >>> c = pd.Categorical(["a", "b", "c", "a", "b", "c"]) 

134 >>> pd.CategoricalIndex(c) 

135 CategoricalIndex(['a', 'b', 'c', 'a', 'b', 'c'], 

136 categories=['a', 'b', 'c'], ordered=False, dtype='category') 

137 

138 Ordered ``CategoricalIndex`` can have a min and max value. 

139 

140 >>> ci = pd.CategoricalIndex( 

141 ... ["a", "b", "c", "a", "b", "c"], ordered=True, categories=["c", "b", "a"] 

142 ... ) 

143 >>> ci 

144 CategoricalIndex(['a', 'b', 'c', 'a', 'b', 'c'], 

145 categories=['c', 'b', 'a'], ordered=True, dtype='category') 

146 >>> ci.min() 

147 'c' 

148 """ 

149 

150 _typ = "categoricalindex" 

151 _data_cls = Categorical 

152 

153 @property 

154 def _can_hold_strings(self) -> bool: 

155 return self.categories._can_hold_strings 

156 

157 @cache_readonly 

158 def _should_fallback_to_positional(self) -> bool: 

159 return self.categories._should_fallback_to_positional 

160 

161 _data: Categorical 

162 _values: Categorical 

163 

164 # -------------------------------------------------------------------- 

165 # Categories/Codes/Ordered 

166 

167 @property 

168 def codes(self) -> np.ndarray: 

169 """ 

170 The category codes of this categorical index. 

171 

172 Codes are an array of integers which are the positions of the actual 

173 values in the categories array. There is no setter. 

174 

175 Returns 

176 ------- 

177 ndarray[int] 

178 A non-writable view of the ``codes`` array. 

179 

180 See Also 

181 -------- 

182 Categorical.from_codes : Make a Categorical from codes. 

183 CategoricalIndex : An Index with an underlying ``Categorical``. 

184 

185 Examples 

186 -------- 

187 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a", "b", "c"]) 

188 >>> ci.codes 

189 array([0, 1, 2, 0, 1, 2], dtype=int8) 

190 

191 >>> ci = pd.CategoricalIndex(["a", "c"], categories=["c", "b", "a"]) 

192 >>> ci.codes 

193 array([2, 0], dtype=int8) 

194 """ 

195 return self._data.codes 

196 

197 @property 

198 def categories(self) -> Index: 

199 """ 

200 The categories of this CategoricalIndex. 

201 

202 These are the unique values the index may hold, in category order. 

203 Use the ``*_categories`` methods to return an index with changed 

204 categories. 

205 

206 See Also 

207 -------- 

208 CategoricalIndex.rename_categories : Rename categories. 

209 CategoricalIndex.reorder_categories : Reorder categories. 

210 CategoricalIndex.add_categories : Add new categories. 

211 CategoricalIndex.remove_categories : Remove the specified categories. 

212 CategoricalIndex.remove_unused_categories : Remove categories which 

213 are not used. 

214 CategoricalIndex.set_categories : Set the categories to the specified ones. 

215 

216 Examples 

217 -------- 

218 >>> ci = pd.CategoricalIndex(["a", "c", "b", "a", "c", "b"]) 

219 >>> ci.categories 

220 Index(['a', 'b', 'c'], dtype='str') 

221 

222 >>> ci = pd.CategoricalIndex(["a", "c"], categories=["c", "b", "a"]) 

223 >>> ci.categories 

224 Index(['c', 'b', 'a'], dtype='str') 

225 """ 

226 return self._data.categories 

227 

228 @property 

229 def ordered(self) -> bool | None: 

230 """ 

231 Whether the categories have an ordered relationship. 

232 

233 This property returns True if the categories are ordered, meaning 

234 they have a meaningful order that allows comparison operations. 

235 

236 See Also 

237 -------- 

238 CategoricalIndex.as_ordered : Set the CategoricalIndex to be ordered. 

239 CategoricalIndex.as_unordered : Set the CategoricalIndex to be unordered. 

240 

241 Examples 

242 -------- 

243 >>> ci = pd.CategoricalIndex(["a", "b"], ordered=True) 

244 >>> ci.ordered 

245 True 

246 

247 >>> ci = pd.CategoricalIndex(["a", "b"], ordered=False) 

248 >>> ci.ordered 

249 False 

250 """ 

251 return self._data.ordered 

252 

253 def _wrap_categorical_result(self, result: Categorical) -> Self: 

254 return type(self)._simple_new(result, name=self.name) 

255 

256 def rename_categories(self, new_categories: Any) -> Self: 

257 """ 

258 Rename categories. 

259 

260 This method is commonly used to re-label or adjust the 

261 category names in categorical data without changing the 

262 underlying data. It is useful in situations where you want 

263 to modify the labels used for clarity, consistency, 

264 or readability. 

265 

266 Parameters 

267 ---------- 

268 new_categories : list-like, dict-like or callable 

269 

270 New categories which will replace old categories. 

271 

272 * list-like: all items must be unique and the number of items in 

273 the new categories must match the existing number of categories. 

274 

275 * dict-like: specifies a mapping from 

276 old categories to new. Categories not contained in the mapping 

277 are passed through and extra categories in the mapping are 

278 ignored. 

279 

280 * callable : a callable that is called on all items in the old 

281 categories and whose return values comprise the new categories. 

282 

283 Returns 

284 ------- 

285 CategoricalIndex 

286 CategoricalIndex with renamed categories. 

287 

288 Raises 

289 ------ 

290 ValueError 

291 If new categories are list-like and do not have the same number of 

292 items as the current categories or do not validate as categories 

293 

294 See Also 

295 -------- 

296 CategoricalIndex.reorder_categories : Reorder categories. 

297 CategoricalIndex.add_categories : Add new categories. 

298 CategoricalIndex.remove_categories : Remove the specified categories. 

299 CategoricalIndex.remove_unused_categories : Remove categories which 

300 are not used. 

301 CategoricalIndex.set_categories : Set the categories to the specified 

302 ones. 

303 

304 Examples 

305 -------- 

306 >>> ci = pd.CategoricalIndex(["a", "a", "b"]) 

307 >>> ci.rename_categories([0, 1]) 

308 CategoricalIndex([0, 0, 1], categories=[0, 1], ordered=False, dtype='category') 

309 

310 For dict-like ``new_categories``, extra keys are ignored and 

311 categories not in the dictionary are passed through 

312 

313 >>> ci.rename_categories({"a": "A", "c": "C"}) 

314 CategoricalIndex(['A', 'A', 'b'], categories=['A', 'b'], ordered=False, 

315 dtype='category') 

316 

317 You may also provide a callable to create the new categories 

318 

319 >>> ci.rename_categories(lambda x: x.upper()) 

320 CategoricalIndex(['A', 'A', 'B'], categories=['A', 'B'], ordered=False, 

321 dtype='category') 

322 """ 

323 result = self._data.rename_categories(new_categories) 

324 return self._wrap_categorical_result(result) 

325 

326 def reorder_categories( 

327 self, new_categories: Axes, ordered: bool | None = None 

328 ) -> Self: 

329 """ 

330 Reorder categories as specified in new_categories. 

331 

332 ``new_categories`` need to include all old categories and no new category 

333 items. 

334 

335 Parameters 

336 ---------- 

337 new_categories : Index-like 

338 The categories in new order. 

339 ordered : bool, optional 

340 Whether or not the categorical is treated as an ordered categorical. 

341 If not given, do not change the ordered information. 

342 

343 Returns 

344 ------- 

345 CategoricalIndex 

346 CategoricalIndex with reordered categories. 

347 

348 Raises 

349 ------ 

350 ValueError 

351 If the new categories do not contain all old category items or any 

352 new ones 

353 

354 See Also 

355 -------- 

356 CategoricalIndex.rename_categories : Rename categories. 

357 CategoricalIndex.add_categories : Add new categories. 

358 CategoricalIndex.remove_categories : Remove the specified categories. 

359 CategoricalIndex.remove_unused_categories : Remove categories which 

360 are not used. 

361 CategoricalIndex.set_categories : Set the categories to the specified 

362 ones. 

363 

364 Examples 

365 -------- 

366 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"]) 

367 >>> ci 

368 CategoricalIndex(['a', 'b', 'c', 'a'], categories=['a', 'b', 'c'], 

369 ordered=False, dtype='category') 

370 >>> ci.reorder_categories(["c", "b", "a"], ordered=True) 

371 CategoricalIndex(['a', 'b', 'c', 'a'], categories=['c', 'b', 'a'], 

372 ordered=True, dtype='category') 

373 """ 

374 result = self._data.reorder_categories(new_categories, ordered=ordered) 

375 return self._wrap_categorical_result(result) 

376 

377 def add_categories(self, new_categories: Any) -> Self: 

378 """ 

379 Add new categories. 

380 

381 `new_categories` will be included at the last/highest place in the 

382 categories and will be unused directly after this call. 

383 

384 Parameters 

385 ---------- 

386 new_categories : category or list-like of category 

387 The new categories to be included. 

388 

389 Returns 

390 ------- 

391 CategoricalIndex 

392 CategoricalIndex with new categories added. 

393 

394 Raises 

395 ------ 

396 ValueError 

397 If the new categories include old categories or do not validate as 

398 categories 

399 

400 See Also 

401 -------- 

402 CategoricalIndex.rename_categories : Rename categories. 

403 CategoricalIndex.reorder_categories : Reorder categories. 

404 CategoricalIndex.remove_categories : Remove the specified categories. 

405 CategoricalIndex.remove_unused_categories : Remove categories which 

406 are not used. 

407 CategoricalIndex.set_categories : Set the categories to the specified 

408 ones. 

409 

410 Examples 

411 -------- 

412 >>> ci = pd.CategoricalIndex(["c", "b", "c"]) 

413 >>> ci 

414 CategoricalIndex(['c', 'b', 'c'], categories=['b', 'c'], ordered=False, 

415 dtype='category') 

416 

417 >>> ci.add_categories(["d", "a"]) 

418 CategoricalIndex(['c', 'b', 'c'], categories=['b', 'c', 'd', 'a'], 

419 ordered=False, dtype='category') 

420 """ 

421 result = self._data.add_categories(new_categories) 

422 return self._wrap_categorical_result(result) 

423 

424 def remove_categories(self, removals: Any) -> Self: 

425 """ 

426 Remove the specified categories. 

427 

428 The ``removals`` argument must be a subset of the current categories. 

429 Any values that were part of the removed categories will be set to NaN. 

430 

431 Parameters 

432 ---------- 

433 removals : category or list of categories 

434 The categories which should be removed. 

435 

436 Returns 

437 ------- 

438 CategoricalIndex 

439 CategoricalIndex with removed categories. 

440 

441 Raises 

442 ------ 

443 ValueError 

444 If the removals are not contained in the categories 

445 

446 See Also 

447 -------- 

448 CategoricalIndex.rename_categories : Rename categories. 

449 CategoricalIndex.reorder_categories : Reorder categories. 

450 CategoricalIndex.add_categories : Add new categories. 

451 CategoricalIndex.remove_unused_categories : Remove categories which 

452 are not used. 

453 CategoricalIndex.set_categories : Set the categories to the specified 

454 ones. 

455 

456 Examples 

457 -------- 

458 >>> ci = pd.CategoricalIndex(["a", "c", "b", "c", "d"]) 

459 >>> ci 

460 CategoricalIndex(['a', 'c', 'b', 'c', 'd'], categories=['a', 'b', 'c', 'd'], 

461 ordered=False, dtype='category') 

462 

463 >>> ci.remove_categories(["d", "a"]) 

464 CategoricalIndex([nan, 'c', 'b', 'c', nan], categories=['b', 'c'], 

465 ordered=False, dtype='category') 

466 """ 

467 result = self._data.remove_categories(removals) 

468 return self._wrap_categorical_result(result) 

469 

470 def remove_unused_categories(self) -> Self: 

471 """ 

472 Remove categories which are not used. 

473 

474 This method is useful when working with datasets 

475 that undergo dynamic changes where categories may no longer be 

476 relevant, allowing to maintain a clean, efficient data structure. 

477 

478 Returns 

479 ------- 

480 CategoricalIndex 

481 CategoricalIndex with unused categories dropped. 

482 

483 See Also 

484 -------- 

485 CategoricalIndex.rename_categories : Rename categories. 

486 CategoricalIndex.reorder_categories : Reorder categories. 

487 CategoricalIndex.add_categories : Add new categories. 

488 CategoricalIndex.remove_categories : Remove the specified categories. 

489 CategoricalIndex.set_categories : Set the categories to the specified 

490 ones. 

491 

492 Examples 

493 -------- 

494 >>> ci = pd.CategoricalIndex(["a", "c", "b", "c", "d"]) 

495 >>> ci 

496 CategoricalIndex(['a', 'c', 'b', 'c', 'd'], categories=['a', 'b', 'c', 'd'], 

497 ordered=False, dtype='category') 

498 

499 >>> ci = ci[[0, 1, 0, 1, 1]] 

500 >>> ci 

501 CategoricalIndex(['a', 'c', 'a', 'c', 'c'], categories=['a', 'b', 'c', 'd'], 

502 ordered=False, dtype='category') 

503 

504 >>> ci.remove_unused_categories() 

505 CategoricalIndex(['a', 'c', 'a', 'c', 'c'], categories=['a', 'c'], 

506 ordered=False, dtype='category') 

507 """ 

508 result = self._data.remove_unused_categories() 

509 return self._wrap_categorical_result(result) 

510 

511 def set_categories( 

512 self, 

513 new_categories: Axes, 

514 ordered: bool | None = None, 

515 rename: bool = False, 

516 ) -> Self: 

517 """ 

518 Set the categories to the specified new categories. 

519 

520 ``new_categories`` can include new categories (which will result in 

521 unused categories) or remove old categories (which results in values 

522 set to ``NaN``). If ``rename=True``, the categories will simply be renamed 

523 (less or more items than in old categories will result in values set to 

524 ``NaN`` or in unused categories respectively). 

525 

526 This method can be used to perform more than one action of adding, 

527 removing, and reordering simultaneously and is therefore faster than 

528 performing the individual steps via the more specialised methods. 

529 

530 On the other hand this method does not check whether the old categories 

531 are included in the new categories on a reorder, which can result in 

532 surprising changes. 

533 

534 Parameters 

535 ---------- 

536 new_categories : Index-like 

537 The categories in new order. 

538 ordered : bool, default None 

539 Whether or not the categorical is treated as an ordered categorical. 

540 If not given, do not change the ordered information. 

541 rename : bool, default False 

542 Whether or not the new_categories should be considered as a rename 

543 of the old categories or as reordered categories. 

544 

545 Returns 

546 ------- 

547 CategoricalIndex 

548 CategoricalIndex with the new categories. 

549 

550 Raises 

551 ------ 

552 ValueError 

553 If new_categories does not validate as categories 

554 

555 See Also 

556 -------- 

557 CategoricalIndex.rename_categories : Rename categories. 

558 CategoricalIndex.reorder_categories : Reorder categories. 

559 CategoricalIndex.add_categories : Add new categories. 

560 CategoricalIndex.remove_categories : Remove the specified categories. 

561 CategoricalIndex.remove_unused_categories : Remove categories which 

562 are not used. 

563 

564 Examples 

565 -------- 

566 >>> ci = pd.CategoricalIndex( 

567 ... ["a", "b", "c", None], categories=["a", "b", "c"], ordered=True 

568 ... ) 

569 >>> ci 

570 CategoricalIndex(['a', 'b', 'c', nan], categories=['a', 'b', 'c'], 

571 ordered=True, dtype='category') 

572 

573 >>> ci.set_categories(["A", "b", "c"]) 

574 CategoricalIndex([nan, 'b', 'c', nan], categories=['A', 'b', 'c'], 

575 ordered=True, dtype='category') 

576 >>> ci.set_categories(["A", "b", "c"], rename=True) 

577 CategoricalIndex(['A', 'b', 'c', nan], categories=['A', 'b', 'c'], 

578 ordered=True, dtype='category') 

579 """ 

580 result = self._data.set_categories( 

581 new_categories, ordered=ordered, rename=rename 

582 ) 

583 return self._wrap_categorical_result(result) 

584 

585 def as_ordered(self) -> Self: 

586 """ 

587 Set the CategoricalIndex to be ordered. 

588 

589 This method returns a new CategoricalIndex with the ordered attribute 

590 set to True, enabling comparison operations between categories. 

591 

592 Returns 

593 ------- 

594 CategoricalIndex 

595 Ordered CategoricalIndex. 

596 

597 See Also 

598 -------- 

599 CategoricalIndex.as_unordered : Set the CategoricalIndex to be unordered. 

600 

601 Examples 

602 -------- 

603 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"]) 

604 >>> ci.ordered 

605 False 

606 >>> ci = ci.as_ordered() 

607 >>> ci.ordered 

608 True 

609 """ 

610 result = self._data.as_ordered() 

611 return self._wrap_categorical_result(result) 

612 

613 def as_unordered(self) -> Self: 

614 """ 

615 Set the CategoricalIndex to be unordered. 

616 

617 This method returns a new CategoricalIndex with the ordered attribute 

618 set to False, disabling comparison operations between categories. 

619 

620 Returns 

621 ------- 

622 CategoricalIndex 

623 Unordered CategoricalIndex. 

624 

625 See Also 

626 -------- 

627 CategoricalIndex.as_ordered : Set the CategoricalIndex to be ordered. 

628 

629 Examples 

630 -------- 

631 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"], ordered=True) 

632 >>> ci.ordered 

633 True 

634 >>> ci = ci.as_unordered() 

635 >>> ci.ordered 

636 False 

637 """ 

638 result = self._data.as_unordered() 

639 return self._wrap_categorical_result(result) 

640 

641 def min(self, *, skipna: bool = True, **kwargs: Any) -> Any: # type: ignore[override] 

642 """ 

643 Return the minimum value of the CategoricalIndex. 

644 

645 Only an ordered CategoricalIndex has a minimum. 

646 

647 Parameters 

648 ---------- 

649 skipna : bool, default True 

650 Exclude NA/null values when showing the result. 

651 **kwargs 

652 Additional keyword arguments passed through to the reduction. 

653 

654 Returns 

655 ------- 

656 scalar 

657 The minimum of this CategoricalIndex, NA value if empty. 

658 

659 Raises 

660 ------ 

661 TypeError 

662 If the CategoricalIndex is not ordered. 

663 

664 See Also 

665 -------- 

666 CategoricalIndex.max : Return the maximum value of the CategoricalIndex. 

667 

668 Examples 

669 -------- 

670 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"], ordered=True) 

671 >>> ci.min() 

672 'a' 

673 """ 

674 return self._data.min(skipna=skipna, **kwargs) 

675 

676 def max(self, *, skipna: bool = True, **kwargs: Any) -> Any: # type: ignore[override] 

677 """ 

678 Return the maximum value of the CategoricalIndex. 

679 

680 Only an ordered CategoricalIndex has a maximum. 

681 

682 Parameters 

683 ---------- 

684 skipna : bool, default True 

685 Exclude NA/null values when showing the result. 

686 **kwargs 

687 Additional keyword arguments passed through to the reduction. 

688 

689 Returns 

690 ------- 

691 scalar 

692 The maximum of this CategoricalIndex, NA value if empty. 

693 

694 Raises 

695 ------ 

696 TypeError 

697 If the CategoricalIndex is not ordered. 

698 

699 See Also 

700 -------- 

701 CategoricalIndex.min : Return the minimum value of the CategoricalIndex. 

702 

703 Examples 

704 -------- 

705 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a"], ordered=True) 

706 >>> ci.max() 

707 'c' 

708 """ 

709 return self._data.max(skipna=skipna, **kwargs) 

710 

711 def _reverse_indexer(self) -> dict[Hashable, npt.NDArray[np.intp]]: 

712 """See Categorical._reverse_indexer.""" 

713 return self._data._reverse_indexer() 

714 

715 @property 

716 def _engine_type(self) -> type[libindex.IndexEngine]: 

717 # self.codes can have dtype int8, int16, int32 or int64, so we need 

718 # to return the corresponding engine type (libindex.Int8Engine, etc.). 

719 return { 

720 np.int8: libindex.Int8Engine, 

721 np.int16: libindex.Int16Engine, 

722 np.int32: libindex.Int32Engine, 

723 np.int64: libindex.Int64Engine, 

724 }[self.codes.dtype.type] 

725 

726 # -------------------------------------------------------------------- 

727 # Constructors 

728 

729 def __new__( 

730 cls, 

731 data: Axes | None = None, 

732 categories: Axes | None = None, 

733 ordered: bool | None = None, 

734 dtype: Dtype | None = None, 

735 copy: bool = False, 

736 name: Hashable | None = None, 

737 ) -> Self: 

738 name = maybe_extract_name(name, data, cls) 

739 

740 if is_scalar(data): 

741 # GH#38944 include None here, which pre-2.0 subbed in [] 

742 cls._raise_scalar_data_error(data) 

743 

744 data = Categorical( 

745 data, categories=categories, ordered=ordered, dtype=dtype, copy=copy 

746 ) 

747 

748 return cls._simple_new(data, name=name) 

749 

750 # -------------------------------------------------------------------- 

751 

752 def _is_dtype_compat(self, other: Index) -> Categorical: 

753 """ 

754 *this is an internal non-public method* 

755 

756 provide a comparison between the dtype of self and other (coercing if 

757 needed) 

758 

759 Parameters 

760 ---------- 

761 other : Index 

762 

763 Returns 

764 ------- 

765 Categorical 

766 

767 Raises 

768 ------ 

769 TypeError if the dtypes are not compatible 

770 """ 

771 if isinstance(other.dtype, CategoricalDtype): 

772 cat = extract_array(other) 

773 cat = cast("Categorical", cat) 

774 if not cat._categories_match_up_to_permutation(self._values): 

775 raise TypeError( 

776 "categories must match existing categories when appending" 

777 ) 

778 

779 elif other._is_multi: 

780 # preempt raising NotImplementedError in isna call 

781 raise TypeError("MultiIndex is not dtype-compatible with CategoricalIndex") 

782 else: 

783 values = other 

784 

785 codes = self.categories.get_indexer(values) 

786 if ((codes == -1) & ~values.isna()).any(): 

787 # GH#37667 see test_equals_non_category 

788 raise TypeError( 

789 "categories must match existing categories when appending" 

790 ) 

791 cat = Categorical(other, dtype=self.dtype) 

792 other = CategoricalIndex(cat) 

793 if not other.isin(values).all(): 

794 raise TypeError( 

795 "cannot append a non-category item to a CategoricalIndex" 

796 ) 

797 cat = other._values 

798 

799 return cat 

800 

801 def equals(self, other: object) -> bool: 

802 """ 

803 Determine if two CategoricalIndex objects contain the same elements. 

804 

805 The order and orderedness of elements matters. The categories matter, 

806 but the order of the categories matters only when ``ordered=True``. 

807 

808 Parameters 

809 ---------- 

810 other : object 

811 The CategoricalIndex object to compare with. 

812 

813 Returns 

814 ------- 

815 bool 

816 ``True`` if two :class:`pandas.CategoricalIndex` objects have equal 

817 elements, ``False`` otherwise. 

818 

819 See Also 

820 -------- 

821 Categorical.equals : Returns True if categorical arrays are equal. 

822 

823 Examples 

824 -------- 

825 >>> ci = pd.CategoricalIndex(["a", "b", "c", "a", "b", "c"]) 

826 >>> ci2 = pd.CategoricalIndex(pd.Categorical(["a", "b", "c", "a", "b", "c"])) 

827 >>> ci.equals(ci2) 

828 True 

829 

830 The order of elements matters. 

831 

832 >>> ci3 = pd.CategoricalIndex(["c", "b", "a", "a", "b", "c"]) 

833 >>> ci.equals(ci3) 

834 False 

835 

836 The orderedness also matters. 

837 

838 >>> ci4 = ci.as_ordered() 

839 >>> ci.equals(ci4) 

840 False 

841 

842 The categories matter, but the order of the categories matters only when 

843 ``ordered=True``. 

844 

845 >>> ci5 = ci.set_categories(["a", "b", "c", "d"]) 

846 >>> ci.equals(ci5) 

847 False 

848 

849 >>> ci6 = ci.set_categories(["b", "c", "a"]) 

850 >>> ci.equals(ci6) 

851 True 

852 >>> ci_ordered = pd.CategoricalIndex( 

853 ... ["a", "b", "c", "a", "b", "c"], ordered=True 

854 ... ) 

855 >>> ci2_ordered = ci_ordered.set_categories(["b", "c", "a"]) 

856 >>> ci_ordered.equals(ci2_ordered) 

857 False 

858 """ 

859 if self.is_(other): # type: ignore[arg-type] 

860 return True 

861 

862 if not isinstance(other, Index): 

863 return False 

864 

865 try: 

866 other = self._is_dtype_compat(other) 

867 except (TypeError, ValueError): 

868 return False 

869 

870 return self._data.equals(other) 

871 

872 # -------------------------------------------------------------------- 

873 # Rendering Methods 

874 

875 def _formatter_func(self, val: Hashable) -> str: 

876 return self.categories._formatter_func(val) 

877 

878 def _format_attrs(self) -> list[tuple[str, str | int | bool | None]]: 

879 """ 

880 Return a list of tuples of the (attr,formatted_value) 

881 """ 

882 attrs: list[tuple[str, str | int | bool | None]] 

883 

884 attrs = [ 

885 ( 

886 "categories", 

887 f"[{', '.join(self._data._repr_categories())}]", 

888 ), 

889 ("ordered", self.ordered), 

890 ] 

891 extra = super()._format_attrs() 

892 return attrs + extra 

893 

894 # -------------------------------------------------------------------- 

895 

896 @property 

897 def inferred_type(self) -> str: 

898 return "categorical" 

899 

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

901 """ 

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

903 

904 Parameters 

905 ---------- 

906 key : label 

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

908 

909 Returns 

910 ------- 

911 bool 

912 Whether the key search is in the index. 

913 

914 Raises 

915 ------ 

916 TypeError 

917 If the key is not hashable. 

918 

919 See Also 

920 -------- 

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

922 list-like key is in the index. 

923 

924 Examples 

925 -------- 

926 >>> idx = pd.Index([1, 2, 3, 4]) 

927 >>> idx 

928 Index([1, 2, 3, 4], dtype='int64') 

929 

930 >>> 2 in idx 

931 True 

932 >>> 6 in idx 

933 False 

934 """ 

935 # if key is a NaN, check if any NaN is in self. 

936 if is_valid_na_for_dtype(key, self.categories.dtype): 

937 return self.hasnans 

938 if self.categories._typ == "rangeindex": 

939 container: Index | libindex.IndexEngine | libindex.ExtensionEngine = ( 

940 self.categories 

941 ) 

942 else: 

943 container = self._engine 

944 return contains(self, key, container=container) 

945 

946 def reindex( 

947 self, 

948 target: Axes, 

949 method: ReindexMethod | None = None, 

950 level: Level | None = None, 

951 limit: int | None = None, 

952 tolerance: float | None = None, 

953 ) -> tuple[Index, npt.NDArray[np.intp] | None]: 

954 """ 

955 Create index with target's values (move/add/delete values as necessary) 

956 

957 Returns 

958 ------- 

959 new_index : pd.Index 

960 Resulting index 

961 indexer : np.ndarray[np.intp] or None 

962 Indices of output values in original index 

963 

964 """ 

965 if method is not None: 

966 raise NotImplementedError( 

967 "argument method is not implemented for CategoricalIndex.reindex" 

968 ) 

969 if level is not None: 

970 raise NotImplementedError( 

971 "argument level is not implemented for CategoricalIndex.reindex" 

972 ) 

973 if limit is not None: 

974 raise NotImplementedError( 

975 "argument limit is not implemented for CategoricalIndex.reindex" 

976 ) 

977 return super().reindex(target) 

978 

979 # -------------------------------------------------------------------- 

980 # Indexing Methods 

981 

982 def _maybe_cast_indexer(self, key: Hashable) -> int: 

983 # GH#41933: we have to do this instead of self._data._validate_scalar 

984 # because this will correctly get partial-indexing on Interval categories 

985 try: 

986 return self._data._unbox_scalar(key) 

987 except KeyError: 

988 if is_valid_na_for_dtype(key, self.categories.dtype): 

989 return -1 

990 raise 

991 

992 def _maybe_cast_listlike_indexer(self, values: Axes) -> CategoricalIndex: 

993 if isinstance(values, CategoricalIndex): 

994 values = values._data 

995 if isinstance(values, Categorical): 

996 # Indexing on codes is more efficient if categories are the same, 

997 # so we can apply some optimizations based on the degree of 

998 # dtype-matching. 

999 cat = self._data._encode_with_my_categories(values) 

1000 codes = cat._codes 

1001 else: 

1002 codes = self.categories.get_indexer(values) 

1003 codes = codes.astype(self.codes.dtype, copy=False) 

1004 cat = self._data._from_backing_data(codes) 

1005 return type(self)._simple_new(cat) 

1006 

1007 # -------------------------------------------------------------------- 

1008 

1009 def _is_comparable_dtype(self, dtype: DtypeObj) -> bool: 

1010 return self.categories._is_comparable_dtype(dtype) 

1011 

1012 def _intersection( 

1013 self, other: Index, sort: bool = False 

1014 ) -> Index | ArrayLike | MultiIndex: 

1015 # Reached only via Index.intersection after dtype reconciliation, so 

1016 # other is necessarily a CategoricalIndex with matching dtype. 

1017 # For unordered CategoricalIndex, libjoin operates on integer codes. 

1018 # When two indexes have the same categories in different order, matching 

1019 # codes map to different values, giving incorrect results (GH#55335). 

1020 # Reorder other's categories to match self so the codes align. 

1021 other = cast("CategoricalIndex", other) 

1022 if not self.ordered and not self.categories.equals(other.categories): 

1023 reordered = other._data.reorder_categories(self.categories) 

1024 other = other._shallow_copy(reordered) 

1025 return super()._intersection(other, sort=sort) 

1026 

1027 def _union(self, other: Index, sort: bool | None) -> Index | ArrayLike | MultiIndex: 

1028 # See _intersection for explanation of GH#55335. 

1029 other = cast("CategoricalIndex", other) 

1030 if not self.ordered and not self.categories.equals(other.categories): 

1031 reordered = other._data.reorder_categories(self.categories) 

1032 other = other._shallow_copy(reordered) 

1033 return super()._union(other, sort) 

1034 

1035 def map( 

1036 self, 

1037 mapper: Callable[..., Any] | dict[Hashable, Any] | Series, 

1038 na_action: Literal["ignore"] | None = None, 

1039 ) -> Index: 

1040 """ 

1041 Map values using input an input mapping or function. 

1042 

1043 Maps the values (their categories, not the codes) of the index to new 

1044 categories. If the mapping correspondence is one-to-one the result is a 

1045 :class:`~pandas.CategoricalIndex` which has the same order property as 

1046 the original, otherwise an :class:`~pandas.Index` is returned. 

1047 

1048 If a `dict` or :class:`~pandas.Series` is used any unmapped category is 

1049 mapped to `NaN`. Note that if this happens an :class:`~pandas.Index` 

1050 will be returned. 

1051 

1052 Parameters 

1053 ---------- 

1054 mapper : function, dict, or Series 

1055 Mapping correspondence. 

1056 na_action : {None, 'ignore'}, default 'ignore' 

1057 If 'ignore', propagate NaN values, without passing them to 

1058 the mapping correspondence. 

1059 

1060 Returns 

1061 ------- 

1062 pandas.CategoricalIndex or pandas.Index 

1063 Mapped index. 

1064 

1065 See Also 

1066 -------- 

1067 Index.map : Apply a mapping correspondence on an 

1068 :class:`~pandas.Index`. 

1069 Series.map : Apply a mapping correspondence on a 

1070 :class:`~pandas.Series`. 

1071 Series.apply : Apply more complex functions on a 

1072 :class:`~pandas.Series`. 

1073 

1074 Examples 

1075 -------- 

1076 >>> idx = pd.CategoricalIndex(["a", "b", "c"]) 

1077 >>> idx 

1078 CategoricalIndex(['a', 'b', 'c'], categories=['a', 'b', 'c'], 

1079 ordered=False, dtype='category') 

1080 >>> idx.map(lambda x: x.upper()) 

1081 CategoricalIndex(['A', 'B', 'C'], categories=['A', 'B', 'C'], 

1082 ordered=False, dtype='category') 

1083 >>> idx.map({"a": "first", "b": "second", "c": "third"}) 

1084 CategoricalIndex(['first', 'second', 'third'], categories=['first', 

1085 'second', 'third'], ordered=False, dtype='category') 

1086 

1087 If the mapping is one-to-one the ordering of the categories is 

1088 preserved: 

1089 

1090 >>> idx = pd.CategoricalIndex(["a", "b", "c"], ordered=True) 

1091 >>> idx 

1092 CategoricalIndex(['a', 'b', 'c'], categories=['a', 'b', 'c'], 

1093 ordered=True, dtype='category') 

1094 >>> idx.map({"a": 3, "b": 2, "c": 1}) 

1095 CategoricalIndex([3, 2, 1], categories=[3, 2, 1], ordered=True, 

1096 dtype='category') 

1097 

1098 If the mapping is not one-to-one an :class:`~pandas.Index` is returned: 

1099 

1100 >>> idx.map({"a": "first", "b": "second", "c": "first"}) 

1101 Index(['first', 'second', 'first'], dtype='str') 

1102 

1103 If a `dict` is used, all unmapped categories are mapped to `NaN` and 

1104 the result is an :class:`~pandas.Index`: 

1105 

1106 >>> idx.map({"a": "first", "b": "second"}) 

1107 Index(['first', 'second', nan], dtype='str') 

1108 """ 

1109 mapped = self._values.map(mapper, na_action=na_action) 

1110 return Index(mapped, name=self.name, copy=False)