1from __future__ import annotations
2
3from collections import abc
4from datetime import datetime
5import functools
6from itertools import zip_longest
7import operator
8from typing import (
9 TYPE_CHECKING,
10 Any,
11 ClassVar,
12 Literal,
13 NoReturn,
14 Self,
15 cast,
16 final,
17 overload,
18)
19import warnings
20
21import numpy as np
22
23from pandas._config import (
24 get_option,
25 is_nan_na,
26 using_string_dtype,
27)
28
29from pandas._libs import (
30 NaT,
31 algos as libalgos,
32 index as libindex,
33 lib,
34 writers,
35)
36from pandas._libs.internals import BlockValuesRefs
37import pandas._libs.join as libjoin
38from pandas._libs.lib import (
39 is_datetime_array,
40 no_default,
41)
42from pandas._libs.tslibs import (
43 OutOfBoundsDatetime,
44 Timestamp,
45 tz_compare,
46)
47from pandas._typing import (
48 AnyAll,
49 ArrayLike,
50 Axes,
51 Axis,
52 AxisInt,
53 DropKeep,
54 Dtype,
55 DtypeObj,
56 F,
57 IgnoreRaise,
58 IndexLabel,
59 IndexT,
60 JoinHow,
61 Level,
62 NaPosition,
63 ReindexMethod,
64 Shape,
65 SliceType,
66 npt,
67)
68from pandas.compat.numpy import function as nv
69from pandas.errors import (
70 DuplicateLabelError,
71 InvalidIndexError,
72 Pandas4Warning,
73)
74from pandas.util._decorators import (
75 cache_readonly,
76 set_module,
77)
78from pandas.util._exceptions import (
79 find_stack_level,
80 rewrite_exception,
81)
82
83from pandas.core.dtypes.astype import (
84 astype_array,
85 astype_is_view,
86)
87from pandas.core.dtypes.cast import (
88 LossySetitemError,
89 can_hold_element,
90 common_dtype_categorical_compat,
91 find_result_type,
92 infer_dtype_from,
93 maybe_unbox_numpy_scalar,
94 np_can_hold_element,
95)
96from pandas.core.dtypes.common import (
97 ensure_int64,
98 ensure_object,
99 ensure_platform_int,
100 is_any_real_numeric_dtype,
101 is_bool_dtype,
102 is_ea_or_datetimelike_dtype,
103 is_float,
104 is_hashable,
105 is_integer,
106 is_iterator,
107 is_list_like,
108 is_numeric_dtype,
109 is_object_dtype,
110 is_scalar,
111 is_signed_integer_dtype,
112 is_string_dtype,
113 needs_i8_conversion,
114 pandas_dtype,
115 validate_all_hashable,
116)
117from pandas.core.dtypes.concat import concat_compat
118from pandas.core.dtypes.dtypes import (
119 ArrowDtype,
120 CategoricalDtype,
121 DatetimeTZDtype,
122 ExtensionDtype,
123 IntervalDtype,
124 PeriodDtype,
125 SparseDtype,
126)
127from pandas.core.dtypes.generic import (
128 ABCCategoricalIndex,
129 ABCDataFrame,
130 ABCDatetimeIndex,
131 ABCIntervalIndex,
132 ABCMultiIndex,
133 ABCPeriodIndex,
134 ABCRangeIndex,
135 ABCSeries,
136 ABCTimedeltaIndex,
137)
138from pandas.core.dtypes.inference import is_dict_like
139from pandas.core.dtypes.missing import (
140 array_equivalent,
141 is_valid_na_for_dtype,
142 isna,
143)
144
145from pandas.core import (
146 arraylike,
147 nanops,
148 ops,
149)
150from pandas.core.accessor import Accessor
151import pandas.core.algorithms as algos
152from pandas.core.array_algos.putmask import (
153 setitem_datetimelike_compat,
154 validate_putmask,
155)
156from pandas.core.arrays import (
157 ArrowExtensionArray,
158 BaseMaskedArray,
159 Categorical,
160 DatetimeArray,
161 ExtensionArray,
162 TimedeltaArray,
163)
164from pandas.core.arrays.floating import FloatingDtype
165from pandas.core.arrays.string_ import (
166 StringArray,
167 StringDtype,
168)
169from pandas.core.base import (
170 IndexOpsMixin,
171 PandasObject,
172)
173import pandas.core.common as com
174from pandas.core.construction import (
175 ensure_wrapped_if_datetimelike,
176 extract_array,
177 sanitize_array,
178)
179from pandas.core.indexers import (
180 disallow_ndim_indexing,
181 is_valid_positional_slice,
182)
183from pandas.core.indexes.frozen import FrozenList
184from pandas.core.missing import clean_reindex_fill_method
185from pandas.core.ops import get_op_result_name
186from pandas.core.sorting import (
187 ensure_key_mapped,
188 get_group_index_sorter,
189 nargsort,
190)
191from pandas.core.strings.accessor import StringMethods
192
193from pandas.io.formats.printing import (
194 PrettyDict,
195 default_pprint,
196 format_object_summary,
197 pprint_thing,
198)
199
200if TYPE_CHECKING:
201 from collections.abc import (
202 Callable,
203 Hashable,
204 Iterable,
205 Sequence,
206 )
207
208 from pandas import (
209 CategoricalIndex,
210 DataFrame,
211 MultiIndex,
212 Series,
213 )
214 from pandas.core.arrays import (
215 IntervalArray,
216 PeriodArray,
217 )
218
219__all__ = ["Index"]
220
221_unsortable_types = frozenset(("mixed", "mixed-integer"))
222
223_index_doc_kwargs: dict[str, str] = {
224 "klass": "Index",
225 "inplace": "",
226 "target_klass": "Index",
227 "raises_section": "",
228 "unique": "Index",
229 "duplicated": "np.ndarray",
230}
231_index_shared_docs: dict[str, str] = {}
232str_t = str
233
234_dtype_obj = np.dtype("object")
235
236_masked_engines = {
237 "Complex128": libindex.MaskedComplex128Engine,
238 "Complex64": libindex.MaskedComplex64Engine,
239 "Float64": libindex.MaskedFloat64Engine,
240 "Float32": libindex.MaskedFloat32Engine,
241 "UInt64": libindex.MaskedUInt64Engine,
242 "UInt32": libindex.MaskedUInt32Engine,
243 "UInt16": libindex.MaskedUInt16Engine,
244 "UInt8": libindex.MaskedUInt8Engine,
245 "Int64": libindex.MaskedInt64Engine,
246 "Int32": libindex.MaskedInt32Engine,
247 "Int16": libindex.MaskedInt16Engine,
248 "Int8": libindex.MaskedInt8Engine,
249 "boolean": libindex.MaskedBoolEngine,
250 "double[pyarrow]": libindex.MaskedFloat64Engine,
251 "float64[pyarrow]": libindex.MaskedFloat64Engine,
252 "float32[pyarrow]": libindex.MaskedFloat32Engine,
253 "float[pyarrow]": libindex.MaskedFloat32Engine,
254 "uint64[pyarrow]": libindex.MaskedUInt64Engine,
255 "uint32[pyarrow]": libindex.MaskedUInt32Engine,
256 "uint16[pyarrow]": libindex.MaskedUInt16Engine,
257 "uint8[pyarrow]": libindex.MaskedUInt8Engine,
258 "int64[pyarrow]": libindex.MaskedInt64Engine,
259 "int32[pyarrow]": libindex.MaskedInt32Engine,
260 "int16[pyarrow]": libindex.MaskedInt16Engine,
261 "int8[pyarrow]": libindex.MaskedInt8Engine,
262 "bool[pyarrow]": libindex.MaskedBoolEngine,
263}
264
265
266def _maybe_return_indexers(meth: F) -> F:
267 """
268 Decorator to simplify 'return_indexers' checks in Index.join.
269 """
270
271 @functools.wraps(meth)
272 def join(
273 self,
274 other: Index,
275 *,
276 how: JoinHow = "left",
277 level=None,
278 return_indexers: bool = False,
279 sort: bool = False,
280 ):
281 join_index, lidx, ridx = meth(self, other, how=how, level=level, sort=sort)
282 if not return_indexers:
283 return join_index
284
285 if lidx is not None:
286 lidx = ensure_platform_int(lidx)
287 if ridx is not None:
288 ridx = ensure_platform_int(ridx)
289 return join_index, lidx, ridx
290
291 return cast(F, join)
292
293
294def _new_Index(cls, d):
295 """
296 This is called upon unpickling, rather than the default which doesn't
297 have arguments and breaks __new__.
298 """
299 # required for backward compat, because PI can't be instantiated with
300 # ordinals through __new__ GH #13277
301 d["copy"] = False
302 if issubclass(cls, ABCPeriodIndex):
303 from pandas.core.indexes.period import _new_PeriodIndex
304
305 return _new_PeriodIndex(cls, **d)
306
307 if issubclass(cls, ABCMultiIndex):
308 if "labels" in d and "codes" not in d:
309 # GH#23752 "labels" kwarg has been replaced with "codes"
310 d["codes"] = d.pop("labels")
311
312 # Since this was a valid MultiIndex at pickle-time, we don't need to
313 # check validty at un-pickle time.
314 d["verify_integrity"] = False
315
316 elif "dtype" not in d and "data" in d:
317 # Prevent Index.__new__ from conducting inference;
318 # "data" key not in RangeIndex
319 d["dtype"] = d["data"].dtype
320 return cls.__new__(cls, **d)
321
322
323@set_module("pandas")
324class Index(IndexOpsMixin, PandasObject):
325 """
326 Immutable sequence used for indexing and alignment.
327
328 The basic object storing axis labels for all pandas objects.
329
330 .. versionchanged:: 2.0.0
331
332 Index can hold all numpy numeric dtypes (except float16). Previously only
333 int64/uint64/float64 dtypes were accepted.
334
335 Parameters
336 ----------
337 data : array-like (1-dimensional)
338 An array-like structure containing the data for the index. This could be a
339 Python list, a NumPy array, or a pandas Series.
340 dtype : str, numpy.dtype, or ExtensionDtype, optional
341 Data type for the output Index. If not specified, this will be
342 inferred from `data`.
343 See the :ref:`user guide <basics.dtypes>` for more usages.
344 copy : bool, default None
345 Whether to copy input data, only relevant for array, Series, and Index
346 inputs (for other input, e.g. a list, a new array is created anyway).
347 Defaults to True for array input and False for Index/Series.
348 Set to False to avoid copying array input at your own risk (if you
349 know the input data won't be modified elsewhere).
350 Set to True to force copying Series/Index input up front.
351 name : object
352 Name to be stored in the index.
353 tupleize_cols : bool (default: True)
354 When True, attempt to create a MultiIndex if possible.
355
356 See Also
357 --------
358 RangeIndex : Index implementing a monotonic integer range.
359 CategoricalIndex : Index of :class:`Categorical` s.
360 MultiIndex : A multi-level, or hierarchical Index.
361 IntervalIndex : An Index of :class:`Interval` s.
362 DatetimeIndex : Index of datetime64 data.
363 TimedeltaIndex : Index of timedelta64 data.
364 PeriodIndex : Index of Period data.
365
366 Notes
367 -----
368 An Index instance can **only** contain hashable objects.
369 An Index instance *can not* hold numpy float16 dtype.
370
371 Examples
372 --------
373 >>> pd.Index([1, 2, 3])
374 Index([1, 2, 3], dtype='int64')
375
376 >>> pd.Index(list("abc"))
377 Index(['a', 'b', 'c'], dtype='str')
378
379 >>> pd.Index([1, 2, 3], dtype="uint8")
380 Index([1, 2, 3], dtype='uint8')
381 """
382
383 # similar to __array_priority__, positions Index after Series and DataFrame
384 # but before ExtensionArray. Should NOT be overridden by subclasses.
385 __pandas_priority__ = 2000
386
387 # Cython methods; see github.com/cython/cython/issues/2647
388 # for why we need to wrap these instead of making them class attributes
389 # Moreover, cython will choose the appropriate-dtyped sub-function
390 # given the dtypes of the passed arguments
391
392 @final
393 def _left_indexer_unique(self, other: Self) -> npt.NDArray[np.intp]:
394 # Caller is responsible for ensuring other.dtype == self.dtype
395 sv = self._get_join_target()
396 ov = other._get_join_target()
397 # similar but not identical to ov.searchsorted(sv)
398 return libjoin.left_join_indexer_unique(sv, ov)
399
400 @final
401 def _left_indexer(
402 self, other: Self
403 ) -> tuple[ArrayLike, npt.NDArray[np.intp], npt.NDArray[np.intp]]:
404 # Caller is responsible for ensuring other.dtype == self.dtype
405 sv = self._get_join_target()
406 ov = other._get_join_target()
407 joined_ndarray, lidx, ridx = libjoin.left_join_indexer(sv, ov)
408 joined = self._from_join_target(joined_ndarray)
409 return joined, lidx, ridx
410
411 @final
412 def _inner_indexer(
413 self, other: Self
414 ) -> tuple[ArrayLike, npt.NDArray[np.intp], npt.NDArray[np.intp]]:
415 # Caller is responsible for ensuring other.dtype == self.dtype
416 sv = self._get_join_target()
417 ov = other._get_join_target()
418 joined_ndarray, lidx, ridx = libjoin.inner_join_indexer(sv, ov)
419 joined = self._from_join_target(joined_ndarray)
420 return joined, lidx, ridx
421
422 @final
423 def _outer_indexer(
424 self, other: Self
425 ) -> tuple[ArrayLike, npt.NDArray[np.intp], npt.NDArray[np.intp]]:
426 # Caller is responsible for ensuring other.dtype == self.dtype
427 sv = self._get_join_target()
428 ov = other._get_join_target()
429 joined_ndarray, lidx, ridx = libjoin.outer_join_indexer(sv, ov)
430 joined = self._from_join_target(joined_ndarray)
431 return joined, lidx, ridx
432
433 _typ: str = "index"
434 _data: ExtensionArray | np.ndarray
435 _data_cls: type[ExtensionArray] | tuple[type[np.ndarray], type[ExtensionArray]] = (
436 np.ndarray,
437 ExtensionArray,
438 )
439 _id: object | None = None
440 _name: Hashable = None
441 # MultiIndex.levels previously allowed setting the index name. We
442 # don't allow this anymore, and raise if it happens rather than
443 # failing silently.
444 _no_setting_name: bool = False
445 _comparables: list[str] = ["name"]
446 _attributes: list[str] = ["name"]
447
448 @cache_readonly
449 def _can_hold_strings(self) -> bool:
450 return not is_numeric_dtype(self.dtype)
451
452 _engine_types: dict[np.dtype | ExtensionDtype, type[libindex.IndexEngine]] = {
453 np.dtype(np.int8): libindex.Int8Engine,
454 np.dtype(np.int16): libindex.Int16Engine,
455 np.dtype(np.int32): libindex.Int32Engine,
456 np.dtype(np.int64): libindex.Int64Engine,
457 np.dtype(np.uint8): libindex.UInt8Engine,
458 np.dtype(np.uint16): libindex.UInt16Engine,
459 np.dtype(np.uint32): libindex.UInt32Engine,
460 np.dtype(np.uint64): libindex.UInt64Engine,
461 np.dtype(np.float32): libindex.Float32Engine,
462 np.dtype(np.float64): libindex.Float64Engine,
463 np.dtype(np.complex64): libindex.Complex64Engine,
464 np.dtype(np.complex128): libindex.Complex128Engine,
465 }
466
467 @property
468 def _engine_type(
469 self,
470 ) -> type[libindex.IndexEngine | libindex.ExtensionEngine]:
471 return self._engine_types.get(self.dtype, libindex.ObjectEngine)
472
473 # whether we support partial string indexing. Overridden
474 # in DatetimeIndex and PeriodIndex
475 _supports_partial_string_indexing = False
476
477 _accessors = {"str"}
478
479 str = Accessor("str", StringMethods)
480
481 _references: BlockValuesRefs | None = None
482
483 # --------------------------------------------------------------------
484 # Constructors
485
486 def __new__(
487 cls,
488 data=None,
489 dtype=None,
490 copy: bool | None = None,
491 name=None,
492 tupleize_cols: bool = True,
493 ) -> Self:
494 from pandas.core.indexes.range import RangeIndex
495
496 name = maybe_extract_name(name, data, cls)
497
498 if dtype is not None:
499 dtype = pandas_dtype(dtype)
500
501 data_dtype = getattr(data, "dtype", None)
502
503 refs = None
504 if not copy and isinstance(data, (ABCSeries, Index)):
505 refs = data._references
506
507 # GH 63306, GH 63388
508 data, copy = cls._maybe_copy_array_input(data, copy, dtype)
509
510 # range
511 if isinstance(data, (range, RangeIndex)):
512 result = RangeIndex(start=data, copy=bool(copy), name=name)
513 if dtype is not None:
514 return result.astype(dtype, copy=False)
515 # error: Incompatible return value type (got "MultiIndex",
516 # expected "Self")
517 return result # type: ignore[return-value]
518
519 elif is_ea_or_datetimelike_dtype(dtype):
520 # non-EA dtype indexes have special casting logic, so we punt here
521 if isinstance(data, (set, frozenset)):
522 data = list(data)
523
524 elif is_ea_or_datetimelike_dtype(data_dtype):
525 pass
526
527 elif isinstance(data, (np.ndarray, ABCMultiIndex)):
528 if isinstance(data, ABCMultiIndex):
529 data = data._values
530
531 if data.dtype.kind not in "iufcbmM":
532 # GH#11836 we need to avoid having numpy coerce
533 # things that look like ints/floats to ints unless
534 # they are actually ints, e.g. '0' and 0.0
535 # should not be coerced
536 data = com.asarray_tuplesafe(data, dtype=_dtype_obj)
537 elif isinstance(data, (ABCSeries, Index)):
538 # GH 56244: Avoid potential inference on object types
539 pass
540 elif is_scalar(data):
541 raise cls._raise_scalar_data_error(data)
542 elif hasattr(data, "__array__"):
543 return cls(np.asarray(data), dtype=dtype, copy=copy, name=name)
544 elif not is_list_like(data) and not isinstance(data, memoryview):
545 # 2022-11-16 the memoryview check is only necessary on some CI
546 # builds, not clear why
547 raise cls._raise_scalar_data_error(data)
548
549 else:
550 if tupleize_cols:
551 # GH21470: convert iterable to list before determining if empty
552 if is_iterator(data):
553 data = list(data)
554
555 if data and all(isinstance(e, tuple) for e in data):
556 # we must be all tuples, otherwise don't construct
557 # 10697
558 from pandas.core.indexes.multi import MultiIndex
559
560 # error: Incompatible return value type (got "MultiIndex",
561 # expected "Self")
562 return MultiIndex.from_tuples( # type: ignore[return-value]
563 data, names=name
564 )
565 # other iterable of some kind
566
567 if not isinstance(data, (list, tuple)):
568 # we allow set/frozenset, which Series/sanitize_array does not, so
569 # cast to list here
570 data = list(data)
571 if len(data) == 0:
572 # unlike Series, we default to object dtype:
573 data = np.array(data, dtype=object)
574
575 if len(data) and isinstance(data[0], tuple):
576 # Ensure we get 1-D array of tuples instead of 2D array.
577 data = com.asarray_tuplesafe(data, dtype=_dtype_obj)
578
579 try:
580 arr = sanitize_array(data, None, dtype=dtype, copy=bool(copy))
581 except ValueError as err:
582 if "index must be specified when data is not list-like" in str(err):
583 raise cls._raise_scalar_data_error(data) from err
584 if "Data must be 1-dimensional" in str(err):
585 raise ValueError("Index data must be 1-dimensional") from err
586 raise
587 arr = ensure_wrapped_if_datetimelike(arr)
588
589 klass = cls._dtype_to_subclass(arr.dtype)
590
591 arr = klass._ensure_array(arr, arr.dtype, copy=False)
592 return klass._simple_new(arr, name, refs=refs)
593
594 @classmethod
595 def _ensure_array(cls, data, dtype, copy: bool):
596 """
597 Ensure we have a valid array to pass to _simple_new.
598 """
599 if data.ndim > 1:
600 # GH#13601, GH#20285, GH#27125
601 raise ValueError("Index data must be 1-dimensional")
602 elif dtype == np.float16:
603 # float16 not supported (no indexing engine)
604 raise NotImplementedError("float16 indexes are not supported")
605
606 if copy:
607 # asarray_tuplesafe does not always copy underlying data,
608 # so need to make sure that this happens
609 data = data.copy()
610 return data
611
612 @final
613 @classmethod
614 def _dtype_to_subclass(cls, dtype: DtypeObj):
615 # Delay import for perf. https://github.com/pandas-dev/pandas/pull/31423
616
617 if isinstance(dtype, ExtensionDtype):
618 return dtype.index_class
619
620 if dtype.kind == "M":
621 from pandas import DatetimeIndex
622
623 return DatetimeIndex
624
625 elif dtype.kind == "m":
626 from pandas import TimedeltaIndex
627
628 return TimedeltaIndex
629
630 elif dtype.kind == "O":
631 # NB: assuming away MultiIndex
632 return Index
633
634 elif issubclass(dtype.type, str) or is_numeric_dtype(dtype):
635 return Index
636
637 raise NotImplementedError(dtype)
638
639 # NOTE for new Index creation:
640
641 # - _simple_new: It returns new Index with the same type as the caller.
642 # All metadata (such as name) must be provided by caller's responsibility.
643 # Using _shallow_copy is recommended because it fills these metadata
644 # otherwise specified.
645
646 # - _shallow_copy: It returns new Index with the same type (using
647 # _simple_new), but fills caller's metadata otherwise specified. Passed
648 # kwargs will overwrite corresponding metadata.
649
650 # See each method's docstring.
651
652 @classmethod
653 def _simple_new(
654 cls,
655 values: ArrayLike,
656 name: Hashable | None = None,
657 refs: BlockValuesRefs | None = None,
658 ) -> Self:
659 """
660 We require that we have a dtype compat for the values. If we are passed
661 a non-dtype compat, then coerce using the constructor.
662
663 Must be careful not to recurse.
664 """
665 assert isinstance(values, cls._data_cls), type(values)
666
667 result = object.__new__(cls)
668 result._data = values
669 result._name = name
670 result._cache = {}
671 result._reset_identity()
672 if refs is not None:
673 result._references = refs
674 else:
675 result._references = BlockValuesRefs()
676 result._references.add_index_reference(result)
677
678 return result
679
680 @classmethod
681 def _with_infer(cls, *args, **kwargs):
682 """
683 Constructor that uses the 1.0.x behavior inferring numeric dtypes
684 for ndarray[object] inputs.
685 """
686 result = cls(*args, **kwargs)
687
688 if result.dtype == _dtype_obj and not result._is_multi:
689 # error: Argument 1 to "maybe_convert_objects" has incompatible type
690 # "Union[ExtensionArray, ndarray[Any, Any]]"; expected
691 # "ndarray[Any, Any]"
692 values = lib.maybe_convert_objects(result._values) # type: ignore[arg-type]
693 if values.dtype.kind in "iufb":
694 return Index(values, name=result.name, copy=False)
695
696 return result
697
698 @cache_readonly
699 def _constructor(self) -> type[Self]:
700 return type(self)
701
702 @final
703 def _maybe_check_unique(self) -> None:
704 """
705 Check that an Index has no duplicates.
706
707 This is typically only called via
708 `NDFrame.flags.allows_duplicate_labels.setter` when it's set to
709 True (duplicates aren't allowed).
710
711 Raises
712 ------
713 DuplicateLabelError
714 When the index is not unique.
715 """
716 if not self.is_unique:
717 msg = """Index has duplicates."""
718 duplicates = self._format_duplicate_message()
719 msg += f"\n{duplicates}"
720
721 raise DuplicateLabelError(msg)
722
723 @final
724 def _format_duplicate_message(self) -> DataFrame:
725 """
726 Construct the DataFrame for a DuplicateLabelError.
727
728 This returns a DataFrame indicating the labels and positions
729 of duplicates in an index. This should only be called when it's
730 already known that duplicates are present.
731
732 Examples
733 --------
734 >>> idx = pd.Index(["a", "b", "a"])
735 >>> idx._format_duplicate_message()
736 positions
737 label
738 a [0, 2]
739 """
740 from pandas import Series
741
742 duplicates = self[self.duplicated(keep="first")].unique()
743 assert len(duplicates)
744
745 out = (
746 Series(np.arange(len(self)), copy=False)
747 .groupby(self, observed=False)
748 .agg(list)[duplicates]
749 )
750 if self._is_multi:
751 # test_format_duplicate_labels_message_multi
752 # error: "Type[Index]" has no attribute "from_tuples" [attr-defined]
753 out.index = type(self).from_tuples(out.index) # type: ignore[attr-defined]
754
755 if self.nlevels == 1:
756 out = out.rename_axis("label")
757 return out.to_frame(name="positions")
758
759 # --------------------------------------------------------------------
760 # Index Internals Methods
761
762 def _shallow_copy(self, values, name: Hashable = no_default) -> Self:
763 """
764 Create a new Index with the same class as the caller, don't copy the
765 data, use the same object attributes with passed in attributes taking
766 precedence.
767
768 *this is an internal non-public method*
769
770 Parameters
771 ----------
772 values : the values to create the new Index, optional
773 name : Label, defaults to self.name
774 """
775 name = self._name if name is no_default else name
776
777 return self._simple_new(values, name=name, refs=self._references)
778
779 def _view(self) -> Self:
780 """
781 fastpath to make a shallow copy, i.e. new object with same data.
782 """
783 result = self._simple_new(self._values, name=self._name, refs=self._references)
784
785 result._cache = self._cache
786 return result
787
788 @final
789 def _rename(self, name: Hashable) -> Self:
790 """
791 fastpath for rename if new name is already validated.
792 """
793 result = self._view()
794 result._name = name
795 return result
796
797 @final
798 def is_(self, other) -> bool:
799 """
800 More flexible, faster check like ``is`` but that works through views.
801
802 Note: this is *not* the same as ``Index.identical()``, which checks
803 that metadata is also the same.
804
805 Parameters
806 ----------
807 other : object
808 Other object to compare against.
809
810 Returns
811 -------
812 bool
813 True if both have same underlying data, False otherwise.
814
815 See Also
816 --------
817 Index.identical : Works like ``Index.is_`` but also checks metadata.
818
819 Examples
820 --------
821 >>> idx1 = pd.Index(["1", "2", "3"])
822 >>> idx1.is_(idx1.view())
823 True
824
825 >>> idx1.is_(idx1.copy())
826 False
827 """
828 if self is other:
829 return True
830 elif not hasattr(other, "_id"):
831 return False
832 elif self._id is None or other._id is None:
833 return False
834 else:
835 return self._id is other._id
836
837 @final
838 def _reset_identity(self) -> None:
839 """
840 Initializes or resets ``_id`` attribute with new object.
841 """
842 self._id = object()
843
844 @final
845 def _cleanup(self) -> None:
846 if "_engine" in self._cache:
847 self._engine.clear_mapping()
848
849 @cache_readonly
850 def _engine(
851 self,
852 ) -> libindex.IndexEngine | libindex.ExtensionEngine | libindex.MaskedIndexEngine:
853 # For base class (object dtype) we get ObjectEngine
854 target_values = self._get_engine_target()
855
856 if isinstance(self._values, ArrowExtensionArray) and self.dtype.kind in "Mm":
857 import pyarrow as pa
858
859 pa_type = self._values._pa_array.type
860 if pa.types.is_timestamp(pa_type):
861 target_values = self._values._to_datetimearray()
862 return libindex.DatetimeEngine(target_values._ndarray)
863 elif pa.types.is_duration(pa_type):
864 target_values = self._values._to_timedeltaarray()
865 return libindex.TimedeltaEngine(target_values._ndarray)
866
867 if isinstance(target_values, ExtensionArray):
868 if isinstance(target_values, (BaseMaskedArray, ArrowExtensionArray)):
869 try:
870 return _masked_engines[target_values.dtype.name](target_values)
871 except KeyError:
872 # Not supported yet e.g. decimal
873 pass
874 elif self._engine_type is libindex.ObjectEngine:
875 return libindex.ExtensionEngine(target_values)
876
877 target_values = cast(np.ndarray, target_values)
878 # to avoid a reference cycle, bind `target_values` to a local variable, so
879 # `self` is not passed into the lambda.
880 if target_values.dtype == bool:
881 return libindex.BoolEngine(target_values)
882 elif target_values.dtype == np.complex64:
883 return libindex.Complex64Engine(target_values)
884 elif target_values.dtype == np.complex128:
885 return libindex.Complex128Engine(target_values)
886 elif needs_i8_conversion(self.dtype):
887 # We need to keep M8/m8 dtype when initializing the Engine,
888 # but don't want to change _get_engine_target bc it is used
889 # elsewhere
890 # error: Item "ExtensionArray" of "Union[ExtensionArray,
891 # ndarray[Any, Any]]" has no attribute "_ndarray" [union-attr]
892 target_values = self._data._ndarray # type: ignore[union-attr]
893 elif is_string_dtype(self.dtype) and not is_object_dtype(self.dtype):
894 return libindex.StringObjectEngine(target_values, self.dtype.na_value) # type: ignore[union-attr]
895
896 # error: Argument 1 to "ExtensionEngine" has incompatible type
897 # "ndarray[Any, Any]"; expected "ExtensionArray"
898 return self._engine_type(target_values) # type: ignore[arg-type]
899
900 @final
901 @cache_readonly
902 def _dir_additions_for_owner(self) -> set[str_t]:
903 """
904 Add the string-like labels to the owner dataframe/series dir output.
905
906 If this is a MultiIndex, it's first level values are used.
907 """
908 return {
909 c
910 for c in self.unique(level=0)[: get_option("display.max_dir_items")]
911 if isinstance(c, str) and c.isidentifier()
912 }
913
914 # --------------------------------------------------------------------
915 # Array-Like Methods
916
917 # ndarray compat
918 def __len__(self) -> int:
919 """
920 Return the length of the Index.
921 """
922 return len(self._data)
923
924 def __array__(self, dtype=None, copy=None) -> np.ndarray:
925 """
926 The array interface, return my values.
927 """
928 if copy is None:
929 # Note, that the if branch exists for NumPy 1.x support
930 return np.asarray(self._data, dtype=dtype)
931
932 return np.array(self._data, dtype=dtype, copy=copy)
933
934 def __array_ufunc__(self, ufunc: np.ufunc, method: str_t, *inputs, **kwargs):
935 if any(isinstance(other, (ABCSeries, ABCDataFrame)) for other in inputs):
936 return NotImplemented
937
938 result = arraylike.maybe_dispatch_ufunc_to_dunder_op(
939 self, ufunc, method, *inputs, **kwargs
940 )
941 if result is not NotImplemented:
942 return result
943
944 if "out" in kwargs:
945 # e.g. test_dti_isub_tdi
946 return arraylike.dispatch_ufunc_with_out(
947 self, ufunc, method, *inputs, **kwargs
948 )
949
950 if method == "reduce":
951 result = arraylike.dispatch_reduction_ufunc(
952 self, ufunc, method, *inputs, **kwargs
953 )
954 if result is not NotImplemented:
955 return result
956
957 new_inputs = [x if x is not self else x._values for x in inputs]
958 result = getattr(ufunc, method)(*new_inputs, **kwargs)
959 if ufunc.nout == 2:
960 # i.e. np.divmod, np.modf, np.frexp
961 return tuple(self.__array_wrap__(x) for x in result)
962 elif method == "reduce":
963 result = lib.item_from_zerodim(result)
964 return maybe_unbox_numpy_scalar(result)
965 elif is_scalar(result):
966 # e.g. matmul
967 return maybe_unbox_numpy_scalar(result)
968
969 if result.dtype == np.float16:
970 result = result.astype(np.float32)
971
972 return self.__array_wrap__(result)
973
974 @final
975 def __array_wrap__(self, result, context=None, return_scalar=False):
976 """
977 Gets called after a ufunc and other functions e.g. np.split.
978 """
979 result = lib.item_from_zerodim(result)
980 if np.ndim(result) > 1:
981 # Reached in plotting tests with e.g. np.nonzero(index)
982 return result
983
984 return Index(result, name=self.name)
985
986 @cache_readonly
987 def dtype(self) -> DtypeObj:
988 """
989 Return the dtype object of the underlying data.
990
991 See Also
992 --------
993 Index.inferred_type: Return a string of the type inferred from the values.
994
995 Examples
996 --------
997 >>> idx = pd.Index([1, 2, 3])
998 >>> idx
999 Index([1, 2, 3], dtype='int64')
1000 >>> idx.dtype
1001 dtype('int64')
1002 """
1003 return self._data.dtype
1004
1005 @final
1006 def ravel(self, order: str_t = "C") -> Self:
1007 """
1008 Return a view on self.
1009
1010 Parameters
1011 ----------
1012 order : {'K', 'A', 'C', 'F'}, default 'C'
1013 Specify the memory layout of the view. This parameter is not
1014 implemented currently.
1015
1016 Returns
1017 -------
1018 Index
1019 A view on self.
1020
1021 See Also
1022 --------
1023 numpy.ndarray.ravel : Return a flattened array.
1024
1025 Examples
1026 --------
1027 >>> s = pd.Series([1, 2, 3], index=["a", "b", "c"])
1028 >>> s.index.ravel()
1029 Index(['a', 'b', 'c'], dtype='str')
1030 """
1031 return self[:]
1032
1033 def view(self, cls=None):
1034 """
1035 Return a view of the Index with the specified dtype or a new Index instance.
1036
1037 This method returns a view of the calling Index object if no arguments are
1038 provided. If a dtype is specified through the `cls` argument, it attempts
1039 to return a view of the Index with the specified dtype. Note that viewing
1040 the Index as a different dtype reinterprets the underlying data, which can
1041 lead to unexpected results for non-numeric or incompatible dtype conversions.
1042
1043 Parameters
1044 ----------
1045 cls : data-type or ndarray sub-class, optional
1046 Data-type descriptor of the returned view, e.g., float32 or int16.
1047 Omitting it results in the view having the same data-type as `self`.
1048 This argument can also be specified as an ndarray sub-class,
1049 e.g., np.int64 or np.float32 which then specifies the type of
1050 the returned object.
1051
1052 Returns
1053 -------
1054 Index or ndarray
1055 A view of the Index. If `cls` is None, the returned object is an Index
1056 view with the same dtype as the calling object. If a numeric `cls` is
1057 specified an ndarray view with the new dtype is returned.
1058
1059 Raises
1060 ------
1061 ValueError
1062 If attempting to change to a dtype in a way that is not compatible with
1063 the original dtype's memory layout, for example, viewing an 'int64' Index
1064 as 'str'.
1065
1066 See Also
1067 --------
1068 Index.copy : Returns a copy of the Index.
1069 numpy.ndarray.view : Returns a new view of array with the same data.
1070
1071 Examples
1072 --------
1073 >>> idx = pd.Index([-1, 0, 1])
1074 >>> idx.view()
1075 Index([-1, 0, 1], dtype='int64')
1076
1077 >>> idx.view(np.uint64)
1078 array([18446744073709551615, 0, 1],
1079 dtype=uint64)
1080
1081 Viewing as 'int32' or 'float32' reinterprets the memory, which may lead to
1082 unexpected behavior:
1083
1084 >>> idx.view("float32")
1085 array([ nan, nan, 0.e+00, 0.e+00, 1.e-45, 0.e+00], dtype=float32)
1086 """
1087 # we need to see if we are subclassing an
1088 # index type here
1089 if cls is not None:
1090 dtype = cls
1091 if isinstance(cls, str):
1092 dtype = pandas_dtype(cls)
1093
1094 if needs_i8_conversion(dtype):
1095 idx_cls = self._dtype_to_subclass(dtype)
1096 arr = self.array.view(dtype)
1097 if isinstance(arr, ExtensionArray):
1098 # here we exclude non-supported dt64/td64 dtypes
1099 return idx_cls._simple_new(
1100 arr, name=self.name, refs=self._references
1101 )
1102 return arr
1103
1104 result = self._data.view(cls)
1105 else:
1106 result = self._view()
1107 if isinstance(result, Index):
1108 result._id = self._id
1109 return result
1110
1111 def astype(self, dtype: Dtype, copy: bool = True):
1112 """
1113 Create an Index with values cast to dtypes.
1114
1115 The class of a new Index is determined by dtype. When conversion is
1116 impossible, a TypeError exception is raised.
1117
1118 Parameters
1119 ----------
1120 dtype : numpy dtype or pandas type
1121 Note that any signed integer `dtype` is treated as ``'int64'``,
1122 and any unsigned integer `dtype` is treated as ``'uint64'``,
1123 regardless of the size.
1124 copy : bool, default True
1125 By default, astype always returns a newly allocated object.
1126 If copy is set to False and internal requirements on dtype are
1127 satisfied, the original data is used to create a new Index
1128 or the original Index is returned.
1129
1130 Returns
1131 -------
1132 Index
1133 Index with values cast to specified dtype.
1134
1135 See Also
1136 --------
1137 Index.dtype: Return the dtype object of the underlying data.
1138 Index.dtypes: Return the dtype object of the underlying data.
1139 Index.convert_dtypes: Convert columns to the best possible dtypes.
1140
1141 Examples
1142 --------
1143 >>> idx = pd.Index([1, 2, 3])
1144 >>> idx
1145 Index([1, 2, 3], dtype='int64')
1146 >>> idx.astype("float")
1147 Index([1.0, 2.0, 3.0], dtype='float64')
1148 """
1149 if dtype is not None:
1150 dtype = pandas_dtype(dtype)
1151
1152 if self.dtype == dtype:
1153 # Ensure that self.astype(self.dtype) is self
1154 return self.copy() if copy else self
1155
1156 values = self._data
1157 if isinstance(values, ExtensionArray):
1158 with rewrite_exception(type(values).__name__, type(self).__name__):
1159 new_values = values.astype(dtype, copy=copy)
1160
1161 elif isinstance(dtype, ExtensionDtype):
1162 cls = dtype.construct_array_type()
1163 # Note: for RangeIndex and CategoricalDtype self vs self._values
1164 # behaves differently here.
1165 new_values = cls._from_sequence(self, dtype=dtype, copy=copy)
1166
1167 else:
1168 # GH#13149 specifically use astype_array instead of astype
1169 new_values = astype_array(values, dtype=dtype, copy=copy)
1170
1171 # pass copy=False because any copying will be done in the astype above
1172 result = Index(new_values, name=self.name, dtype=new_values.dtype, copy=False)
1173 if (
1174 not copy
1175 and self._references is not None
1176 and astype_is_view(self.dtype, dtype)
1177 ):
1178 result._references = self._references
1179 result._references.add_index_reference(result)
1180 return result
1181
1182 _index_shared_docs["take"] = """
1183 Return a new %(klass)s of the values selected by the indices.
1184
1185 For internal compatibility with numpy arrays.
1186
1187 Parameters
1188 ----------
1189 indices : array-like
1190 Indices to be taken.
1191 axis : {0 or 'index'}, optional
1192 The axis over which to select values, always 0 or 'index'.
1193 allow_fill : bool, default True
1194 How to handle negative values in `indices`.
1195
1196 * False: negative values in `indices` indicate positional indices
1197 from the right (the default). This is similar to
1198 :func:`numpy.take`.
1199
1200 * True: negative values in `indices` indicate
1201 missing values. These values are set to `fill_value`. Any other
1202 other negative values raise a ``ValueError``.
1203
1204 fill_value : scalar, default None
1205 If allow_fill=True and fill_value is not None, indices specified by
1206 -1 are regarded as NA. If Index doesn't hold NA, raise ValueError.
1207 **kwargs
1208 Required for compatibility with numpy.
1209
1210 Returns
1211 -------
1212 Index
1213 An index formed of elements at the given indices. Will be the same
1214 type as self, except for RangeIndex.
1215
1216 See Also
1217 --------
1218 numpy.ndarray.take: Return an array formed from the
1219 elements of a at the given indices.
1220
1221 Examples
1222 --------
1223 >>> idx = pd.Index(['a', 'b', 'c'])
1224 >>> idx.take([2, 2, 1, 2])
1225 Index(['c', 'c', 'b', 'c'], dtype='str')
1226 """
1227
1228 def take(
1229 self,
1230 indices,
1231 axis: Axis = 0,
1232 allow_fill: bool = True,
1233 fill_value=None,
1234 **kwargs,
1235 ) -> Self:
1236 """
1237 Return a new Index of the values selected by the indices.
1238
1239 For internal compatibility with numpy arrays.
1240
1241 Parameters
1242 ----------
1243 indices : array-like
1244 Indices to be taken.
1245 axis : {0 or 'index'}, optional
1246 The axis over which to select values, always 0 or 'index'.
1247 allow_fill : bool, default True
1248 How to handle negative values in `indices`.
1249
1250 * False: negative values in `indices` indicate positional indices
1251 from the right (the default). This is similar to
1252 :func:`numpy.take`.
1253
1254 * True: negative values in `indices` indicate
1255 missing values. These values are set to `fill_value`. Any
1256 other negative values raise a ``ValueError``.
1257
1258 fill_value : scalar, default None
1259 If allow_fill=True and fill_value is not None, indices specified by
1260 -1 are regarded as NA. If Index doesn't hold NA, raise ValueError.
1261 **kwargs
1262 Required for compatibility with numpy.
1263
1264 Returns
1265 -------
1266 Index
1267 An index formed of elements at the given indices. Will be the same
1268 type as self, except for RangeIndex.
1269
1270 See Also
1271 --------
1272 numpy.ndarray.take: Return an array formed from the
1273 elements of a at the given indices.
1274
1275 Examples
1276 --------
1277 >>> idx = pd.Index(["a", "b", "c"])
1278 >>> idx.take([2, 2, 1, 2])
1279 Index(['c', 'c', 'b', 'c'], dtype='str')
1280 """
1281 if kwargs:
1282 nv.validate_take((), kwargs)
1283 if is_scalar(indices):
1284 raise TypeError("Expected indices to be array-like")
1285 indices = ensure_platform_int(indices)
1286 allow_fill = self._maybe_disallow_fill(allow_fill, fill_value, indices)
1287
1288 if indices.ndim == 1 and lib.is_range_indexer(indices, len(self)):
1289 return self.copy()
1290
1291 # Note: we discard fill_value and use self._na_value, only relevant
1292 # in the case where allow_fill is True and fill_value is not None
1293 values = self._values
1294 if isinstance(values, np.ndarray):
1295 taken = algos.take(
1296 values, indices, allow_fill=allow_fill, fill_value=self._na_value
1297 )
1298 else:
1299 # algos.take passes 'axis' keyword which not all EAs accept
1300 taken = values.take(
1301 indices, allow_fill=allow_fill, fill_value=self._na_value
1302 )
1303 return self._constructor._simple_new(taken, name=self.name)
1304
1305 @final
1306 def _maybe_disallow_fill(self, allow_fill: bool, fill_value, indices) -> bool:
1307 """
1308 We only use pandas-style take when allow_fill is True _and_
1309 fill_value is not None.
1310 """
1311 if allow_fill and fill_value is not None:
1312 # only fill if we are passing a non-None fill_value
1313 if self._can_hold_na:
1314 if (indices < -1).any():
1315 raise ValueError(
1316 "When allow_fill=True and fill_value is not None, "
1317 "all indices must be >= -1"
1318 )
1319 else:
1320 cls_name = type(self).__name__
1321 raise ValueError(
1322 f"Unable to fill values because {cls_name} cannot contain NA"
1323 )
1324 else:
1325 allow_fill = False
1326 return allow_fill
1327
1328 def repeat(self, repeats, axis: None = None) -> Self:
1329 """
1330 Repeat elements of an Index.
1331
1332 Returns a new Index where each element of the current Index
1333 is repeated consecutively a given number of times.
1334
1335 Parameters
1336 ----------
1337 repeats : int or array of ints
1338 The number of repetitions for each element. This should be a
1339 non-negative integer. Repeating 0 times will return an empty
1340 Index.
1341 axis : None
1342 Must be ``None``. Has no effect but is accepted for compatibility
1343 with numpy.
1344
1345 Returns
1346 -------
1347 Index
1348 Newly created Index with repeated elements.
1349
1350 See Also
1351 --------
1352 Series.repeat : Equivalent function for Series.
1353 numpy.repeat : Similar method for :class:`numpy.ndarray`.
1354
1355 Examples
1356 --------
1357 >>> idx = pd.Index(["a", "b", "c"])
1358 >>> idx
1359 Index(['a', 'b', 'c'], dtype='str')
1360 >>> idx.repeat(2)
1361 Index(['a', 'a', 'b', 'b', 'c', 'c'], dtype='str')
1362 >>> idx.repeat([1, 2, 3])
1363 Index(['a', 'b', 'b', 'c', 'c', 'c'], dtype='str')
1364 """
1365 repeats = ensure_platform_int(repeats)
1366 nv.validate_repeat((), {"axis": axis})
1367 res_values = self._values.repeat(repeats)
1368
1369 # _constructor so RangeIndex-> Index with an int64 dtype
1370 return self._constructor._simple_new(res_values, name=self.name)
1371
1372 # --------------------------------------------------------------------
1373 # Copying Methods
1374
1375 def copy(
1376 self,
1377 name: Hashable | None = None,
1378 deep: bool = False,
1379 ) -> Self:
1380 """
1381 Make a copy of this object.
1382
1383 Name is set on the new object.
1384
1385 Parameters
1386 ----------
1387 name : Label, optional
1388 Set name for new object.
1389 deep : bool, default False
1390 If True attempts to make a deep copy of the Index.
1391 Else makes a shallow copy.
1392
1393 Returns
1394 -------
1395 Index
1396 Index refer to new object which is a copy of this object.
1397
1398 See Also
1399 --------
1400 Index.delete: Make new Index with passed location(-s) deleted.
1401 Index.drop: Make new Index with passed list of labels deleted.
1402
1403 Notes
1404 -----
1405 In most cases, there should be no functional difference from using
1406 ``deep``, but if ``deep`` is passed it will attempt to deepcopy.
1407
1408 Examples
1409 --------
1410 >>> idx = pd.Index(["a", "b", "c"])
1411 >>> new_idx = idx.copy()
1412 >>> idx is new_idx
1413 False
1414 """
1415
1416 name = self._validate_names(name=name, deep=deep)[0]
1417 if deep:
1418 new_data = self._data.copy()
1419 new_index = type(self)._simple_new(new_data, name=name)
1420 else:
1421 new_index = self._rename(name=name)
1422 return new_index
1423
1424 @final
1425 def __copy__(self) -> Self:
1426 return self.copy(deep=False)
1427
1428 @final
1429 def __deepcopy__(self, memo=None) -> Self:
1430 """
1431 Parameters
1432 ----------
1433 memo, default None
1434 Standard signature. Unused
1435 """
1436 return self.copy(deep=True)
1437
1438 # --------------------------------------------------------------------
1439 # Rendering Methods
1440
1441 @final
1442 def __repr__(self) -> str_t:
1443 """
1444 Return a string representation for this object.
1445 """
1446 klass_name = type(self).__name__
1447 data = self._format_data()
1448 attrs = self._format_attrs()
1449 attrs_str = [f"{k}={v}" for k, v in attrs]
1450 prepr = ", ".join(attrs_str)
1451
1452 return f"{klass_name}({data}{prepr})"
1453
1454 @property
1455 def _formatter_func(self):
1456 """
1457 Return the formatter function.
1458 """
1459 return default_pprint
1460
1461 @final
1462 def _format_data(self, name=None) -> str_t:
1463 """
1464 Return the formatted data as a unicode string.
1465 """
1466 # do we want to justify (only do so for non-objects)
1467 is_justify = True
1468
1469 if self.inferred_type == "string":
1470 is_justify = False
1471 elif isinstance(self.dtype, CategoricalDtype):
1472 self = cast("CategoricalIndex", self)
1473 if is_string_dtype(self.categories.dtype):
1474 is_justify = False
1475 elif isinstance(self, ABCRangeIndex):
1476 # We will do the relevant formatting via attrs
1477 return ""
1478
1479 return format_object_summary(
1480 self,
1481 self._formatter_func,
1482 is_justify=is_justify,
1483 name=name,
1484 line_break_each_value=self._is_multi,
1485 )
1486
1487 def _format_attrs(self) -> list[tuple[str_t, str_t | int | bool | None]]:
1488 """
1489 Return a list of tuples of the (attr,formatted_value).
1490 """
1491 attrs: list[tuple[str_t, str_t | int | bool | None]] = []
1492
1493 if not self._is_multi:
1494 attrs.append(("dtype", f"'{self.dtype}'"))
1495
1496 if self.name is not None:
1497 attrs.append(("name", default_pprint(self.name)))
1498 elif self._is_multi and any(x is not None for x in self.names):
1499 attrs.append(("names", default_pprint(self.names)))
1500
1501 max_seq_items = get_option("display.max_seq_items") or len(self)
1502 if len(self) > max_seq_items:
1503 attrs.append(("length", len(self)))
1504 return attrs
1505
1506 @final
1507 def _get_level_names(self) -> range | Sequence[Hashable]:
1508 """
1509 Return a name or list of names with None replaced by the level number.
1510 """
1511 if self._is_multi:
1512 return maybe_sequence_to_range(
1513 [
1514 level if name is None else name
1515 for level, name in enumerate(self.names)
1516 ]
1517 )
1518 else:
1519 return range(1) if self.name is None else [self.name]
1520
1521 @final
1522 def _mpl_repr(self) -> np.ndarray:
1523 # how to represent ourselves to matplotlib
1524 if isinstance(self.dtype, np.dtype) and self.dtype.kind != "M":
1525 return cast(np.ndarray, self.values)
1526 return self.astype(object, copy=False)._values
1527
1528 _default_na_rep = "NaN"
1529
1530 @final
1531 def _format_flat(
1532 self,
1533 *,
1534 include_name: bool,
1535 formatter: Callable | None = None,
1536 ) -> list[str_t]:
1537 """
1538 Render a string representation of the Index.
1539 """
1540 header = []
1541 if include_name:
1542 header.append(
1543 pprint_thing(self.name, escape_chars=("\t", "\r", "\n"))
1544 if self.name is not None
1545 else ""
1546 )
1547
1548 if formatter is not None:
1549 return header + list(self.map(formatter))
1550
1551 return self._format_with_header(header=header, na_rep=self._default_na_rep)
1552
1553 def _format_with_header(self, *, header: list[str_t], na_rep: str_t) -> list[str_t]:
1554 from pandas.io.formats.format import format_array
1555
1556 values = self._values
1557
1558 if (
1559 is_object_dtype(values.dtype)
1560 or is_string_dtype(values.dtype)
1561 or isinstance(self.dtype, (IntervalDtype, CategoricalDtype))
1562 ):
1563 # TODO: why do we need different justify for these cases?
1564 justify = "all"
1565 else:
1566 justify = "left"
1567 # passing leading_space=False breaks test_format_missing,
1568 # test_index_repr_in_frame_with_nan, but would otherwise make
1569 # trim_front unnecessary
1570 formatted = format_array(values, None, justify=justify)
1571 result = trim_front(formatted)
1572 return header + result
1573
1574 def _get_values_for_csv(
1575 self,
1576 *,
1577 na_rep: str_t = "",
1578 decimal: str_t = ".",
1579 float_format=None,
1580 date_format=None,
1581 quoting=None,
1582 ) -> npt.NDArray[np.object_]:
1583 return get_values_for_csv(
1584 self._values,
1585 na_rep=na_rep,
1586 decimal=decimal,
1587 float_format=float_format,
1588 date_format=date_format,
1589 quoting=quoting,
1590 )
1591
1592 def _summary(self, name=None) -> str_t:
1593 """
1594 Return a summarized representation.
1595
1596 Parameters
1597 ----------
1598 name : str
1599 name to use in the summary representation
1600
1601 Returns
1602 -------
1603 String with a summarized representation of the index
1604 """
1605 if len(self) > 0:
1606 head = self[0]
1607 if hasattr(head, "format") and not isinstance(head, str):
1608 head = head.format()
1609 elif needs_i8_conversion(self.dtype):
1610 # e.g. Timedelta, display as values, not quoted
1611 head = self._formatter_func(head).replace("'", "")
1612 tail = self[-1]
1613 if hasattr(tail, "format") and not isinstance(tail, str):
1614 tail = tail.format()
1615 elif needs_i8_conversion(self.dtype):
1616 # e.g. Timedelta, display as values, not quoted
1617 tail = self._formatter_func(tail).replace("'", "")
1618
1619 index_summary = f", {head} to {tail}"
1620 else:
1621 index_summary = ""
1622
1623 if name is None:
1624 name = type(self).__name__
1625 return f"{name}: {len(self)} entries{index_summary}"
1626
1627 # --------------------------------------------------------------------
1628 # Conversion Methods
1629
1630 def to_flat_index(self) -> Self:
1631 """
1632 Identity method.
1633
1634 This is implemented for compatibility with subclass implementations
1635 when chaining.
1636
1637 Returns
1638 -------
1639 pd.Index
1640 Caller.
1641
1642 See Also
1643 --------
1644 MultiIndex.to_flat_index : Subclass implementation.
1645 """
1646 return self
1647
1648 @final
1649 def to_series(self, index=None, name: Hashable | None = None) -> Series:
1650 """
1651 Create a Series with both index and values equal to the index keys.
1652
1653 Useful with map for returning an indexer based on an index.
1654
1655 Parameters
1656 ----------
1657 index : Index, optional
1658 Index of resulting Series. If None, defaults to original index.
1659 name : str, optional
1660 Name of resulting Series. If None, defaults to name of original
1661 index.
1662
1663 Returns
1664 -------
1665 Series
1666 The dtype will be based on the type of the Index values.
1667
1668 See Also
1669 --------
1670 Index.to_frame : Convert an Index to a DataFrame.
1671 Series.to_frame : Convert Series to DataFrame.
1672
1673 Examples
1674 --------
1675 >>> idx = pd.Index(["Ant", "Bear", "Cow"], name="animal")
1676
1677 By default, the original index and original name is reused.
1678
1679 >>> idx.to_series()
1680 animal
1681 Ant Ant
1682 Bear Bear
1683 Cow Cow
1684 Name: animal, dtype: str
1685
1686 To enforce a new index, specify new labels to ``index``:
1687
1688 >>> idx.to_series(index=[0, 1, 2])
1689 0 Ant
1690 1 Bear
1691 2 Cow
1692 Name: animal, dtype: str
1693
1694 To override the name of the resulting column, specify ``name``:
1695
1696 >>> idx.to_series(name="zoo")
1697 animal
1698 Ant Ant
1699 Bear Bear
1700 Cow Cow
1701 Name: zoo, dtype: str
1702 """
1703 from pandas import Series
1704
1705 if index is None:
1706 index = self._view()
1707 if name is None:
1708 name = self.name
1709
1710 return Series(self._values.copy(), index=index, name=name)
1711
1712 def to_frame(
1713 self, index: bool = True, name: Hashable = lib.no_default
1714 ) -> DataFrame:
1715 """
1716 Create a DataFrame with a column containing the Index.
1717
1718 Parameters
1719 ----------
1720 index : bool, default True
1721 Set the index of the returned DataFrame as the original Index.
1722
1723 name : object, defaults to index.name
1724 The passed name should substitute for the index name (if it has
1725 one).
1726
1727 Returns
1728 -------
1729 DataFrame
1730 DataFrame containing the original Index data.
1731
1732 See Also
1733 --------
1734 Index.to_series : Convert an Index to a Series.
1735 Series.to_frame : Convert Series to DataFrame.
1736
1737 Examples
1738 --------
1739 >>> idx = pd.Index(["Ant", "Bear", "Cow"], name="animal")
1740 >>> idx.to_frame()
1741 animal
1742 animal
1743 Ant Ant
1744 Bear Bear
1745 Cow Cow
1746
1747 By default, the original Index is reused. To enforce a new Index:
1748
1749 >>> idx.to_frame(index=False)
1750 animal
1751 0 Ant
1752 1 Bear
1753 2 Cow
1754
1755 To override the name of the resulting column, specify `name`:
1756
1757 >>> idx.to_frame(index=False, name="zoo")
1758 zoo
1759 0 Ant
1760 1 Bear
1761 2 Cow
1762 """
1763 from pandas import DataFrame
1764
1765 if name is lib.no_default:
1766 result_name = self._get_level_names()
1767 else:
1768 result_name = Index([name]) # type: ignore[assignment]
1769 result = DataFrame(self, copy=False)
1770 result.columns = result_name
1771
1772 if index:
1773 result.index = self
1774 return result
1775
1776 # --------------------------------------------------------------------
1777 # Name-Centric Methods
1778
1779 @property
1780 def name(self) -> Hashable:
1781 """
1782 Return Index or MultiIndex name.
1783
1784 Returns
1785 -------
1786 label (hashable object)
1787 The name of the Index.
1788
1789 See Also
1790 --------
1791 Index.set_names: Able to set new names partially and by level.
1792 Index.rename: Able to set new names partially and by level.
1793 Series.name: Corresponding Series property.
1794
1795 Examples
1796 --------
1797 >>> idx = pd.Index([1, 2, 3], name="x")
1798 >>> idx
1799 Index([1, 2, 3], dtype='int64', name='x')
1800 >>> idx.name
1801 'x'
1802 """
1803 return self._name
1804
1805 @name.setter
1806 def name(self, value: Hashable) -> None:
1807 if self._no_setting_name:
1808 # Used in MultiIndex.levels to avoid silently ignoring name updates.
1809 raise RuntimeError(
1810 "Cannot set name on a level of a MultiIndex. Use "
1811 "'MultiIndex.set_names' instead."
1812 )
1813 maybe_extract_name(value, None, type(self))
1814 self._name = value
1815
1816 @final
1817 def _validate_names(
1818 self, name=None, names=None, deep: bool = False
1819 ) -> list[Hashable]:
1820 """
1821 Handles the quirks of having a singular 'name' parameter for general
1822 Index and plural 'names' parameter for MultiIndex.
1823 """
1824 from copy import deepcopy
1825
1826 if names is not None and name is not None:
1827 raise TypeError("Can only provide one of `names` and `name`")
1828 if names is None and name is None:
1829 new_names = deepcopy(self.names) if deep else self.names
1830 elif names is not None:
1831 if not is_list_like(names):
1832 raise TypeError("Must pass list-like as `names`.")
1833 new_names = names
1834 elif not is_list_like(name):
1835 new_names = [name]
1836 else:
1837 new_names = name
1838
1839 if len(new_names) != len(self.names):
1840 raise ValueError(
1841 f"Length of new names must be {len(self.names)}, got {len(new_names)}"
1842 )
1843
1844 # All items in 'new_names' need to be hashable
1845 validate_all_hashable(*new_names, error_name=f"{type(self).__name__}.name")
1846
1847 return new_names
1848
1849 def _get_default_index_names(
1850 self, names: Hashable | Sequence[Hashable] | None = None, default=None
1851 ) -> list[Hashable]:
1852 """
1853 Get names of index.
1854
1855 Parameters
1856 ----------
1857 names : int, str or 1-dimensional list, default None
1858 Index names to set.
1859 default : str
1860 Default name of index.
1861
1862 Raises
1863 ------
1864 TypeError
1865 if names not str or list-like
1866 """
1867 from pandas.core.indexes.multi import MultiIndex
1868
1869 if names is not None:
1870 if isinstance(names, (int, str)):
1871 names = [names]
1872
1873 if not isinstance(names, list) and names is not None:
1874 raise ValueError("Index names must be str or 1-dimensional list")
1875
1876 if not names:
1877 if isinstance(self, MultiIndex):
1878 names = com.fill_missing_names(self.names)
1879 else:
1880 names = [default] if self.name is None else [self.name]
1881
1882 return names
1883
1884 def _get_names(self) -> FrozenList:
1885 """
1886 Get names on index.
1887
1888 This method returns a FrozenList containing the names of the object.
1889 It's primarily intended for internal use.
1890
1891 Returns
1892 -------
1893 FrozenList
1894 A FrozenList containing the object's names, contains None if the object
1895 does not have a name.
1896
1897 See Also
1898 --------
1899 Index.name : Index name as a string, or None for MultiIndex.
1900
1901 Examples
1902 --------
1903 >>> idx = pd.Index([1, 2, 3], name="x")
1904 >>> idx.names
1905 FrozenList(['x'])
1906
1907 >>> idx = pd.Index([1, 2, 3], name=("x", "y"))
1908 >>> idx.names
1909 FrozenList([('x', 'y')])
1910
1911 If the index does not have a name set:
1912
1913 >>> idx = pd.Index([1, 2, 3])
1914 >>> idx.names
1915 FrozenList([None])
1916 """
1917 return FrozenList((self.name,))
1918
1919 def _set_names(self, values, *, level=None) -> None:
1920 """
1921 Set new names on index. Each name has to be a hashable type.
1922
1923 Parameters
1924 ----------
1925 values : str or sequence
1926 name(s) to set
1927 level : int, level name, or sequence of int/level names (default None)
1928 If the index is a MultiIndex (hierarchical), level(s) to set (None
1929 for all levels). Otherwise level must be None
1930
1931 Raises
1932 ------
1933 TypeError if each name is not hashable.
1934 """
1935 if not is_list_like(values):
1936 raise ValueError("Names must be a list-like")
1937 if len(values) != 1:
1938 raise ValueError(f"Length of new names must be 1, got {len(values)}")
1939
1940 # GH 20527
1941 # All items in 'name' need to be hashable:
1942 validate_all_hashable(*values, error_name=f"{type(self).__name__}.name")
1943
1944 self._name = values[0]
1945
1946 names = property(fset=_set_names, fget=_get_names)
1947
1948 @overload
1949 def set_names(self, names, *, level=..., inplace: Literal[False] = ...) -> Self: ...
1950
1951 @overload
1952 def set_names(self, names, *, level=..., inplace: Literal[True]) -> None: ...
1953
1954 @overload
1955 def set_names(self, names, *, level=..., inplace: bool = ...) -> Self | None: ...
1956
1957 def set_names(self, names, *, level=None, inplace: bool = False) -> Self | None:
1958 """
1959 Set Index or MultiIndex name.
1960
1961 Able to set new names partially and by level.
1962
1963 Parameters
1964 ----------
1965 names : Hashable or a sequence of the previous or dict-like for MultiIndex
1966 Name(s) to set.
1967
1968 level : int, Hashable or a sequence of the previous, optional
1969 If the index is a MultiIndex and names is not dict-like, level(s) to set
1970 (None for all levels). Otherwise level must be None.
1971
1972 inplace : bool, default False
1973 Modifies the object directly, instead of creating a new Index or
1974 MultiIndex.
1975
1976 Returns
1977 -------
1978 Index or None
1979 The same type as the caller or None if ``inplace=True``.
1980
1981 See Also
1982 --------
1983 Index.rename : Able to set new names without level.
1984
1985 Examples
1986 --------
1987 >>> idx = pd.Index([1, 2, 3, 4])
1988 >>> idx
1989 Index([1, 2, 3, 4], dtype='int64')
1990 >>> idx.set_names("quarter")
1991 Index([1, 2, 3, 4], dtype='int64', name='quarter')
1992
1993 >>> idx = pd.MultiIndex.from_product([["python", "cobra"], [2018, 2019]])
1994 >>> idx
1995 MultiIndex([('python', 2018),
1996 ('python', 2019),
1997 ( 'cobra', 2018),
1998 ( 'cobra', 2019)],
1999 )
2000 >>> idx = idx.set_names(["kind", "year"])
2001 >>> idx.set_names("species", level=0)
2002 MultiIndex([('python', 2018),
2003 ('python', 2019),
2004 ( 'cobra', 2018),
2005 ( 'cobra', 2019)],
2006 names=['species', 'year'])
2007
2008 When renaming levels with a dict, levels can not be passed.
2009
2010 >>> idx.set_names({"kind": "snake"})
2011 MultiIndex([('python', 2018),
2012 ('python', 2019),
2013 ( 'cobra', 2018),
2014 ( 'cobra', 2019)],
2015 names=['snake', 'year'])
2016 """
2017 if level is not None and not isinstance(self, ABCMultiIndex):
2018 raise ValueError("Level must be None for non-MultiIndex")
2019
2020 if level is not None and not is_list_like(level) and is_list_like(names):
2021 raise TypeError("Names must be a string when a single level is provided.")
2022
2023 if not is_list_like(names) and level is None and self.nlevels > 1:
2024 raise TypeError("Must pass list-like as `names`.")
2025
2026 if is_dict_like(names) and not isinstance(self, ABCMultiIndex):
2027 raise TypeError("Can only pass dict-like as `names` for MultiIndex.")
2028
2029 if is_dict_like(names) and level is not None:
2030 raise TypeError("Can not pass level for dictlike `names`.")
2031
2032 if isinstance(self, ABCMultiIndex) and is_dict_like(names) and level is None:
2033 # Transform dict to list of new names and corresponding levels
2034 level, names_adjusted = [], []
2035 for i, name in enumerate(self.names):
2036 if name in names.keys():
2037 level.append(i)
2038 names_adjusted.append(names[name])
2039 names = names_adjusted
2040
2041 if not is_list_like(names):
2042 names = [names]
2043 if level is not None and not is_list_like(level):
2044 level = [level]
2045
2046 if inplace:
2047 idx = self
2048 else:
2049 idx = self._view()
2050
2051 idx._set_names(names, level=level)
2052 if not inplace:
2053 return idx
2054 return None
2055
2056 @overload
2057 def rename(self, name, *, inplace: Literal[False] = ...) -> Self: ...
2058
2059 @overload
2060 def rename(self, name, *, inplace: Literal[True]) -> None: ...
2061
2062 def rename(self, name, *, inplace: bool = False) -> Self | None:
2063 """
2064 Alter Index or MultiIndex name.
2065
2066 Able to set new names without level. Defaults to returning new index.
2067 Length of names must match number of levels in MultiIndex.
2068
2069 Parameters
2070 ----------
2071 name : Hashable or a sequence of the previous
2072 Name(s) to set.
2073 inplace : bool, default False
2074 Modifies the object directly, instead of creating a new Index or
2075 MultiIndex.
2076
2077 Returns
2078 -------
2079 Index or None
2080 The same type as the caller or None if ``inplace=True``.
2081
2082 See Also
2083 --------
2084 Index.set_names : Able to set new names partially and by level.
2085
2086 Examples
2087 --------
2088 >>> idx = pd.Index(["A", "C", "A", "B"], name="score")
2089 >>> idx.rename("grade")
2090 Index(['A', 'C', 'A', 'B'], dtype='str', name='grade')
2091
2092 >>> idx = pd.MultiIndex.from_product(
2093 ... [["python", "cobra"], [2018, 2019]], names=["kind", "year"]
2094 ... )
2095 >>> idx
2096 MultiIndex([('python', 2018),
2097 ('python', 2019),
2098 ( 'cobra', 2018),
2099 ( 'cobra', 2019)],
2100 names=['kind', 'year'])
2101 >>> idx.rename(["species", "year"])
2102 MultiIndex([('python', 2018),
2103 ('python', 2019),
2104 ( 'cobra', 2018),
2105 ( 'cobra', 2019)],
2106 names=['species', 'year'])
2107 >>> idx.rename("species")
2108 Traceback (most recent call last):
2109 TypeError: Must pass list-like as `names`.
2110 """
2111 return self.set_names([name], inplace=inplace)
2112
2113 # --------------------------------------------------------------------
2114 # Level-Centric Methods
2115
2116 @property
2117 def nlevels(self) -> int:
2118 """
2119 Number of levels.
2120 """
2121 return 1
2122
2123 def _sort_levels_monotonic(self) -> Self:
2124 """
2125 Compat with MultiIndex.
2126 """
2127 return self
2128
2129 @final
2130 def _validate_index_level(self, level) -> None:
2131 """
2132 Validate index level.
2133
2134 For single-level Index getting level number is a no-op, but some
2135 verification must be done like in MultiIndex.
2136
2137 """
2138 if isinstance(level, int):
2139 if level < 0 and level != -1:
2140 raise IndexError(
2141 "Too many levels: Index has only 1 level, "
2142 f"{level} is not a valid level number"
2143 )
2144 if level > 0:
2145 raise IndexError(
2146 f"Too many levels: Index has only 1 level, not {level + 1}"
2147 )
2148 elif level != self.name:
2149 raise KeyError(
2150 f"Requested level ({level}) does not match index name ({self.name})"
2151 )
2152
2153 def _get_level_number(self, level) -> int:
2154 self._validate_index_level(level)
2155 return 0
2156
2157 def sortlevel(
2158 self,
2159 level=None,
2160 ascending: bool | list[bool] = True,
2161 sort_remaining=None,
2162 na_position: NaPosition = "first",
2163 ) -> tuple[Self, np.ndarray]:
2164 """
2165 For internal compatibility with the Index API.
2166
2167 Sort the Index. This is for compat with MultiIndex
2168
2169 Parameters
2170 ----------
2171 ascending : bool, default True
2172 False to sort in descending order
2173 na_position : {'first' or 'last'}, default 'first'
2174 Argument 'first' puts NaNs at the beginning, 'last' puts NaNs at
2175 the end.
2176
2177 .. versionadded:: 2.1.0
2178
2179 level, sort_remaining are compat parameters
2180
2181 Returns
2182 -------
2183 Index
2184 """
2185 if not isinstance(ascending, (list, bool)):
2186 raise TypeError(
2187 "ascending must be a single bool value or"
2188 "a list of bool values of length 1"
2189 )
2190
2191 if isinstance(ascending, list):
2192 if len(ascending) != 1:
2193 raise TypeError("ascending must be a list of bool values of length 1")
2194 ascending = ascending[0]
2195
2196 if not isinstance(ascending, bool):
2197 raise TypeError("ascending must be a bool value")
2198
2199 return self.sort_values(
2200 return_indexer=True, ascending=ascending, na_position=na_position
2201 )
2202
2203 def _get_level_values(self, level) -> Index:
2204 """
2205 Return an Index of values for requested level.
2206
2207 This is primarily useful to get an individual level of values from a
2208 MultiIndex, but is provided on Index as well for compatibility.
2209
2210 Parameters
2211 ----------
2212 level : int or str
2213 It is either the integer position or the name of the level.
2214
2215 Returns
2216 -------
2217 Index
2218 Calling object, as there is only one level in the Index.
2219
2220 See Also
2221 --------
2222 MultiIndex.get_level_values : Get values for a level of a MultiIndex.
2223
2224 Notes
2225 -----
2226 For Index, level should be 0, since there are no multiple levels.
2227
2228 Examples
2229 --------
2230 >>> idx = pd.Index(list("abc"))
2231 >>> idx
2232 Index(['a', 'b', 'c'], dtype='str')
2233
2234 Get level values by supplying `level` as integer:
2235
2236 >>> idx.get_level_values(0)
2237 Index(['a', 'b', 'c'], dtype='str')
2238 """
2239 self._validate_index_level(level)
2240 return self
2241
2242 get_level_values = _get_level_values
2243
2244 @final
2245 def droplevel(self, level: IndexLabel = 0):
2246 """
2247 Return index with requested level(s) removed.
2248
2249 If resulting index has only 1 level left, the result will be
2250 of Index type, not MultiIndex. The original index is not modified inplace.
2251
2252 Parameters
2253 ----------
2254 level : int, str, or list-like, default 0
2255 If a string is given, must be the name of a level
2256 If list-like, elements must be names or indexes of levels.
2257
2258 Returns
2259 -------
2260 Index or MultiIndex
2261 Returns an Index or MultiIndex object, depending on the resulting index
2262 after removing the requested level(s).
2263
2264 See Also
2265 --------
2266 Index.dropna : Return Index without NA/NaN values.
2267
2268 Examples
2269 --------
2270 >>> mi = pd.MultiIndex.from_arrays(
2271 ... [[1, 2], [3, 4], [5, 6]], names=["x", "y", "z"]
2272 ... )
2273 >>> mi
2274 MultiIndex([(1, 3, 5),
2275 (2, 4, 6)],
2276 names=['x', 'y', 'z'])
2277
2278 >>> mi.droplevel()
2279 MultiIndex([(3, 5),
2280 (4, 6)],
2281 names=['y', 'z'])
2282
2283 >>> mi.droplevel(2)
2284 MultiIndex([(1, 3),
2285 (2, 4)],
2286 names=['x', 'y'])
2287
2288 >>> mi.droplevel("z")
2289 MultiIndex([(1, 3),
2290 (2, 4)],
2291 names=['x', 'y'])
2292
2293 >>> mi.droplevel(["x", "y"])
2294 Index([5, 6], dtype='int64', name='z')
2295 """
2296 if not isinstance(level, (tuple, list)):
2297 level = [level]
2298
2299 levnums = sorted((self._get_level_number(lev) for lev in level), reverse=True)
2300
2301 return self._drop_level_numbers(levnums)
2302
2303 @final
2304 def _drop_level_numbers(self, levnums: list[int]):
2305 """
2306 Drop MultiIndex levels by level _number_, not name.
2307 """
2308
2309 if not levnums and not isinstance(self, ABCMultiIndex):
2310 return self
2311 if len(levnums) >= self.nlevels:
2312 raise ValueError(
2313 f"Cannot remove {len(levnums)} levels from an index with "
2314 f"{self.nlevels} levels: at least one level must be left."
2315 )
2316 # The two checks above guarantee that here self is a MultiIndex
2317 self = cast("MultiIndex", self)
2318
2319 new_levels = list(self.levels)
2320 new_codes = list(self.codes)
2321 new_names = list(self.names)
2322
2323 for i in levnums:
2324 new_levels.pop(i)
2325 new_codes.pop(i)
2326 new_names.pop(i)
2327
2328 if len(new_levels) == 1:
2329 lev = new_levels[0]
2330
2331 if len(lev) == 0:
2332 # If lev is empty, lev.take will fail GH#42055
2333 if len(new_codes[0]) == 0:
2334 # GH#45230 preserve RangeIndex here
2335 # see test_reset_index_empty_rangeindex
2336 result = lev[:0]
2337 else:
2338 res_values = algos.take(lev._values, new_codes[0], allow_fill=True)
2339 # _constructor instead of type(lev) for RangeIndex compat GH#35230
2340 result = lev._constructor._simple_new(res_values, name=new_names[0])
2341 else:
2342 # set nan if needed
2343 mask = new_codes[0] == -1
2344 result = new_levels[0].take(new_codes[0])
2345 if mask.any():
2346 result = result.putmask(mask, np.nan)
2347
2348 result._name = new_names[0]
2349
2350 return result
2351 else:
2352 from pandas.core.indexes.multi import MultiIndex
2353
2354 return MultiIndex(
2355 levels=new_levels,
2356 codes=new_codes,
2357 names=new_names,
2358 verify_integrity=False,
2359 )
2360
2361 # --------------------------------------------------------------------
2362 # Introspection Methods
2363
2364 @cache_readonly
2365 @final
2366 def _can_hold_na(self) -> bool:
2367 if isinstance(self.dtype, ExtensionDtype):
2368 return self.dtype._can_hold_na
2369 if self.dtype.kind in "iub":
2370 return False
2371 return True
2372
2373 @property
2374 def is_monotonic_increasing(self) -> bool:
2375 """
2376 Return a boolean if the values are equal or increasing.
2377
2378 Returns
2379 -------
2380 bool
2381
2382 See Also
2383 --------
2384 Index.is_monotonic_decreasing : Check if the values are equal or decreasing.
2385
2386 Examples
2387 --------
2388 >>> pd.Index([1, 2, 3]).is_monotonic_increasing
2389 True
2390 >>> pd.Index([1, 2, 2]).is_monotonic_increasing
2391 True
2392 >>> pd.Index([1, 3, 2]).is_monotonic_increasing
2393 False
2394 """
2395 return self._engine.is_monotonic_increasing
2396
2397 @property
2398 def is_monotonic_decreasing(self) -> bool:
2399 """
2400 Return a boolean if the values are equal or decreasing.
2401
2402 Returns
2403 -------
2404 bool
2405
2406 See Also
2407 --------
2408 Index.is_monotonic_increasing : Check if the values are equal or increasing.
2409
2410 Examples
2411 --------
2412 >>> pd.Index([3, 2, 1]).is_monotonic_decreasing
2413 True
2414 >>> pd.Index([3, 2, 2]).is_monotonic_decreasing
2415 True
2416 >>> pd.Index([3, 1, 2]).is_monotonic_decreasing
2417 False
2418 """
2419 return self._engine.is_monotonic_decreasing
2420
2421 @final
2422 @property
2423 def _is_strictly_monotonic_increasing(self) -> bool:
2424 """
2425 Return if the index is strictly monotonic increasing
2426 (only increasing) values.
2427
2428 Examples
2429 --------
2430 >>> Index([1, 2, 3])._is_strictly_monotonic_increasing
2431 True
2432 >>> Index([1, 2, 2])._is_strictly_monotonic_increasing
2433 False
2434 >>> Index([1, 3, 2])._is_strictly_monotonic_increasing
2435 False
2436 """
2437 return self.is_unique and self.is_monotonic_increasing
2438
2439 @final
2440 @property
2441 def _is_strictly_monotonic_decreasing(self) -> bool:
2442 """
2443 Return if the index is strictly monotonic decreasing
2444 (only decreasing) values.
2445
2446 Examples
2447 --------
2448 >>> Index([3, 2, 1])._is_strictly_monotonic_decreasing
2449 True
2450 >>> Index([3, 2, 2])._is_strictly_monotonic_decreasing
2451 False
2452 >>> Index([3, 1, 2])._is_strictly_monotonic_decreasing
2453 False
2454 """
2455 return self.is_unique and self.is_monotonic_decreasing
2456
2457 @cache_readonly
2458 def is_unique(self) -> bool:
2459 """
2460 Return if the index has unique values.
2461
2462 Returns
2463 -------
2464 bool
2465
2466 See Also
2467 --------
2468 Index.has_duplicates : Inverse method that checks if it has duplicate values.
2469
2470 Examples
2471 --------
2472 >>> idx = pd.Index([1, 5, 7, 7])
2473 >>> idx.is_unique
2474 False
2475
2476 >>> idx = pd.Index([1, 5, 7])
2477 >>> idx.is_unique
2478 True
2479
2480 >>> idx = pd.Index(["Watermelon", "Orange", "Apple", "Watermelon"]).astype(
2481 ... "category"
2482 ... )
2483 >>> idx.is_unique
2484 False
2485
2486 >>> idx = pd.Index(["Orange", "Apple", "Watermelon"]).astype("category")
2487 >>> idx.is_unique
2488 True
2489 """
2490 return self._engine.is_unique
2491
2492 @final
2493 @property
2494 def has_duplicates(self) -> bool:
2495 """
2496 Check if the Index has duplicate values.
2497
2498 Returns
2499 -------
2500 bool
2501 Whether or not the Index has duplicate values.
2502
2503 See Also
2504 --------
2505 Index.is_unique : Inverse method that checks if it has unique values.
2506
2507 Examples
2508 --------
2509 >>> idx = pd.Index([1, 5, 7, 7])
2510 >>> idx.has_duplicates
2511 True
2512
2513 >>> idx = pd.Index([1, 5, 7])
2514 >>> idx.has_duplicates
2515 False
2516
2517 >>> idx = pd.Index(["Watermelon", "Orange", "Apple", "Watermelon"]).astype(
2518 ... "category"
2519 ... )
2520 >>> idx.has_duplicates
2521 True
2522
2523 >>> idx = pd.Index(["Orange", "Apple", "Watermelon"]).astype("category")
2524 >>> idx.has_duplicates
2525 False
2526 """
2527 return not self.is_unique
2528
2529 @cache_readonly
2530 def inferred_type(self) -> str_t:
2531 """
2532 Return a string of the type inferred from the values.
2533
2534 See Also
2535 --------
2536 Index.dtype : Return the dtype object of the underlying data.
2537
2538 Examples
2539 --------
2540 >>> idx = pd.Index([1, 2, 3])
2541 >>> idx
2542 Index([1, 2, 3], dtype='int64')
2543 >>> idx.inferred_type
2544 'integer'
2545 """
2546 return lib.infer_dtype(self._values, skipna=False)
2547
2548 @cache_readonly
2549 @final
2550 def _is_all_dates(self) -> bool:
2551 """
2552 Whether or not the index values only consist of dates.
2553 """
2554 if needs_i8_conversion(self.dtype):
2555 return True
2556 elif self.dtype != _dtype_obj:
2557 # TODO(ExtensionIndex): 3rd party EA might override?
2558 # Note: this includes IntervalIndex, even when the left/right
2559 # contain datetime-like objects.
2560 return False
2561 elif self._is_multi:
2562 return False
2563 return is_datetime_array(ensure_object(self._values))
2564
2565 @final
2566 @cache_readonly
2567 def _is_multi(self) -> bool:
2568 """
2569 Cached check equivalent to isinstance(self, MultiIndex)
2570 """
2571 return isinstance(self, ABCMultiIndex)
2572
2573 # --------------------------------------------------------------------
2574 # Pickle Methods
2575
2576 def __reduce__(self):
2577 d = {"data": self._data, "name": self.name}
2578 return _new_Index, (type(self), d), None
2579
2580 # --------------------------------------------------------------------
2581 # Null Handling Methods
2582
2583 @cache_readonly
2584 def _na_value(self):
2585 """The expected NA value to use with this index."""
2586 dtype = self.dtype
2587 if isinstance(dtype, np.dtype):
2588 if dtype.kind in "mM":
2589 return NaT
2590 return np.nan
2591 return dtype.na_value
2592
2593 @cache_readonly
2594 def _isnan(self) -> npt.NDArray[np.bool_]:
2595 """
2596 Return if each value is NaN.
2597 """
2598 if self._can_hold_na:
2599 return isna(self)
2600 else:
2601 # shouldn't reach to this condition by checking hasnans beforehand
2602 values = np.empty(len(self), dtype=np.bool_)
2603 values.fill(False)
2604 return values
2605
2606 @cache_readonly
2607 def hasnans(self) -> bool:
2608 """
2609 Return True if there are any NaNs.
2610
2611 Enables various performance speedups.
2612
2613 Returns
2614 -------
2615 bool
2616
2617 See Also
2618 --------
2619 Index.isna : Detect missing values.
2620 Index.dropna : Return Index without NA/NaN values.
2621 Index.fillna : Fill NA/NaN values with the specified value.
2622
2623 Examples
2624 --------
2625 >>> s = pd.Series([1, 2, 3], index=["a", "b", None])
2626 >>> s
2627 a 1
2628 b 2
2629 None 3
2630 dtype: int64
2631 >>> s.index.hasnans
2632 True
2633 """
2634 if self._can_hold_na:
2635 return bool(self._isnan.any())
2636 else:
2637 return False
2638
2639 @final
2640 def isna(self) -> npt.NDArray[np.bool_]:
2641 """
2642 Detect missing values.
2643
2644 Return a boolean same-sized object indicating if the values are NA.
2645 NA values, such as ``None``, :attr:`numpy.NaN` or :attr:`pd.NaT`, get
2646 mapped to ``True`` values.
2647 Everything else get mapped to ``False`` values. Characters such as
2648 empty strings `''` or :attr:`numpy.inf` are not considered NA values.
2649
2650 Returns
2651 -------
2652 numpy.ndarray[bool]
2653 A boolean array of whether my values are NA.
2654
2655 See Also
2656 --------
2657 Index.notna : Boolean inverse of isna.
2658 Index.dropna : Omit entries with missing values.
2659 isna : Top-level isna.
2660 Series.isna : Detect missing values in Series object.
2661
2662 Examples
2663 --------
2664 Show which entries in a pandas.Index are NA. The result is an
2665 array.
2666
2667 >>> idx = pd.Index([5.2, 6.0, np.nan])
2668 >>> idx
2669 Index([5.2, 6.0, nan], dtype='float64')
2670 >>> idx.isna()
2671 array([False, False, True])
2672
2673 Empty strings are not considered NA values. None is considered an NA
2674 value.
2675
2676 >>> idx = pd.Index(["black", "", "red", None])
2677 >>> idx
2678 Index(['black', '', 'red', nan], dtype='str')
2679 >>> idx.isna()
2680 array([False, False, False, True])
2681
2682 For datetimes, `NaT` (Not a Time) is considered as an NA value.
2683
2684 >>> idx = pd.DatetimeIndex(
2685 ... [pd.Timestamp("1940-04-25"), pd.Timestamp(""), None, pd.NaT]
2686 ... )
2687 >>> idx
2688 DatetimeIndex(['1940-04-25', 'NaT', 'NaT', 'NaT'],
2689 dtype='datetime64[us]', freq=None)
2690 >>> idx.isna()
2691 array([False, True, True, True])
2692 """
2693 return self._isnan
2694
2695 isnull = isna
2696
2697 @final
2698 def notna(self) -> npt.NDArray[np.bool_]:
2699 """
2700 Detect existing (non-missing) values.
2701
2702 Return a boolean same-sized object indicating if the values are not NA.
2703 Non-missing values get mapped to ``True``. Characters such as empty
2704 strings ``''`` or :attr:`numpy.inf` are not considered NA values.
2705 NA values, such as None or :attr:`numpy.NaN`, get mapped to ``False``
2706 values.
2707
2708 Returns
2709 -------
2710 numpy.ndarray[bool]
2711 Boolean array to indicate which entries are not NA.
2712
2713 See Also
2714 --------
2715 Index.notnull : Alias of notna.
2716 Index.isna: Inverse of notna.
2717 notna : Top-level notna.
2718
2719 Examples
2720 --------
2721 Show which entries in an Index are not NA. The result is an
2722 array.
2723
2724 >>> idx = pd.Index([5.2, 6.0, np.nan])
2725 >>> idx
2726 Index([5.2, 6.0, nan], dtype='float64')
2727 >>> idx.notna()
2728 array([ True, True, False])
2729
2730 Empty strings are not considered NA values. None is considered a NA
2731 value.
2732
2733 >>> idx = pd.Index(["black", "", "red", None])
2734 >>> idx
2735 Index(['black', '', 'red', nan], dtype='str')
2736 >>> idx.notna()
2737 array([ True, True, True, False])
2738 """
2739 return ~self.isna()
2740
2741 notnull = notna
2742
2743 def fillna(self, value):
2744 """
2745 Fill NA/NaN values with the specified value.
2746
2747 Parameters
2748 ----------
2749 value : scalar
2750 Scalar value to use to fill holes (e.g. 0).
2751 This value cannot be a list-likes.
2752
2753 Returns
2754 -------
2755 Index
2756 NA/NaN values replaced with `value`.
2757
2758 See Also
2759 --------
2760 DataFrame.fillna : Fill NaN values of a DataFrame.
2761 Series.fillna : Fill NaN Values of a Series.
2762
2763 Examples
2764 --------
2765 >>> idx = pd.Index([np.nan, np.nan, 3])
2766 >>> idx.fillna(0)
2767 Index([0.0, 0.0, 3.0], dtype='float64')
2768 """
2769 if not is_scalar(value):
2770 raise TypeError(f"'value' must be a scalar, passed: {type(value).__name__}")
2771
2772 if self.hasnans:
2773 result = self.putmask(self._isnan, value)
2774 # no need to care metadata other than name
2775 # because it can't have freq if it has NaTs
2776 # _with_infer needed for test_fillna_categorical
2777 return Index._with_infer(result, name=self.name, copy=False)
2778 return self._view()
2779
2780 def dropna(self, how: AnyAll = "any") -> Self:
2781 """
2782 Return Index without NA/NaN values.
2783
2784 Parameters
2785 ----------
2786 how : {'any', 'all'}, default 'any'
2787 If the Index is a MultiIndex, drop the value when any or all levels
2788 are NaN.
2789
2790 Returns
2791 -------
2792 Index
2793 Returns an Index object after removing NA/NaN values.
2794
2795 See Also
2796 --------
2797 Index.fillna : Fill NA/NaN values with the specified value.
2798 Index.isna : Detect missing values.
2799
2800 Examples
2801 --------
2802 >>> idx = pd.Index([1, np.nan, 3])
2803 >>> idx.dropna()
2804 Index([1.0, 3.0], dtype='float64')
2805 """
2806 if how not in ("any", "all"):
2807 raise ValueError(f"invalid how option: {how}")
2808
2809 if self.hasnans:
2810 res_values = self._values[~self._isnan]
2811 return type(self)._simple_new(res_values, name=self.name)
2812 return self._view()
2813
2814 # --------------------------------------------------------------------
2815 # Uniqueness Methods
2816
2817 def unique(self, level: Hashable | None = None) -> Self:
2818 """
2819 Return unique values in the index.
2820
2821 Unique values are returned in order of appearance, this does NOT sort.
2822
2823 Parameters
2824 ----------
2825 level : int or hashable, optional
2826 Only return values from specified level (for MultiIndex).
2827 If int, gets the level by integer position, else by level name.
2828
2829 Returns
2830 -------
2831 Index
2832 Unique values in the index.
2833
2834 See Also
2835 --------
2836 unique : Numpy array of unique values in that column.
2837 Series.unique : Return unique values of Series object.
2838
2839 Examples
2840 --------
2841 >>> idx = pd.Index([1, 1, 2, 3, 3])
2842 >>> idx.unique()
2843 Index([1, 2, 3], dtype='int64')
2844 """
2845 if level is not None:
2846 self._validate_index_level(level)
2847
2848 if self.is_unique:
2849 return self._view()
2850
2851 result = super().unique()
2852 return self._shallow_copy(result)
2853
2854 def drop_duplicates(self, *, keep: DropKeep = "first") -> Self:
2855 """
2856 Return Index with duplicate values removed.
2857
2858 Parameters
2859 ----------
2860 keep : {'first', 'last', ``False``}, default 'first'
2861 - 'first' : Drop duplicates except for the first occurrence.
2862 - 'last' : Drop duplicates except for the last occurrence.
2863 - ``False`` : Drop all duplicates.
2864
2865 Returns
2866 -------
2867 Index
2868 A new Index object with the duplicate values removed.
2869
2870 See Also
2871 --------
2872 Series.drop_duplicates : Equivalent method on Series.
2873 DataFrame.drop_duplicates : Equivalent method on DataFrame.
2874 Index.duplicated : Related method on Index, indicating duplicate
2875 Index values.
2876
2877 Examples
2878 --------
2879 Generate a pandas.Index with duplicate values.
2880
2881 >>> idx = pd.Index(["llama", "cow", "llama", "beetle", "llama", "hippo"])
2882
2883 The `keep` parameter controls which duplicate values are removed.
2884 The value 'first' keeps the first occurrence for each
2885 set of duplicated entries. The default value of keep is 'first'.
2886
2887 >>> idx.drop_duplicates(keep="first")
2888 Index(['llama', 'cow', 'beetle', 'hippo'], dtype='str')
2889
2890 The value 'last' keeps the last occurrence for each set of duplicated
2891 entries.
2892
2893 >>> idx.drop_duplicates(keep="last")
2894 Index(['cow', 'beetle', 'llama', 'hippo'], dtype='str')
2895
2896 The value ``False`` discards all sets of duplicated entries.
2897
2898 >>> idx.drop_duplicates(keep=False)
2899 Index(['cow', 'beetle', 'hippo'], dtype='str')
2900 """
2901 if self.is_unique:
2902 return self._view()
2903
2904 return super().drop_duplicates(keep=keep)
2905
2906 def duplicated(self, keep: DropKeep = "first") -> npt.NDArray[np.bool_]:
2907 """
2908 Indicate duplicate index values.
2909
2910 Duplicated values are indicated as ``True`` values in the resulting
2911 array. Either all duplicates, all except the first, or all except the
2912 last occurrence of duplicates can be indicated.
2913
2914 Parameters
2915 ----------
2916 keep : {'first', 'last', False}, default 'first'
2917 The value or values in a set of duplicates to mark as missing.
2918
2919 - 'first' : Mark duplicates as ``True`` except for the first
2920 occurrence.
2921 - 'last' : Mark duplicates as ``True`` except for the last
2922 occurrence.
2923 - ``False`` : Mark all duplicates as ``True``.
2924
2925 Returns
2926 -------
2927 np.ndarray[bool]
2928 A numpy array of boolean values indicating duplicate index values.
2929
2930 See Also
2931 --------
2932 Series.duplicated : Equivalent method on pandas.Series.
2933 DataFrame.duplicated : Equivalent method on pandas.DataFrame.
2934 Index.drop_duplicates : Remove duplicate values from Index.
2935
2936 Examples
2937 --------
2938 By default, for each set of duplicated values, the first occurrence is
2939 set to False and all others to True:
2940
2941 >>> idx = pd.Index(["llama", "cow", "llama", "beetle", "llama"])
2942 >>> idx.duplicated()
2943 array([False, False, True, False, True])
2944
2945 which is equivalent to
2946
2947 >>> idx.duplicated(keep="first")
2948 array([False, False, True, False, True])
2949
2950 By using 'last', the last occurrence of each set of duplicated values
2951 is set on False and all others on True:
2952
2953 >>> idx.duplicated(keep="last")
2954 array([ True, False, True, False, False])
2955
2956 By setting keep on ``False``, all duplicates are True:
2957
2958 >>> idx.duplicated(keep=False)
2959 array([ True, False, True, False, True])
2960 """
2961 if self.is_unique:
2962 # fastpath available bc we are immutable
2963 return np.zeros(len(self), dtype=bool)
2964 return self._duplicated(keep=keep)
2965
2966 # --------------------------------------------------------------------
2967 # Arithmetic & Logical Methods
2968
2969 def __iadd__(self, other):
2970 # alias for __add__
2971 return self + other
2972
2973 @final
2974 def __bool__(self) -> NoReturn:
2975 raise ValueError(
2976 f"The truth value of a {type(self).__name__} is ambiguous. "
2977 "Use a.empty, a.bool(), a.item(), a.any() or a.all()."
2978 )
2979
2980 # --------------------------------------------------------------------
2981 # Set Operation Methods
2982
2983 def _get_reconciled_name_object(self, other):
2984 """
2985 If the result of a set operation will be self,
2986 return a shallow copy of self.
2987 """
2988 name = get_op_result_name(self, other)
2989 if self.name is not name:
2990 return self.rename(name)
2991 return self.copy(deep=False)
2992
2993 @final
2994 def _validate_sort_keyword(self, sort) -> None:
2995 if sort not in [None, False, True]:
2996 raise ValueError(
2997 "The 'sort' keyword only takes the values of "
2998 f"None, True, or False; {sort} was passed."
2999 )
3000
3001 @final
3002 def _dti_setop_align_tzs(self, other: Index, setop: str_t) -> tuple[Index, Index]:
3003 """
3004 With mismatched timezones, cast both to UTC.
3005 """
3006 # Caller is responsible for checking
3007 # `self.dtype != other.dtype`
3008 if (
3009 isinstance(self, ABCDatetimeIndex)
3010 and isinstance(other, ABCDatetimeIndex)
3011 and self.tz is not None
3012 and other.tz is not None
3013 ):
3014 # GH#39328, GH#45357, GH#60080
3015 # If both timezones are the same, no need to convert to UTC
3016 if self.tz == other.tz:
3017 return self, other
3018 else:
3019 left = self.tz_convert("UTC")
3020 right = other.tz_convert("UTC")
3021 return left, right
3022 return self, other
3023
3024 @final
3025 def union(self, other, sort: bool | None = None):
3026 """
3027 Form the union of two Index objects.
3028
3029 If the Index objects are incompatible, both Index objects will be
3030 cast to dtype('object') first.
3031
3032 Parameters
3033 ----------
3034 other : Index or array-like
3035 Index or an array-like object containing elements to form the union
3036 with the original Index.
3037 sort : bool or None, default None
3038 Whether to sort the resulting Index.
3039
3040 * None : Sort the result, except when
3041
3042 1. `self` and `other` are equal.
3043 2. `self` or `other` has length 0.
3044 3. Some values in `self` or `other` cannot be compared.
3045 A RuntimeWarning is issued in this case.
3046
3047 * False : do not sort the result.
3048 * True : Sort the result (which may raise TypeError).
3049
3050 Returns
3051 -------
3052 Index
3053 Returns a new Index object with all unique elements from both the original
3054 Index and the `other` Index.
3055
3056 See Also
3057 --------
3058 Index.unique : Return unique values in the index.
3059 Index.intersection : Form the intersection of two Index objects.
3060 Index.difference : Return a new Index with elements of index not in `other`.
3061
3062 Examples
3063 --------
3064 Union matching dtypes
3065
3066 >>> idx1 = pd.Index([1, 2, 3, 4])
3067 >>> idx2 = pd.Index([3, 4, 5, 6])
3068 >>> idx1.union(idx2)
3069 Index([1, 2, 3, 4, 5, 6], dtype='int64')
3070
3071 Union mismatched dtypes
3072
3073 >>> idx1 = pd.Index(["a", "b", "c", "d"])
3074 >>> idx2 = pd.Index([1, 2, 3, 4])
3075 >>> idx1.union(idx2)
3076 Index(['a', 'b', 'c', 'd', 1, 2, 3, 4], dtype='object')
3077
3078 MultiIndex case
3079
3080 >>> idx1 = pd.MultiIndex.from_arrays(
3081 ... [[1, 1, 2, 2], ["Red", "Blue", "Red", "Blue"]]
3082 ... )
3083 >>> idx1
3084 MultiIndex([(1, 'Red'),
3085 (1, 'Blue'),
3086 (2, 'Red'),
3087 (2, 'Blue')],
3088 )
3089 >>> idx2 = pd.MultiIndex.from_arrays(
3090 ... [[3, 3, 2, 2], ["Red", "Green", "Red", "Green"]]
3091 ... )
3092 >>> idx2
3093 MultiIndex([(3, 'Red'),
3094 (3, 'Green'),
3095 (2, 'Red'),
3096 (2, 'Green')],
3097 )
3098 >>> idx1.union(idx2)
3099 MultiIndex([(1, 'Blue'),
3100 (1, 'Red'),
3101 (2, 'Blue'),
3102 (2, 'Green'),
3103 (2, 'Red'),
3104 (3, 'Green'),
3105 (3, 'Red')],
3106 )
3107 >>> idx1.union(idx2, sort=False)
3108 MultiIndex([(1, 'Red'),
3109 (1, 'Blue'),
3110 (2, 'Red'),
3111 (2, 'Blue'),
3112 (3, 'Red'),
3113 (3, 'Green'),
3114 (2, 'Green')],
3115 )
3116 """
3117 self._validate_sort_keyword(sort)
3118 self._assert_can_do_setop(other)
3119 other, result_name = self._convert_can_do_setop(other)
3120
3121 if self.dtype != other.dtype:
3122 if (
3123 isinstance(self, ABCMultiIndex)
3124 and not is_object_dtype(_unpack_nested_dtype(other))
3125 and len(other) > 0
3126 ):
3127 raise NotImplementedError(
3128 "Can only union MultiIndex with MultiIndex or Index of tuples, "
3129 "try mi.to_flat_index().union(other) instead."
3130 )
3131 self, other = self._dti_setop_align_tzs(other, "union")
3132
3133 dtype = self._find_common_type_compat(other)
3134 left = self.astype(dtype, copy=False)
3135 right = other.astype(dtype, copy=False)
3136 return left.union(right, sort=sort)
3137
3138 elif not len(other) or self.equals(other):
3139 # NB: whether this (and the `if not len(self)` check below) come before
3140 # or after the dtype equality check above affects the returned dtype
3141 result = self._get_reconciled_name_object(other)
3142 if sort is True:
3143 return result.sort_values()
3144 return result
3145
3146 elif not len(self):
3147 result = other._get_reconciled_name_object(self)
3148 if sort is True:
3149 return result.sort_values()
3150 return result
3151
3152 result = self._union(other, sort=sort)
3153
3154 return self._wrap_setop_result(other, result)
3155
3156 def _union(self, other: Index, sort: bool | None):
3157 """
3158 Specific union logic should go here. In subclasses, union behavior
3159 should be overwritten here rather than in `self.union`.
3160
3161 Parameters
3162 ----------
3163 other : Index or array-like
3164 sort : False or None, default False
3165 Whether to sort the resulting index.
3166
3167 * True : sort the result
3168 * False : do not sort the result.
3169 * None : sort the result, except when `self` and `other` are equal
3170 or when the values cannot be compared.
3171
3172 Returns
3173 -------
3174 Index
3175 """
3176 lvals = self._values
3177 rvals = other._values
3178
3179 if (
3180 sort in (None, True)
3181 and (self.is_unique or other.is_unique)
3182 and self._can_use_libjoin
3183 and other._can_use_libjoin
3184 ):
3185 # Both are monotonic and at least one is unique, so can use outer join
3186 # (actually don't need either unique, but without this restriction
3187 # test_union_same_value_duplicated_in_both fails)
3188 try:
3189 return self._outer_indexer(other)[0]
3190 except TypeError:
3191 # incomparable objects; should only be for object dtype
3192 value_list = list(lvals)
3193
3194 # worth making this faster? a very unusual case
3195 value_set = set(lvals)
3196 value_list.extend(x for x in rvals if x not in value_set)
3197 # If objects are unorderable, we must have object dtype.
3198 return np.array(value_list, dtype=object)
3199
3200 elif not other.is_unique:
3201 # other has duplicates
3202 result_dups = algos.union_with_duplicates(self, other)
3203 return _maybe_try_sort(result_dups, sort)
3204
3205 # The rest of this method is analogous to Index._intersection_via_get_indexer
3206
3207 # Self may have duplicates; other already checked as unique
3208 # find indexes of things in "other" that are not in "self"
3209 if self._index_as_unique:
3210 indexer = self.get_indexer(other)
3211 missing = (indexer == -1).nonzero()[0]
3212 else:
3213 missing = algos.unique1d(self.get_indexer_non_unique(other)[1])
3214
3215 result: Index | MultiIndex | ArrayLike
3216 if self._is_multi:
3217 # Preserve MultiIndex to avoid losing dtypes
3218 result = self.append(other.take(missing))
3219
3220 elif len(missing) > 0:
3221 other_diff = rvals.take(missing)
3222 result = concat_compat((lvals, other_diff))
3223 else:
3224 result = lvals
3225
3226 if not self.is_monotonic_increasing or not other.is_monotonic_increasing:
3227 # if both are monotonic then result should already be sorted
3228 result = _maybe_try_sort(result, sort)
3229
3230 return result
3231
3232 @final
3233 def _wrap_setop_result(self, other: Index, result) -> Index:
3234 name = get_op_result_name(self, other)
3235 if isinstance(result, Index):
3236 if result.name != name:
3237 result = result.rename(name)
3238 else:
3239 result = self._shallow_copy(result, name=name)
3240 return result
3241
3242 @final
3243 def intersection(self, other, sort: bool = False):
3244 # default sort keyword is different here from other setops intentionally
3245 # done in GH#25063
3246 """
3247 Form the intersection of two Index objects.
3248
3249 This returns a new Index with elements common to the index and `other`.
3250
3251 Parameters
3252 ----------
3253 other : Index or array-like
3254 An Index or an array-like object containing elements to form the
3255 intersection with the original Index.
3256 sort : True, False or None, default False
3257 Whether to sort the resulting index.
3258
3259 * None : sort the result, except when `self` and `other` are equal
3260 or when the values cannot be compared.
3261 * False : do not sort the result.
3262 * True : Sort the result (which may raise TypeError).
3263
3264 Returns
3265 -------
3266 Index
3267 Returns a new Index object with elements common to both the original Index
3268 and the `other` Index.
3269
3270 See Also
3271 --------
3272 Index.union : Form the union of two Index objects.
3273 Index.difference : Return a new Index with elements of index not in other.
3274 Index.isin : Return a boolean array where the index values are in values.
3275
3276 Examples
3277 --------
3278 >>> idx1 = pd.Index([1, 2, 3, 4])
3279 >>> idx2 = pd.Index([3, 4, 5, 6])
3280 >>> idx1.intersection(idx2)
3281 Index([3, 4], dtype='int64')
3282 """
3283 self._validate_sort_keyword(sort)
3284 self._assert_can_do_setop(other)
3285 other, result_name = self._convert_can_do_setop(other)
3286
3287 if self.dtype != other.dtype:
3288 self, other = self._dti_setop_align_tzs(other, "intersection")
3289
3290 if self.equals(other):
3291 if not self.is_unique:
3292 result = self.unique()._get_reconciled_name_object(other)
3293 else:
3294 result = self._get_reconciled_name_object(other)
3295 if sort is True:
3296 result = result.sort_values()
3297 return result
3298
3299 if len(self) == 0 or len(other) == 0:
3300 # fastpath; we need to be careful about having commutativity
3301
3302 if self._is_multi or other._is_multi:
3303 # _convert_can_do_setop ensures that we have both or neither
3304 # We retain self.levels
3305 return self[:0].rename(result_name)
3306
3307 dtype = self._find_common_type_compat(other)
3308 if self.dtype == dtype:
3309 # Slicing allows us to retain DTI/TDI.freq, RangeIndex
3310
3311 # Note: self[:0] vs other[:0] affects
3312 # 1) which index's `freq` we get in DTI/TDI cases
3313 # This may be a historical artifact, i.e. no documented
3314 # reason for this choice.
3315 # 2) The `step` we get in RangeIndex cases
3316 if len(self) == 0:
3317 return self[:0].rename(result_name)
3318 else:
3319 return other[:0].rename(result_name)
3320
3321 return Index([], dtype=dtype, name=result_name)
3322
3323 elif not self._should_compare(other):
3324 # We can infer that the intersection is empty.
3325 if isinstance(self, ABCMultiIndex):
3326 return self[:0].rename(result_name)
3327 return Index([], name=result_name)
3328
3329 elif self.dtype != other.dtype:
3330 dtype = self._find_common_type_compat(other)
3331 this = self.astype(dtype, copy=False)
3332 other = other.astype(dtype, copy=False)
3333 return this.intersection(other, sort=sort)
3334
3335 result = self._intersection(other, sort=sort)
3336 return self._wrap_intersection_result(other, result)
3337
3338 def _intersection(self, other: Index, sort: bool = False):
3339 """
3340 intersection specialized to the case with matching dtypes.
3341 """
3342 if self._can_use_libjoin and other._can_use_libjoin:
3343 try:
3344 res_indexer, indexer, _ = self._inner_indexer(other)
3345 except TypeError:
3346 # non-comparable; should only be for object dtype
3347 pass
3348 else:
3349 # TODO: algos.unique1d should preserve DTA/TDA
3350 if is_numeric_dtype(self.dtype):
3351 # This is faster, because Index.unique() checks for uniqueness
3352 # before calculating the unique values.
3353 res = algos.unique1d(res_indexer)
3354 else:
3355 result = self.take(indexer)
3356 res = result.drop_duplicates() # type: ignore[assignment]
3357 return ensure_wrapped_if_datetimelike(res)
3358
3359 res_values = self._intersection_via_get_indexer(other, sort=sort)
3360 res_values = _maybe_try_sort(res_values, sort)
3361 return res_values
3362
3363 def _wrap_intersection_result(self, other, result):
3364 # We will override for MultiIndex to handle empty results
3365 return self._wrap_setop_result(other, result)
3366
3367 @final
3368 def _intersection_via_get_indexer(
3369 self, other: Index | MultiIndex, sort
3370 ) -> ArrayLike | MultiIndex:
3371 """
3372 Find the intersection of two Indexes using get_indexer.
3373
3374 Returns
3375 -------
3376 np.ndarray or ExtensionArray or MultiIndex
3377 The returned array will be unique.
3378 """
3379 left_unique = self.unique()
3380 right_unique = other.unique()
3381
3382 # even though we are unique, we need get_indexer_for for IntervalIndex
3383 indexer = left_unique.get_indexer_for(right_unique)
3384
3385 mask = indexer != -1
3386
3387 taker = indexer.take(mask.nonzero()[0])
3388 if sort is False:
3389 # sort bc we want the elements in the same order they are in self
3390 # unnecessary in the case with sort=None bc we will sort later
3391 taker = np.sort(taker)
3392
3393 result: MultiIndex | ExtensionArray | np.ndarray
3394 if isinstance(left_unique, ABCMultiIndex):
3395 result = left_unique.take(taker)
3396 else:
3397 result = left_unique.take(taker)._values
3398 return result
3399
3400 @final
3401 def difference(self, other, sort: bool | None = None):
3402 """
3403 Return a new Index with elements of index not in `other`.
3404
3405 This is the set difference of two Index objects.
3406
3407 Parameters
3408 ----------
3409 other : Index or array-like
3410 Index object or an array-like object containing elements to be compared
3411 with the elements of the original Index.
3412 sort : bool or None, default None
3413 Whether to sort the resulting index. By default, the
3414 values are attempted to be sorted, but any TypeError from
3415 incomparable elements is caught by pandas.
3416
3417 * None : Attempt to sort the result, but catch any TypeErrors
3418 from comparing incomparable elements.
3419 * False : Do not sort the result.
3420 * True : Sort the result (which may raise TypeError).
3421
3422 Returns
3423 -------
3424 Index
3425 Returns a new Index object containing elements that are in the original
3426 Index but not in the `other` Index.
3427
3428 See Also
3429 --------
3430 Index.symmetric_difference : Compute the symmetric difference of two Index
3431 objects.
3432 Index.intersection : Form the intersection of two Index objects.
3433
3434 Examples
3435 --------
3436 >>> idx1 = pd.Index([2, 1, 3, 4])
3437 >>> idx2 = pd.Index([3, 4, 5, 6])
3438 >>> idx1.difference(idx2)
3439 Index([1, 2], dtype='int64')
3440 >>> idx1.difference(idx2, sort=False)
3441 Index([2, 1], dtype='int64')
3442 """
3443 self._validate_sort_keyword(sort)
3444 self._assert_can_do_setop(other)
3445 other, result_name = self._convert_can_do_setop(other)
3446
3447 # Note: we do NOT call _dti_setop_align_tzs here, as there
3448 # is no requirement that .difference be commutative, so it does
3449 # not cast to object.
3450
3451 if self.equals(other):
3452 # Note: we do not (yet) sort even if sort=None GH#24959
3453 return self[:0].rename(result_name)
3454
3455 if len(other) == 0:
3456 # Note: we do not (yet) sort even if sort=None GH#24959
3457 result = self.unique().rename(result_name)
3458 if sort is True:
3459 return result.sort_values()
3460 return result
3461
3462 if not self._should_compare(other):
3463 # Nothing matches -> difference is everything
3464 result = self.unique().rename(result_name)
3465 if sort is True:
3466 return result.sort_values()
3467 return result
3468
3469 result = self._difference(other, sort=sort)
3470 return self._wrap_difference_result(other, result)
3471
3472 def _difference(self, other, sort):
3473 # overridden by RangeIndex
3474 this = self
3475 if isinstance(self, ABCCategoricalIndex) and self.hasnans and other.hasnans:
3476 this = this.dropna()
3477 other = other.unique()
3478 the_diff = this[other.get_indexer_for(this) == -1]
3479 the_diff = the_diff if this.is_unique else the_diff.unique()
3480 the_diff = _maybe_try_sort(the_diff, sort)
3481 return the_diff
3482
3483 def _wrap_difference_result(self, other, result):
3484 # We will override for MultiIndex to handle empty results
3485 return self._wrap_setop_result(other, result)
3486
3487 def symmetric_difference(
3488 self,
3489 other,
3490 result_name: abc.Hashable | None = None,
3491 sort: bool | None = None,
3492 ):
3493 """
3494 Compute the symmetric difference of two Index objects.
3495
3496 Parameters
3497 ----------
3498 other : Index or array-like
3499 Index or an array-like object with elements to compute the symmetric
3500 difference with the original Index.
3501 result_name : str
3502 A string representing the name of the resulting Index, if desired.
3503 sort : bool or None, default None
3504 Whether to sort the resulting index. By default, the
3505 values are attempted to be sorted, but any TypeError from
3506 incomparable elements is caught by pandas.
3507
3508 * None : Attempt to sort the result, but catch any TypeErrors
3509 from comparing incomparable elements.
3510 * False : Do not sort the result.
3511 * True : Sort the result (which may raise TypeError).
3512
3513 Returns
3514 -------
3515 Index
3516 Returns a new Index object containing elements that appear in either the
3517 original Index or the `other` Index, but not both.
3518
3519 See Also
3520 --------
3521 Index.difference : Return a new Index with elements of index not in other.
3522 Index.union : Form the union of two Index objects.
3523 Index.intersection : Form the intersection of two Index objects.
3524
3525 Notes
3526 -----
3527 ``symmetric_difference`` contains elements that appear in either
3528 ``idx1`` or ``idx2`` but not both. Equivalent to the Index created by
3529 ``idx1.difference(idx2) | idx2.difference(idx1)`` with duplicates
3530 dropped.
3531
3532 Examples
3533 --------
3534 >>> idx1 = pd.Index([1, 2, 3, 4])
3535 >>> idx2 = pd.Index([2, 3, 4, 5])
3536 >>> idx1.symmetric_difference(idx2)
3537 Index([1, 5], dtype='int64')
3538 """
3539 self._validate_sort_keyword(sort)
3540 self._assert_can_do_setop(other)
3541 other, result_name_update = self._convert_can_do_setop(other)
3542 if result_name is None:
3543 result_name = result_name_update
3544
3545 if self.dtype != other.dtype:
3546 self, other = self._dti_setop_align_tzs(other, "symmetric_difference")
3547
3548 if not self._should_compare(other):
3549 return self.union(other, sort=sort).rename(result_name)
3550
3551 elif self.dtype != other.dtype:
3552 dtype = self._find_common_type_compat(other)
3553 this = self.astype(dtype, copy=False)
3554 that = other.astype(dtype, copy=False)
3555 return this.symmetric_difference(that, sort=sort).rename(result_name)
3556
3557 this = self.unique()
3558 other = other.unique()
3559 indexer = this.get_indexer_for(other)
3560
3561 # {this} minus {other}
3562 common_indexer = indexer.take((indexer != -1).nonzero()[0])
3563 left_indexer = np.setdiff1d(
3564 np.arange(this.size), common_indexer, assume_unique=True
3565 )
3566 left_diff = this.take(left_indexer)
3567
3568 # {other} minus {this}
3569 right_indexer = (indexer == -1).nonzero()[0]
3570 right_diff = other.take(right_indexer)
3571
3572 res_values = left_diff.append(right_diff)
3573 result = _maybe_try_sort(res_values, sort)
3574
3575 if not self._is_multi:
3576 return Index(result, name=result_name, dtype=res_values.dtype)
3577 else:
3578 left_diff = cast("MultiIndex", left_diff)
3579 if len(result) == 0:
3580 # result might be an Index, if other was an Index
3581 return left_diff.remove_unused_levels().set_names(result_name)
3582 return result.set_names(result_name)
3583
3584 @final
3585 def _assert_can_do_setop(self, other) -> bool:
3586 if not is_list_like(other):
3587 raise TypeError("Input must be Index or array-like")
3588 return True
3589
3590 def _convert_can_do_setop(self, other) -> tuple[Index, Hashable]:
3591 if not isinstance(other, Index):
3592 other = Index(other, name=self.name)
3593 result_name = self.name
3594 else:
3595 result_name = get_op_result_name(self, other)
3596 return other, result_name
3597
3598 # --------------------------------------------------------------------
3599 # Indexing Methods
3600
3601 def get_loc(self, key):
3602 """
3603 Get integer location, slice or boolean mask for requested label.
3604
3605 Parameters
3606 ----------
3607 key : label
3608 The key to check its location if it is present in the index.
3609
3610 Returns
3611 -------
3612 int if unique index, slice if monotonic index, else mask
3613 Integer location, slice or boolean mask.
3614
3615 See Also
3616 --------
3617 Index.get_slice_bound : Calculate slice bound that corresponds to
3618 given label.
3619 Index.get_indexer : Computes indexer and mask for new index given
3620 the current index.
3621 Index.get_non_unique : Returns indexer and masks for new index given
3622 the current index.
3623 Index.get_indexer_for : Returns an indexer even when non-unique.
3624
3625 Examples
3626 --------
3627 >>> unique_index = pd.Index(list("abc"))
3628 >>> unique_index.get_loc("b")
3629 1
3630
3631 >>> monotonic_index = pd.Index(list("abbc"))
3632 >>> monotonic_index.get_loc("b")
3633 slice(1, 3, None)
3634
3635 >>> non_monotonic_index = pd.Index(list("abcb"))
3636 >>> non_monotonic_index.get_loc("b")
3637 array([False, True, False, True])
3638 """
3639 casted_key = self._maybe_cast_indexer(key)
3640 try:
3641 return self._engine.get_loc(casted_key)
3642 except KeyError as err:
3643 if isinstance(casted_key, slice) or (
3644 isinstance(casted_key, abc.Iterable)
3645 and any(isinstance(x, slice) for x in casted_key)
3646 ):
3647 raise InvalidIndexError(key) from err
3648 raise KeyError(key) from err
3649 except TypeError:
3650 # If we have a listlike key, _check_indexing_error will raise
3651 # InvalidIndexError. Otherwise we fall through and re-raise
3652 # the TypeError.
3653 self._check_indexing_error(key)
3654 raise
3655
3656 @final
3657 def get_indexer(
3658 self,
3659 target,
3660 method: ReindexMethod | None = None,
3661 limit: int | None = None,
3662 tolerance=None,
3663 ) -> npt.NDArray[np.intp]:
3664 """
3665 Compute indexer and mask for new index given the current index.
3666
3667 The indexer should be then used as an input to ndarray.take to align the
3668 current data to the new index.
3669
3670 Parameters
3671 ----------
3672 target : Index
3673 An iterable containing the values to be used for computing indexer.
3674 method : {None, 'pad'/'ffill', 'backfill'/'bfill', 'nearest'}, optional
3675 * default: exact matches only.
3676 * pad / ffill: find the PREVIOUS index value if no exact match.
3677 * backfill / bfill: use NEXT index value if no exact match
3678 * nearest: use the NEAREST index value if no exact match. Tied
3679 distances are broken by preferring the larger index value.
3680 limit : int, optional
3681 Maximum number of consecutive labels in ``target`` to match for
3682 inexact matches.
3683 tolerance : optional
3684 Maximum distance between original and new labels for inexact
3685 matches. The values of the index at the matching locations must
3686 satisfy the equation ``abs(index[indexer] - target) <= tolerance``.
3687
3688 Tolerance may be a scalar value, which applies the same tolerance
3689 to all values, or list-like, which applies variable tolerance per
3690 element. List-like includes list, tuple, array, Series, and must be
3691 the same size as the index and its dtype must exactly match the
3692 index's type.
3693
3694 Returns
3695 -------
3696 np.ndarray[np.intp]
3697 Integers from 0 to n - 1 indicating that the index at these
3698 positions matches the corresponding target values. Missing values
3699 in the target are marked by -1.
3700
3701 See Also
3702 --------
3703 Index.get_indexer_for : Returns an indexer even when non-unique.
3704 Index.get_non_unique : Returns indexer and masks for new index given
3705 the current index.
3706
3707 Notes
3708 -----
3709 Returns -1 for unmatched values, for further explanation see the
3710 example below.
3711
3712 Examples
3713 --------
3714 >>> index = pd.Index(["c", "a", "b"])
3715 >>> index.get_indexer(["a", "b", "x"])
3716 array([ 1, 2, -1])
3717
3718 Notice that the return value is an array of locations in ``index``
3719 and ``x`` is marked by -1, as it is not in ``index``.
3720 """
3721 method = clean_reindex_fill_method(method)
3722 orig_target = target
3723 target = self._maybe_cast_listlike_indexer(target)
3724
3725 self._check_indexing_method(method, limit, tolerance)
3726
3727 if not self._index_as_unique:
3728 raise InvalidIndexError(self._requires_unique_msg)
3729
3730 if len(target) == 0:
3731 return np.array([], dtype=np.intp)
3732
3733 if not self._should_compare(target) and not self._should_partial_index(target):
3734 # IntervalIndex get special treatment bc numeric scalars can be
3735 # matched to Interval scalars
3736 return self._get_indexer_non_comparable(target, method=method, unique=True)
3737
3738 if isinstance(self.dtype, CategoricalDtype):
3739 # _maybe_cast_listlike_indexer ensures target has our dtype
3740 # (could improve perf by doing _should_compare check earlier?)
3741 assert self.dtype == target.dtype
3742
3743 indexer = self._engine.get_indexer(target.codes)
3744 if self.hasnans and target.hasnans:
3745 # After _maybe_cast_listlike_indexer, target elements which do not
3746 # belong to some category are changed to NaNs
3747 # Mask to track actual NaN values compared to inserted NaN values
3748 # GH#45361
3749 target_nans = isna(orig_target)
3750 loc = self.get_loc(np.nan)
3751 mask = target.isna()
3752 indexer[target_nans] = loc
3753 indexer[mask & ~target_nans] = -1
3754 return indexer
3755
3756 if isinstance(target.dtype, CategoricalDtype):
3757 # potential fastpath
3758 # get an indexer for unique categories then propagate to codes via take_nd
3759 # get_indexer instead of _get_indexer needed for MultiIndex cases
3760 # e.g. test_append_different_columns_types
3761 categories_indexer = self.get_indexer(target.categories)
3762
3763 indexer = algos.take_nd(categories_indexer, target.codes, fill_value=-1)
3764
3765 if (not self._is_multi and self.hasnans) and target.hasnans:
3766 # Exclude MultiIndex because hasnans raises NotImplementedError
3767 # we should only get here if we are unique, so loc is an integer
3768 # GH#41934
3769 loc = self.get_loc(np.nan)
3770 mask = target.isna()
3771 indexer[mask] = loc
3772
3773 return ensure_platform_int(indexer)
3774
3775 pself, ptarget = self._maybe_downcast_for_indexing(target)
3776 if pself is not self or ptarget is not target:
3777 return pself.get_indexer(
3778 ptarget, method=method, limit=limit, tolerance=tolerance
3779 )
3780
3781 if self.dtype == target.dtype and self.equals(target):
3782 # Only call equals if we have same dtype to avoid inference/casting
3783 return np.arange(len(target), dtype=np.intp)
3784
3785 if self.dtype != target.dtype and not self._should_partial_index(target):
3786 # _should_partial_index e.g. IntervalIndex with numeric scalars
3787 # that can be matched to Interval scalars.
3788 dtype = self._find_common_type_compat(target)
3789
3790 this = self.astype(dtype, copy=False)
3791 target = target.astype(dtype, copy=False)
3792 return this._get_indexer(
3793 target, method=method, limit=limit, tolerance=tolerance
3794 )
3795
3796 return self._get_indexer(target, method, limit, tolerance)
3797
3798 def _get_indexer(
3799 self,
3800 target: Index,
3801 method: str_t | None = None,
3802 limit: int | None = None,
3803 tolerance=None,
3804 ) -> npt.NDArray[np.intp]:
3805 if tolerance is not None:
3806 tolerance = self._convert_tolerance(tolerance, target)
3807
3808 if method in ["pad", "backfill"]:
3809 indexer = self._get_fill_indexer(target, method, limit, tolerance)
3810 elif method == "nearest":
3811 indexer = self._get_nearest_indexer(target, limit, tolerance)
3812 else:
3813 if target._is_multi and self._is_multi:
3814 engine = self._engine
3815 # error: Item "IndexEngine" of "Union[IndexEngine, ExtensionEngine]"
3816 # has no attribute "_extract_level_codes"
3817 tgt_values = engine._extract_level_codes( # type: ignore[union-attr]
3818 target
3819 )
3820 else:
3821 tgt_values = target._get_engine_target()
3822
3823 indexer = self._engine.get_indexer(tgt_values)
3824
3825 return ensure_platform_int(indexer)
3826
3827 @final
3828 def _should_partial_index(self, target: Index) -> bool:
3829 """
3830 Should we attempt partial-matching indexing?
3831 """
3832 if isinstance(self.dtype, IntervalDtype):
3833 if isinstance(target.dtype, IntervalDtype):
3834 return False
3835 # "Index" has no attribute "left"
3836 return self.left._should_compare(target) # type: ignore[attr-defined]
3837 return False
3838
3839 @final
3840 def _check_indexing_method(
3841 self,
3842 method: str_t | None,
3843 limit: int | None = None,
3844 tolerance=None,
3845 ) -> None:
3846 """
3847 Raise if we have a get_indexer `method` that is not supported or valid.
3848 """
3849 if method not in [None, "bfill", "backfill", "pad", "ffill", "nearest"]:
3850 # in practice the clean_reindex_fill_method call would raise
3851 # before we get here
3852 raise ValueError("Invalid fill method") # pragma: no cover
3853
3854 if self._is_multi:
3855 if method == "nearest":
3856 raise NotImplementedError(
3857 "method='nearest' not implemented yet "
3858 "for MultiIndex; see GitHub issue 9365"
3859 )
3860 if method in ("pad", "backfill"):
3861 if tolerance is not None:
3862 raise NotImplementedError(
3863 "tolerance not implemented yet for MultiIndex"
3864 )
3865
3866 if isinstance(self.dtype, (IntervalDtype, CategoricalDtype)):
3867 # GH#37871 for now this is only for IntervalIndex and CategoricalIndex
3868 if method is not None:
3869 raise NotImplementedError(
3870 f"method {method} not yet implemented for {type(self).__name__}"
3871 )
3872
3873 if method is None:
3874 if tolerance is not None:
3875 raise ValueError(
3876 "tolerance argument only valid if doing pad, "
3877 "backfill or nearest reindexing"
3878 )
3879 if limit is not None:
3880 raise ValueError(
3881 "limit argument only valid if doing pad, "
3882 "backfill or nearest reindexing"
3883 )
3884
3885 def _convert_tolerance(self, tolerance, target: np.ndarray | Index) -> np.ndarray:
3886 # override this method on subclasses
3887 tolerance = np.asarray(tolerance)
3888 if target.size != tolerance.size and tolerance.size > 1:
3889 raise ValueError("list-like tolerance size must match target index size")
3890 elif is_numeric_dtype(self) and not np.issubdtype(tolerance.dtype, np.number):
3891 if tolerance.ndim > 0:
3892 raise ValueError(
3893 f"tolerance argument for {type(self).__name__} with dtype "
3894 f"{self.dtype} must contain numeric elements if it is list type"
3895 )
3896
3897 raise ValueError(
3898 f"tolerance argument for {type(self).__name__} with dtype {self.dtype} "
3899 f"must be numeric if it is a scalar: {tolerance!r}"
3900 )
3901 return tolerance
3902
3903 @final
3904 def _get_fill_indexer(
3905 self, target: Index, method: str_t, limit: int | None = None, tolerance=None
3906 ) -> npt.NDArray[np.intp]:
3907 if self._is_multi:
3908 if not (self.is_monotonic_increasing or self.is_monotonic_decreasing):
3909 raise ValueError("index must be monotonic increasing or decreasing")
3910 encoded = self.append(target)._engine.values # type: ignore[union-attr]
3911 self_encoded = Index(encoded[: len(self)], copy=False)
3912 target_encoded = Index(encoded[len(self) :], copy=False)
3913 return self_encoded._get_fill_indexer(
3914 target_encoded, method, limit, tolerance
3915 )
3916
3917 if self.is_monotonic_increasing and target.is_monotonic_increasing:
3918 target_values = target._get_engine_target()
3919 own_values = self._get_engine_target()
3920 if not isinstance(target_values, np.ndarray) or not isinstance(
3921 own_values, np.ndarray
3922 ):
3923 raise NotImplementedError
3924
3925 if method == "pad":
3926 indexer = libalgos.pad(own_values, target_values, limit=limit)
3927 else:
3928 # i.e. "backfill"
3929 indexer = libalgos.backfill(own_values, target_values, limit=limit)
3930 else:
3931 indexer = self._get_fill_indexer_searchsorted(target, method, limit)
3932 if tolerance is not None and len(self):
3933 indexer = self._filter_indexer_tolerance(target, indexer, tolerance)
3934 return indexer
3935
3936 @final
3937 def _get_fill_indexer_searchsorted(
3938 self, target: Index, method: str_t, limit: int | None = None
3939 ) -> npt.NDArray[np.intp]:
3940 """
3941 Fallback pad/backfill get_indexer that works for monotonic decreasing
3942 indexes and non-monotonic targets.
3943 """
3944 if limit is not None:
3945 raise ValueError(
3946 f"limit argument for {method!r} method only well-defined "
3947 "if index and target are monotonic"
3948 )
3949
3950 side: Literal["left", "right"] = "left" if method == "pad" else "right"
3951
3952 # find exact matches first (this simplifies the algorithm)
3953 indexer = self.get_indexer(target)
3954 nonexact = indexer == -1
3955 indexer[nonexact] = self._searchsorted_monotonic(target[nonexact], side)
3956 if side == "left":
3957 # searchsorted returns "indices into a sorted array such that,
3958 # if the corresponding elements in v were inserted before the
3959 # indices, the order of a would be preserved".
3960 # Thus, we need to subtract 1 to find values to the left.
3961 indexer[nonexact] -= 1
3962 # This also mapped not found values (values of 0 from
3963 # np.searchsorted) to -1, which conveniently is also our
3964 # sentinel for missing values
3965 else:
3966 # Mark indices to the right of the largest value as not found
3967 indexer[indexer == len(self)] = -1
3968 return indexer
3969
3970 @final
3971 def _get_nearest_indexer(
3972 self, target: Index, limit: int | None, tolerance
3973 ) -> npt.NDArray[np.intp]:
3974 """
3975 Get the indexer for the nearest index labels; requires an index with
3976 values that can be subtracted from each other (e.g., not strings or
3977 tuples).
3978 """
3979 if not len(self):
3980 return self._get_fill_indexer(target, "pad")
3981
3982 left_indexer = self.get_indexer(target, "pad", limit=limit)
3983 right_indexer = self.get_indexer(target, "backfill", limit=limit)
3984
3985 left_distances = self._difference_compat(target, left_indexer)
3986 right_distances = self._difference_compat(target, right_indexer)
3987
3988 op = operator.lt if self.is_monotonic_increasing else operator.le
3989 indexer = np.where(
3990 # error: Argument 1&2 has incompatible type "Union[ExtensionArray,
3991 # ndarray[Any, Any]]"; expected "Union[SupportsDunderLE,
3992 # SupportsDunderGE, SupportsDunderGT, SupportsDunderLT]"
3993 op(left_distances, right_distances) # type: ignore[arg-type]
3994 | (right_indexer == -1),
3995 left_indexer,
3996 right_indexer,
3997 )
3998 if tolerance is not None:
3999 indexer = self._filter_indexer_tolerance(target, indexer, tolerance)
4000 return indexer
4001
4002 @final
4003 def _filter_indexer_tolerance(
4004 self,
4005 target: Index,
4006 indexer: npt.NDArray[np.intp],
4007 tolerance,
4008 ) -> npt.NDArray[np.intp]:
4009 distance = self._difference_compat(target, indexer)
4010
4011 return np.where(distance <= tolerance, indexer, -1)
4012
4013 @final
4014 def _difference_compat(
4015 self, target: Index, indexer: npt.NDArray[np.intp]
4016 ) -> ArrayLike:
4017 # Compatibility for PeriodArray, for which __sub__ returns an ndarray[object]
4018 # of DateOffset objects, which do not support __abs__ (and would be slow
4019 # if they did)
4020
4021 if isinstance(self.dtype, PeriodDtype):
4022 # Note: we only get here with matching dtypes
4023 own_values = cast("PeriodArray", self._data)._ndarray
4024 target_values = cast("PeriodArray", target._data)._ndarray
4025 diff = own_values[indexer] - target_values
4026 else:
4027 # error: Unsupported left operand type for - ("ExtensionArray")
4028 diff = self._values[indexer] - target._values # type: ignore[operator]
4029 return abs(diff)
4030
4031 # --------------------------------------------------------------------
4032 # Indexer Conversion Methods
4033
4034 @final
4035 def _validate_positional_slice(self, key: slice) -> None:
4036 """
4037 For positional indexing, a slice must have either int or None
4038 for each of start, stop, and step.
4039 """
4040 self._validate_indexer("positional", key.start, "iloc")
4041 self._validate_indexer("positional", key.stop, "iloc")
4042 self._validate_indexer("positional", key.step, "iloc")
4043
4044 def _convert_slice_indexer(self, key: slice, kind: Literal["loc", "getitem"]):
4045 """
4046 Convert a slice indexer.
4047
4048 By definition, these are labels unless 'iloc' is passed in.
4049 Floats are not allowed as the start, step, or stop of the slice.
4050
4051 Parameters
4052 ----------
4053 key : label of the slice bound
4054 kind : {'loc', 'getitem'}
4055 """
4056
4057 # potentially cast the bounds to integers
4058 start, stop, step = key.start, key.stop, key.step
4059
4060 # figure out if this is a positional indexer
4061 is_index_slice = is_valid_positional_slice(key)
4062
4063 # TODO(GH#50617): once Series.__[gs]etitem__ is removed we should be able
4064 # to simplify this.
4065 if kind == "getitem":
4066 # called from the getitem slicers, validate that we are in fact integers
4067 if is_index_slice:
4068 # In this case the _validate_indexer checks below are redundant
4069 return key
4070 elif self.dtype.kind in "iu":
4071 # Note: these checks are redundant if we know is_index_slice
4072 self._validate_indexer("slice", key.start, "getitem")
4073 self._validate_indexer("slice", key.stop, "getitem")
4074 self._validate_indexer("slice", key.step, "getitem")
4075 return key
4076
4077 # convert the slice to an indexer here; checking that the user didn't
4078 # pass a positional slice to loc
4079 is_positional = is_index_slice and self._should_fallback_to_positional
4080
4081 # if we are mixed and have integers
4082 if is_positional:
4083 try:
4084 # Validate start & stop
4085 if start is not None:
4086 self.get_loc(start)
4087 if stop is not None:
4088 self.get_loc(stop)
4089 is_positional = False
4090 except KeyError:
4091 pass
4092
4093 if com.is_null_slice(key):
4094 # It doesn't matter if we are positional or label based
4095 indexer = key
4096 elif is_positional:
4097 if kind == "loc":
4098 # GH#16121, GH#24612, GH#31810
4099 raise TypeError(
4100 "Slicing a positional slice with .loc is not allowed, "
4101 "Use .loc with labels or .iloc with positions instead.",
4102 )
4103 indexer = key
4104 else:
4105 indexer = self.slice_indexer(start, stop, step)
4106
4107 return indexer
4108
4109 @final
4110 def _raise_invalid_indexer(
4111 self,
4112 form: Literal["slice", "positional"],
4113 key,
4114 reraise: lib.NoDefault | None | Exception = lib.no_default,
4115 ) -> None:
4116 """
4117 Raise consistent invalid indexer message.
4118 """
4119 msg = (
4120 f"cannot do {form} indexing on {type(self).__name__} with these "
4121 f"indexers [{key}] of type {type(key).__name__}"
4122 )
4123 if reraise is not lib.no_default:
4124 raise TypeError(msg) from reraise
4125 raise TypeError(msg)
4126
4127 # --------------------------------------------------------------------
4128 # Reindex Methods
4129
4130 @final
4131 def _validate_can_reindex(self, indexer: np.ndarray) -> None:
4132 """
4133 Check if we are allowing reindexing with this particular indexer.
4134
4135 Parameters
4136 ----------
4137 indexer : an integer ndarray
4138
4139 Raises
4140 ------
4141 ValueError if its a duplicate axis
4142 """
4143 # trying to reindex on an axis with duplicates
4144 if not self._index_as_unique and len(indexer):
4145 raise ValueError("cannot reindex on an axis with duplicate labels")
4146
4147 def reindex(
4148 self,
4149 target,
4150 method: ReindexMethod | None = None,
4151 level=None,
4152 limit: int | None = None,
4153 tolerance: float | None = None,
4154 ) -> tuple[Index, npt.NDArray[np.intp] | None]:
4155 """
4156 Create index with target's values.
4157
4158 Parameters
4159 ----------
4160 target : an iterable
4161 An iterable containing the values to be used for creating the new index.
4162 method : {None, 'pad'/'ffill', 'backfill'/'bfill', 'nearest'}, optional
4163 * default: exact matches only.
4164 * pad / ffill: find the PREVIOUS index value if no exact match.
4165 * backfill / bfill: use NEXT index value if no exact match
4166 * nearest: use the NEAREST index value if no exact match. Tied
4167 distances are broken by preferring the larger index value.
4168 level : int, optional
4169 Level of multiindex.
4170 limit : int, optional
4171 Maximum number of consecutive labels in ``target`` to match for
4172 inexact matches.
4173 tolerance : int, float, or list-like, optional
4174 Maximum distance between original and new labels for inexact
4175 matches. The values of the index at the matching locations must
4176 satisfy the equation ``abs(index[indexer] - target) <= tolerance``.
4177
4178 Tolerance may be a scalar value, which applies the same tolerance
4179 to all values, or list-like, which applies variable tolerance per
4180 element. List-like includes list, tuple, array, Series, and must be
4181 the same size as the index and its dtype must exactly match the
4182 index's type.
4183
4184 Returns
4185 -------
4186 new_index : pd.Index
4187 Resulting index.
4188 indexer : np.ndarray[np.intp] or None
4189 Indices of output values in original index.
4190
4191 Raises
4192 ------
4193 TypeError
4194 If ``method`` passed along with ``level``.
4195 ValueError
4196 If non-unique multi-index
4197 ValueError
4198 If non-unique index and ``method`` or ``limit`` passed.
4199
4200 See Also
4201 --------
4202 Series.reindex : Conform Series to new index with optional filling logic.
4203 DataFrame.reindex : Conform DataFrame to new index with optional filling logic.
4204
4205 Examples
4206 --------
4207 >>> idx = pd.Index(["car", "bike", "train", "tractor"])
4208 >>> idx
4209 Index(['car', 'bike', 'train', 'tractor'], dtype='str')
4210 >>> idx.reindex(["car", "bike"])
4211 (Index(['car', 'bike'], dtype='str'), array([0, 1]))
4212 """
4213 # GH6552: preserve names when reindexing to non-named target
4214 # (i.e. neither Index nor Series).
4215 preserve_names = not hasattr(target, "name")
4216
4217 # GH7774: preserve dtype/tz if target is empty and not an Index.
4218 if is_iterator(target):
4219 target = list(target)
4220
4221 if not isinstance(target, Index) and len(target) == 0:
4222 if level is not None and self._is_multi:
4223 # "Index" has no attribute "levels"; maybe "nlevels"?
4224 idx = self.levels[level] # type: ignore[attr-defined]
4225 else:
4226 idx = self
4227 target = idx[:0]
4228 else:
4229 target = ensure_index(target)
4230
4231 if level is not None and (
4232 isinstance(self, ABCMultiIndex) or isinstance(target, ABCMultiIndex)
4233 ):
4234 if method is not None:
4235 raise TypeError("Fill method not supported if level passed")
4236
4237 # TODO: tests where passing `keep_order=not self._is_multi`
4238 # makes a difference for non-MultiIndex case
4239 target, indexer, _ = self._join_level(
4240 target, level, how="right", keep_order=not self._is_multi
4241 )
4242
4243 elif self.equals(target):
4244 indexer = None
4245 elif self._index_as_unique:
4246 indexer = self.get_indexer(
4247 target, method=method, limit=limit, tolerance=tolerance
4248 )
4249 elif self._is_multi:
4250 raise ValueError("cannot handle a non-unique multi-index!")
4251 elif not self.is_unique:
4252 # GH#42568
4253 raise ValueError("cannot reindex on an axis with duplicate labels")
4254 else:
4255 indexer, _ = self.get_indexer_non_unique(target)
4256
4257 target = self._wrap_reindex_result(target, indexer, preserve_names)
4258 return target, indexer
4259
4260 def _wrap_reindex_result(self, target, indexer, preserve_names: bool):
4261 target = self._maybe_preserve_names(target, preserve_names)
4262 return target
4263
4264 def _maybe_preserve_names(self, target: IndexT, preserve_names: bool) -> IndexT:
4265 if preserve_names and target.nlevels == 1 and target.name != self.name:
4266 target = target.copy(deep=False)
4267 target.name = self.name
4268 return target
4269
4270 @final
4271 def _reindex_non_unique(
4272 self, target: Index
4273 ) -> tuple[Index, npt.NDArray[np.intp], npt.NDArray[np.intp] | None]:
4274 """
4275 Create a new index with target's values (move/add/delete values as
4276 necessary) use with non-unique Index and a possibly non-unique target.
4277
4278 Parameters
4279 ----------
4280 target : an iterable
4281
4282 Returns
4283 -------
4284 new_index : pd.Index
4285 Resulting index.
4286 indexer : np.ndarray[np.intp]
4287 Indices of output values in original index.
4288 new_indexer : np.ndarray[np.intp] or None
4289
4290 """
4291 target = ensure_index(target)
4292 if len(target) == 0:
4293 # GH#13691
4294 return self[:0], np.array([], dtype=np.intp), None
4295
4296 indexer, missing = self.get_indexer_non_unique(target)
4297 check = indexer != -1
4298 new_labels: Index | np.ndarray = self.take(indexer[check])
4299 new_indexer = None
4300
4301 if len(missing):
4302 length = np.arange(len(indexer), dtype=np.intp)
4303
4304 missing = ensure_platform_int(missing)
4305 missing_labels = target.take(missing)
4306 missing_indexer = length[~check]
4307 cur_labels = self.take(indexer[check]).values
4308 cur_indexer = length[check]
4309
4310 # Index constructor below will do inference
4311 new_labels = np.empty((len(indexer),), dtype=object)
4312 new_labels[cur_indexer] = cur_labels
4313 new_labels[missing_indexer] = missing_labels
4314
4315 # GH#38906
4316 if not len(self):
4317 new_indexer = np.arange(0, dtype=np.intp)
4318
4319 # a unique indexer
4320 elif target.is_unique:
4321 # see GH5553, make sure we use the right indexer
4322 new_indexer = np.arange(len(indexer), dtype=np.intp)
4323 new_indexer[cur_indexer] = np.arange(len(cur_labels))
4324 new_indexer[missing_indexer] = -1
4325
4326 # we have a non_unique selector, need to use the original
4327 # indexer here
4328 else:
4329 # need to retake to have the same size as the indexer
4330 indexer[~check] = -1
4331
4332 # reset the new indexer to account for the new size
4333 new_indexer = np.arange(len(self.take(indexer)), dtype=np.intp)
4334 new_indexer[~check] = -1
4335
4336 if not isinstance(self, ABCMultiIndex):
4337 new_index = Index(new_labels, name=self.name, copy=False)
4338 else:
4339 new_index = type(self).from_tuples(new_labels, names=self.names)
4340 return new_index, indexer, new_indexer
4341
4342 # --------------------------------------------------------------------
4343 # Join Methods
4344
4345 @overload
4346 def join(
4347 self,
4348 other: Index,
4349 *,
4350 how: JoinHow = ...,
4351 level: Level = ...,
4352 return_indexers: Literal[True],
4353 sort: bool = ...,
4354 ) -> tuple[Index, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]: ...
4355
4356 @overload
4357 def join(
4358 self,
4359 other: Index,
4360 *,
4361 how: JoinHow = ...,
4362 level: Level = ...,
4363 return_indexers: Literal[False] = ...,
4364 sort: bool = ...,
4365 ) -> Index: ...
4366
4367 @overload
4368 def join(
4369 self,
4370 other: Index,
4371 *,
4372 how: JoinHow = ...,
4373 level: Level = ...,
4374 return_indexers: bool = ...,
4375 sort: bool = ...,
4376 ) -> (
4377 Index | tuple[Index, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]
4378 ): ...
4379
4380 @final
4381 @_maybe_return_indexers
4382 def join(
4383 self,
4384 other: Index,
4385 *,
4386 how: JoinHow = "left",
4387 level: Level | None = None,
4388 return_indexers: bool = False,
4389 sort: bool = False,
4390 ) -> Index | tuple[Index, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
4391 """
4392 Compute join_index and indexers to conform data structures to the new index.
4393
4394 Parameters
4395 ----------
4396 other : Index
4397 The other index on which join is performed.
4398 how : {'left', 'right', 'inner', 'outer'}
4399 level : int or level name, default None
4400 It is either the integer position or the name of the level.
4401 return_indexers : bool, default False
4402 Whether to return the indexers or not for both the index objects.
4403 sort : bool, default False
4404 Sort the join keys lexicographically in the result Index. If False,
4405 the order of the join keys depends on the join type (how keyword).
4406
4407 Returns
4408 -------
4409 join_index, (left_indexer, right_indexer)
4410 The new index.
4411
4412 See Also
4413 --------
4414 DataFrame.join : Join columns with `other` DataFrame either on index
4415 or on a key.
4416 DataFrame.merge : Merge DataFrame or named Series objects with a
4417 database-style join.
4418
4419 Examples
4420 --------
4421 >>> idx1 = pd.Index([1, 2, 3])
4422 >>> idx2 = pd.Index([4, 5, 6])
4423 >>> idx1.join(idx2, how="outer")
4424 Index([1, 2, 3, 4, 5, 6], dtype='int64')
4425 >>> idx1.join(other=idx2, how="outer", return_indexers=True)
4426 (Index([1, 2, 3, 4, 5, 6], dtype='int64'),
4427 array([ 0, 1, 2, -1, -1, -1]), array([-1, -1, -1, 0, 1, 2]))
4428 """
4429 if not isinstance(other, Index):
4430 warnings.warn(
4431 f"Passing {type(other).__name__} to {type(self).__name__}.join "
4432 "is deprecated and will raise in a future version. "
4433 "Pass an Index instead.",
4434 Pandas4Warning,
4435 stacklevel=find_stack_level(),
4436 )
4437
4438 other = ensure_index(other)
4439 sort = sort or how == "outer"
4440
4441 if isinstance(self, ABCDatetimeIndex) and isinstance(other, ABCDatetimeIndex):
4442 if (self.tz is None) ^ (other.tz is None):
4443 # Raise instead of casting to object below.
4444 raise TypeError("Cannot join tz-naive with tz-aware DatetimeIndex")
4445
4446 if not self._is_multi and not other._is_multi:
4447 # We have specific handling for MultiIndex below
4448 pself, pother = self._maybe_downcast_for_indexing(other)
4449 if pself is not self or pother is not other:
4450 return pself.join(
4451 pother, how=how, level=level, return_indexers=True, sort=sort
4452 )
4453
4454 # try to figure out the join level
4455 # GH3662
4456 if level is None and (self._is_multi or other._is_multi):
4457 # have the same levels/names so a simple join
4458 if self.names == other.names:
4459 pass
4460 else:
4461 return self._join_multi(other, how=how)
4462
4463 # join on the level
4464 if level is not None and (self._is_multi or other._is_multi):
4465 return self._join_level(other, level, how=how)
4466
4467 if len(self) == 0 or len(other) == 0:
4468 try:
4469 return self._join_empty(other, how, sort)
4470 except TypeError:
4471 # object dtype; non-comparable objects
4472 pass
4473
4474 if self.dtype != other.dtype:
4475 dtype = self._find_common_type_compat(other)
4476 this = self.astype(dtype, copy=False)
4477 other = other.astype(dtype, copy=False)
4478 return this.join(other, how=how, return_indexers=True)
4479 elif (
4480 isinstance(self, ABCCategoricalIndex)
4481 and isinstance(other, ABCCategoricalIndex)
4482 and not self.ordered
4483 and not self.categories.equals(other.categories)
4484 ):
4485 # dtypes are "equal" but categories are in different order
4486 other = Index(other._values.reorder_categories(self.categories), copy=False)
4487
4488 _validate_join_method(how)
4489
4490 if (
4491 self.is_monotonic_increasing
4492 and other.is_monotonic_increasing
4493 and self._can_use_libjoin
4494 and other._can_use_libjoin
4495 and (self.is_unique or other.is_unique)
4496 ):
4497 try:
4498 return self._join_monotonic(other, how=how)
4499 except TypeError:
4500 # object dtype; non-comparable objects
4501 pass
4502 elif not self.is_unique or not other.is_unique:
4503 return self._join_non_unique(other, how=how, sort=sort)
4504
4505 return self._join_via_get_indexer(other, how, sort)
4506
4507 def _join_empty(
4508 self, other: Index, how: JoinHow, sort: bool
4509 ) -> tuple[Index, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
4510 assert len(self) == 0 or len(other) == 0
4511 _validate_join_method(how)
4512
4513 lidx: np.ndarray | None
4514 ridx: np.ndarray | None
4515
4516 if len(other):
4517 how = cast(JoinHow, {"left": "right", "right": "left"}.get(how, how))
4518 join_index, ridx, lidx = other._join_empty(self, how, sort)
4519 elif how in ["left", "outer"]:
4520 if sort and not self.is_monotonic_increasing:
4521 lidx = self.argsort()
4522 join_index = self.take(lidx)
4523 else:
4524 lidx = None
4525 join_index = self._view()
4526 ridx = np.broadcast_to(np.intp(-1), len(join_index))
4527 else:
4528 join_index = other._view()
4529 lidx = np.array([], dtype=np.intp)
4530 ridx = None
4531 return join_index, lidx, ridx
4532
4533 @final
4534 def _join_via_get_indexer(
4535 self, other: Index, how: JoinHow, sort: bool
4536 ) -> tuple[Index, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
4537 # Fallback if we do not have any fastpaths available based on
4538 # uniqueness/monotonicity
4539
4540 # Note: at this point we have checked matching dtypes
4541 lindexer: npt.NDArray[np.intp] | None
4542 rindexer: npt.NDArray[np.intp] | None
4543
4544 if how == "left":
4545 if sort:
4546 join_index, lindexer = self.sort_values(return_indexer=True)
4547 rindexer = other.get_indexer_for(join_index)
4548 return join_index, lindexer, rindexer
4549 else:
4550 join_index = self
4551 elif how == "right":
4552 if sort:
4553 join_index, rindexer = other.sort_values(return_indexer=True)
4554 lindexer = self.get_indexer_for(join_index)
4555 return join_index, lindexer, rindexer
4556 else:
4557 join_index = other
4558 elif how == "inner":
4559 join_index = self.intersection(other, sort=sort)
4560 elif how == "outer":
4561 try:
4562 join_index = self.union(other, sort=sort)
4563 except TypeError:
4564 join_index = self.union(other)
4565 try:
4566 join_index = _maybe_try_sort(join_index, sort)
4567 except TypeError:
4568 pass
4569
4570 names = other.names if how == "right" else self.names
4571 if join_index.names != names:
4572 join_index = join_index.set_names(names)
4573
4574 if join_index is self:
4575 lindexer = None
4576 else:
4577 lindexer = self.get_indexer_for(join_index)
4578 if join_index is other:
4579 rindexer = None
4580 else:
4581 rindexer = other.get_indexer_for(join_index)
4582 return join_index, lindexer, rindexer
4583
4584 @final
4585 def _join_multi(self, other: Index, how: JoinHow):
4586 from pandas.core.indexes.multi import MultiIndex
4587 from pandas.core.reshape.merge import restore_dropped_levels_multijoin
4588
4589 # figure out join names
4590 self_names_list = list(self.names)
4591 other_names_list = list(other.names)
4592 self_names_order = self_names_list.index
4593 other_names_order = other_names_list.index
4594 self_names = set(self_names_list)
4595 other_names = set(other_names_list)
4596 overlap = self_names & other_names
4597
4598 # need at least 1 in common
4599 if not overlap:
4600 raise ValueError("cannot join with no overlapping index names")
4601
4602 if isinstance(self, MultiIndex) and isinstance(other, MultiIndex):
4603 # Drop the non-matching levels from left and right respectively
4604 ldrop_names = sorted(self_names - overlap, key=self_names_order)
4605 rdrop_names = sorted(other_names - overlap, key=other_names_order)
4606
4607 # if only the order differs
4608 if not len(ldrop_names + rdrop_names):
4609 self_jnlevels = self
4610 other_jnlevels = other.reorder_levels(self.names)
4611 else:
4612 self_jnlevels = self.droplevel(ldrop_names)
4613 other_jnlevels = other.droplevel(rdrop_names)
4614
4615 # Join left and right
4616 # Join on same leveled multi-index frames is supported
4617 join_idx, lidx, ridx = self_jnlevels.join(
4618 other_jnlevels, how=how, return_indexers=True
4619 )
4620
4621 # Restore the dropped levels
4622 # Returned index level order is
4623 # common levels, ldrop_names, rdrop_names
4624 dropped_names = ldrop_names + rdrop_names
4625
4626 # error: Argument 5/6 to "restore_dropped_levels_multijoin" has
4627 # incompatible type "Optional[ndarray[Any, dtype[signedinteger[Any
4628 # ]]]]"; expected "ndarray[Any, dtype[signedinteger[Any]]]"
4629 levels, codes, names = restore_dropped_levels_multijoin(
4630 self,
4631 other,
4632 dropped_names,
4633 join_idx,
4634 lidx, # type: ignore[arg-type]
4635 ridx, # type: ignore[arg-type]
4636 )
4637
4638 # Re-create the multi-index
4639 multi_join_idx = MultiIndex(
4640 levels=levels, codes=codes, names=names, verify_integrity=False
4641 )
4642
4643 multi_join_idx = multi_join_idx.remove_unused_levels()
4644
4645 # maintain the order of the index levels
4646 if how == "right":
4647 level_order = other_names_list + ldrop_names
4648 else:
4649 level_order = self_names_list + rdrop_names
4650 multi_join_idx = multi_join_idx.reorder_levels(level_order)
4651
4652 return multi_join_idx, lidx, ridx
4653
4654 jl = next(iter(overlap))
4655
4656 # Case where only one index is multi
4657 # make the indices into mi's that match
4658 flip_order = False
4659 if isinstance(self, MultiIndex):
4660 self, other = other, self
4661 flip_order = True
4662 # flip if join method is right or left
4663 flip: dict[JoinHow, JoinHow] = {"right": "left", "left": "right"}
4664 how = flip.get(how, how)
4665
4666 level = other.names.index(jl)
4667 result = self._join_level(other, level, how=how)
4668
4669 if flip_order:
4670 return result[0], result[2], result[1]
4671 return result
4672
4673 @final
4674 def _join_non_unique(
4675 self, other: Index, how: JoinHow = "left", sort: bool = False
4676 ) -> tuple[Index, npt.NDArray[np.intp], npt.NDArray[np.intp]]:
4677 from pandas.core.reshape.merge import get_join_indexers_non_unique
4678
4679 # We only get here if dtypes match
4680 assert self.dtype == other.dtype
4681
4682 left_idx, right_idx = get_join_indexers_non_unique(
4683 self._values, other._values, how=how, sort=sort
4684 )
4685
4686 if how == "right":
4687 join_index = other.take(right_idx)
4688 else:
4689 join_index = self.take(left_idx)
4690
4691 if how == "outer":
4692 mask = left_idx == -1
4693 if mask.any():
4694 right = other.take(right_idx)
4695 join_index = join_index.putmask(mask, right)
4696
4697 if isinstance(join_index, ABCMultiIndex) and how == "outer":
4698 # test_join_index_levels
4699 join_index = join_index._sort_levels_monotonic()
4700 return join_index, left_idx, right_idx
4701
4702 @final
4703 def _join_level(
4704 self, other: Index, level, how: JoinHow = "left", keep_order: bool = True
4705 ) -> tuple[MultiIndex, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
4706 """
4707 The join method *only* affects the level of the resulting
4708 MultiIndex. Otherwise it just exactly aligns the Index data to the
4709 labels of the level in the MultiIndex.
4710
4711 If ```keep_order == True```, the order of the data indexed by the
4712 MultiIndex will not be changed; otherwise, it will tie out
4713 with `other`.
4714 """
4715 from pandas.core.indexes.multi import MultiIndex
4716
4717 def _get_leaf_sorter(labels: list[np.ndarray]) -> npt.NDArray[np.intp]:
4718 """
4719 Returns sorter for the inner most level while preserving the
4720 order of higher levels.
4721
4722 Parameters
4723 ----------
4724 labels : list[np.ndarray]
4725 Each ndarray has signed integer dtype, not necessarily identical.
4726
4727 Returns
4728 -------
4729 np.ndarray[np.intp]
4730 """
4731 if labels[0].size == 0:
4732 return np.empty(0, dtype=np.intp)
4733
4734 if len(labels) == 1:
4735 return get_group_index_sorter(ensure_platform_int(labels[0]))
4736
4737 # find indexers of beginning of each set of
4738 # same-key labels w.r.t all but last level
4739 tic = labels[0][:-1] != labels[0][1:]
4740 for lab in labels[1:-1]:
4741 tic |= lab[:-1] != lab[1:]
4742
4743 starts = np.hstack(([True], tic, [True])).nonzero()[0]
4744 lab = ensure_int64(labels[-1])
4745 return lib.get_level_sorter(lab, ensure_platform_int(starts))
4746
4747 if isinstance(self, MultiIndex) and isinstance(other, MultiIndex):
4748 raise TypeError("Join on level between two MultiIndex objects is ambiguous")
4749
4750 left, right = self, other
4751
4752 flip_order = not isinstance(self, MultiIndex)
4753 if flip_order:
4754 left, right = right, left
4755 flip: dict[JoinHow, JoinHow] = {"right": "left", "left": "right"}
4756 how = flip.get(how, how)
4757
4758 assert isinstance(left, MultiIndex)
4759
4760 level = left._get_level_number(level)
4761 old_level = left.levels[level]
4762
4763 if not right.is_unique:
4764 raise NotImplementedError(
4765 "Index._join_level on non-unique index is not implemented"
4766 )
4767
4768 new_level, left_lev_indexer, right_lev_indexer = old_level.join(
4769 right, how=how, return_indexers=True
4770 )
4771
4772 if left_lev_indexer is None:
4773 if keep_order or len(left) == 0:
4774 left_indexer = None
4775 join_index = left
4776 else: # sort the leaves
4777 left_indexer = _get_leaf_sorter(left.codes[: level + 1])
4778 join_index = left[left_indexer]
4779
4780 else:
4781 left_lev_indexer = ensure_platform_int(left_lev_indexer)
4782 rev_indexer = lib.get_reverse_indexer(left_lev_indexer, len(old_level))
4783 old_codes = left.codes[level]
4784
4785 taker = old_codes[old_codes != -1]
4786 new_lev_codes = rev_indexer.take(taker)
4787
4788 new_codes = list(left.codes)
4789 new_codes[level] = new_lev_codes
4790
4791 new_levels = list(left.levels)
4792 new_levels[level] = new_level
4793
4794 if keep_order: # just drop missing values. o.w. keep order
4795 left_indexer = np.arange(len(left), dtype=np.intp)
4796 left_indexer = cast(np.ndarray, left_indexer)
4797 mask = new_lev_codes != -1
4798 if not mask.all():
4799 new_codes = [lab[mask] for lab in new_codes]
4800 left_indexer = left_indexer[mask]
4801
4802 elif level == 0: # outer most level, take the fast route
4803 max_new_lev = 0 if len(new_lev_codes) == 0 else new_lev_codes.max()
4804 ngroups = 1 + max_new_lev
4805 left_indexer, counts = libalgos.groupsort_indexer(
4806 new_lev_codes, ngroups
4807 )
4808
4809 # missing values are placed first; drop them!
4810 left_indexer = left_indexer[counts[0] :]
4811 new_codes = [lab[left_indexer] for lab in new_codes]
4812
4813 else: # sort the leaves
4814 mask = new_lev_codes != -1
4815 mask_all = mask.all()
4816 if not mask_all:
4817 new_codes = [lab[mask] for lab in new_codes]
4818
4819 left_indexer = _get_leaf_sorter(new_codes[: level + 1])
4820 new_codes = [lab[left_indexer] for lab in new_codes]
4821
4822 # left_indexers are w.r.t masked frame.
4823 # reverse to original frame!
4824 if not mask_all:
4825 left_indexer = mask.nonzero()[0][left_indexer]
4826
4827 join_index = MultiIndex(
4828 levels=new_levels,
4829 codes=new_codes,
4830 names=left.names,
4831 verify_integrity=False,
4832 )
4833
4834 if right_lev_indexer is not None:
4835 right_indexer = right_lev_indexer.take(join_index.codes[level])
4836 else:
4837 right_indexer = join_index.codes[level]
4838
4839 if flip_order:
4840 left_indexer, right_indexer = right_indexer, left_indexer
4841
4842 left_indexer = (
4843 None if left_indexer is None else ensure_platform_int(left_indexer)
4844 )
4845 right_indexer = (
4846 None if right_indexer is None else ensure_platform_int(right_indexer)
4847 )
4848 return join_index, left_indexer, right_indexer
4849
4850 def _join_monotonic(
4851 self, other: Index, how: JoinHow = "left"
4852 ) -> tuple[Index, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
4853 # We only get here with (caller is responsible for ensuring):
4854 # 1) matching dtypes
4855 # 2) both monotonic increasing
4856 # 3) other.is_unique or self.is_unique
4857 assert other.dtype == self.dtype
4858 assert self._can_use_libjoin and other._can_use_libjoin
4859
4860 if self.equals(other):
4861 # This is a convenient place for this check, but its correctness
4862 # does not depend on monotonicity, so it could go earlier
4863 # in the calling method.
4864 ret_index = other if how == "right" else self
4865 return ret_index, None, None
4866
4867 ridx: npt.NDArray[np.intp] | None
4868 lidx: npt.NDArray[np.intp] | None
4869
4870 if how == "left":
4871 if other.is_unique:
4872 # We can perform much better than the general case
4873 join_index = self
4874 lidx = None
4875 ridx = self._left_indexer_unique(other)
4876 else:
4877 join_array, lidx, ridx = self._left_indexer(other)
4878 join_index, lidx, ridx = self._wrap_join_result(
4879 join_array, other, lidx, ridx, how
4880 )
4881 elif how == "right":
4882 if self.is_unique:
4883 # We can perform much better than the general case
4884 join_index = other
4885 lidx = other._left_indexer_unique(self)
4886 ridx = None
4887 else:
4888 join_array, ridx, lidx = other._left_indexer(self)
4889 join_index, lidx, ridx = self._wrap_join_result(
4890 join_array, other, lidx, ridx, how
4891 )
4892 elif how == "inner":
4893 join_array, lidx, ridx = self._inner_indexer(other)
4894 join_index, lidx, ridx = self._wrap_join_result(
4895 join_array, other, lidx, ridx, how
4896 )
4897 elif how == "outer":
4898 join_array, lidx, ridx = self._outer_indexer(other)
4899 join_index, lidx, ridx = self._wrap_join_result(
4900 join_array, other, lidx, ridx, how
4901 )
4902
4903 lidx = None if lidx is None else ensure_platform_int(lidx)
4904 ridx = None if ridx is None else ensure_platform_int(ridx)
4905 return join_index, lidx, ridx
4906
4907 def _wrap_join_result(
4908 self,
4909 joined: ArrayLike,
4910 other: Self,
4911 lidx: npt.NDArray[np.intp] | None,
4912 ridx: npt.NDArray[np.intp] | None,
4913 how: JoinHow,
4914 ) -> tuple[Self, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
4915 assert other.dtype == self.dtype
4916
4917 if lidx is not None and lib.is_range_indexer(lidx, len(self)):
4918 lidx = None
4919 if ridx is not None and lib.is_range_indexer(ridx, len(other)):
4920 ridx = None
4921
4922 # return self or other if possible to maintain cached attributes
4923 if lidx is None:
4924 join_index = self
4925 elif ridx is None:
4926 join_index = other
4927 else:
4928 join_index = self._constructor._with_infer(
4929 joined, dtype=self.dtype, copy=False
4930 )
4931
4932 names = other.names if how == "right" else self.names
4933 if join_index.names != names:
4934 join_index = join_index.set_names(names)
4935
4936 return join_index, lidx, ridx
4937
4938 @final
4939 @cache_readonly
4940 def _can_use_libjoin(self) -> bool:
4941 """
4942 Whether we can use the fastpaths implemented in _libs.join.
4943
4944 This is driven by whether (in monotonic increasing cases that are
4945 guaranteed not to have NAs) we can convert to an np.ndarray without
4946 making a copy. If we cannot, this negates the performance benefit
4947 of using libjoin.
4948 """
4949 if not self.is_monotonic_increasing:
4950 # The libjoin functions all assume monotonicity.
4951 return False
4952
4953 if type(self) is Index:
4954 # excludes EAs, but include masks, we get here with monotonic
4955 # values only, meaning no NA
4956 return (
4957 isinstance(self.dtype, np.dtype)
4958 or isinstance(self._values, (ArrowExtensionArray, BaseMaskedArray))
4959 or (
4960 isinstance(self.dtype, StringDtype)
4961 and self.dtype.storage == "python"
4962 )
4963 )
4964 # Exclude index types where the conversion to numpy converts to object dtype,
4965 # which negates the performance benefit of libjoin
4966 # Subclasses should override to return False if _get_join_target is
4967 # not zero-copy.
4968 # TODO: exclude RangeIndex (which allocates memory)?
4969 # Doing so seems to break test_concat_datetime_timezone
4970 return not isinstance(self, (ABCIntervalIndex, ABCMultiIndex))
4971
4972 # --------------------------------------------------------------------
4973 # Uncategorized Methods
4974
4975 @property
4976 def values(self) -> ArrayLike:
4977 """
4978 Return an array representing the data in the Index.
4979
4980 .. warning::
4981
4982 We recommend using :attr:`Index.array` or
4983 :meth:`Index.to_numpy`, depending on whether you need
4984 a reference to the underlying data or a NumPy array.
4985
4986 .. versionchanged:: 3.0.0
4987
4988 The returned array is read-only.
4989
4990 Returns
4991 -------
4992 array: numpy.ndarray or ExtensionArray
4993
4994 See Also
4995 --------
4996 Index.array : Reference to the underlying data.
4997 Index.to_numpy : A NumPy array representing the underlying data.
4998
4999 Examples
5000 --------
5001 For :class:`pandas.Index`:
5002
5003 >>> idx = pd.Index([1, 2, 3])
5004 >>> idx
5005 Index([1, 2, 3], dtype='int64')
5006 >>> idx.values
5007 array([1, 2, 3])
5008
5009 For :class:`pandas.IntervalIndex`:
5010
5011 >>> idx = pd.interval_range(start=0, end=5)
5012 >>> idx.values
5013 <IntervalArray>
5014 [(0, 1], (1, 2], (2, 3], (3, 4], (4, 5]]
5015 Length: 5, dtype: interval[int64, right]
5016 """
5017 data = self._data
5018 if isinstance(data, np.ndarray):
5019 data = data.view()
5020 data.flags.writeable = False
5021 return data
5022
5023 @cache_readonly
5024 def array(self) -> ExtensionArray:
5025 """
5026 The ExtensionArray of the data backing this Index.
5027
5028 This property provides direct access to the underlying array data of
5029 an Index without requiring conversion to a NumPy array. It
5030 returns an ExtensionArray, which is the native storage format for
5031 pandas extension dtypes.
5032
5033 Returns
5034 -------
5035 ExtensionArray
5036 An ExtensionArray of the values stored within. For extension
5037 types, this is the actual array. For NumPy native types, this
5038 is a thin (no copy) wrapper around :class:`numpy.ndarray`.
5039
5040 ``.array`` differs from ``.values``, which may require converting
5041 the data to a different form.
5042
5043 See Also
5044 --------
5045 Index.to_numpy : Similar method that always returns a NumPy array.
5046 Series.to_numpy : Similar method that always returns a NumPy array.
5047
5048 Notes
5049 -----
5050 This table lays out the different array types for each extension
5051 dtype within pandas.
5052
5053 ================== =============================
5054 dtype array type
5055 ================== =============================
5056 category Categorical
5057 period PeriodArray
5058 interval IntervalArray
5059 IntegerNA IntegerArray
5060 string StringArray
5061 boolean BooleanArray
5062 datetime64[ns, tz] DatetimeArray
5063 ================== =============================
5064
5065 For any 3rd-party extension types, the array type will be an
5066 ExtensionArray.
5067
5068 For all remaining dtypes ``.array`` will be a
5069 :class:`arrays.NumpyExtensionArray` wrapping the actual ndarray
5070 stored within. If you absolutely need a NumPy array (possibly with
5071 copying / coercing data), then use :meth:`Series.to_numpy` instead.
5072
5073 Examples
5074 --------
5075 For regular NumPy types like int, and float, a NumpyExtensionArray
5076 is returned.
5077
5078 >>> pd.Index([1, 2, 3]).array
5079 <NumpyExtensionArray>
5080 [1, 2, 3]
5081 Length: 3, dtype: int64
5082
5083 For extension types, like Categorical, the actual ExtensionArray
5084 is returned
5085
5086 >>> idx = pd.Index(pd.Categorical(["a", "b", "a"]))
5087 >>> idx.array
5088 ['a', 'b', 'a']
5089 Categories (2, str): ['a', 'b']
5090 """
5091 array = self._data
5092 if isinstance(array, np.ndarray):
5093 from pandas.core.arrays.numpy_ import NumpyExtensionArray
5094
5095 array = NumpyExtensionArray(array)
5096 # TODO decide on read-only https://github.com/pandas-dev/pandas/issues/63099
5097 # array = array.view()
5098 # array._readonly = True
5099 return array
5100
5101 @property
5102 def _values(self) -> ExtensionArray | np.ndarray:
5103 """
5104 The best array representation.
5105
5106 This is an ndarray or ExtensionArray.
5107
5108 ``_values`` are consistent between ``Series`` and ``Index``.
5109
5110 It may differ from the public '.values' method.
5111
5112 index | values | _values |
5113 ----------------- | --------------- | ------------- |
5114 Index | ndarray | ndarray |
5115 CategoricalIndex | Categorical | Categorical |
5116 DatetimeIndex | ndarray[M8ns] | DatetimeArray |
5117 DatetimeIndex[tz] | ndarray[M8ns] | DatetimeArray |
5118 PeriodIndex | ndarray[object] | PeriodArray |
5119 IntervalIndex | IntervalArray | IntervalArray |
5120
5121 See Also
5122 --------
5123 values : Values
5124 """
5125 return self._data
5126
5127 def _get_engine_target(self) -> ArrayLike:
5128 """
5129 Get the ndarray or ExtensionArray that we can pass to the IndexEngine
5130 constructor.
5131 """
5132 vals = self._values
5133 if isinstance(vals, StringArray):
5134 # GH#45652 much more performant than ExtensionEngine
5135 return vals._ndarray
5136 if isinstance(vals, ArrowExtensionArray) and self.dtype.kind in "Mm":
5137 import pyarrow as pa
5138
5139 pa_type = vals._pa_array.type
5140 if pa.types.is_timestamp(pa_type):
5141 vals = vals._to_datetimearray()
5142 return vals._ndarray.view("i8")
5143 elif pa.types.is_duration(pa_type):
5144 vals = vals._to_timedeltaarray()
5145 return vals._ndarray.view("i8")
5146 if (
5147 type(self) is Index
5148 and isinstance(self._values, ExtensionArray)
5149 and not isinstance(self._values, BaseMaskedArray)
5150 and not (
5151 isinstance(self._values, ArrowExtensionArray)
5152 and is_numeric_dtype(self.dtype)
5153 # Exclude decimal
5154 and self.dtype.kind != "O"
5155 )
5156 ):
5157 # TODO(ExtensionIndex): remove special-case, just use self._values
5158 return self._values.astype(object)
5159 return vals
5160
5161 @final
5162 def _get_join_target(self) -> np.ndarray:
5163 """
5164 Get the ndarray or ExtensionArray that we can pass to the join
5165 functions.
5166 """
5167 if isinstance(self._values, BaseMaskedArray):
5168 # This is only used if our array is monotonic, so no NAs present
5169 return self._values._data
5170 elif (
5171 isinstance(self._values, ArrowExtensionArray)
5172 and self.dtype.kind not in "mM"
5173 ):
5174 # This is only used if our array is monotonic, so no missing values
5175 # present
5176 # "mM" cases will go through _get_engine_target and cast to i8
5177 return self._values.to_numpy()
5178
5179 # TODO: exclude ABCRangeIndex case here as it copies
5180 target = self._get_engine_target()
5181 if not isinstance(target, np.ndarray):
5182 raise ValueError("_can_use_libjoin should return False.")
5183 return target
5184
5185 def _from_join_target(self, result: np.ndarray) -> ArrayLike:
5186 """
5187 Cast the ndarray returned from one of the libjoin.foo_indexer functions
5188 back to type(self._data).
5189 """
5190 if isinstance(self.values, BaseMaskedArray):
5191 return type(self.values)(result, np.zeros(result.shape, dtype=np.bool_))
5192 elif isinstance(self.values, (ArrowExtensionArray, StringArray)):
5193 return type(self.values)._from_sequence(result, dtype=self.dtype)
5194 return result
5195
5196 def memory_usage(self, deep: bool = False) -> int:
5197 """
5198 Memory usage of the values.
5199
5200 Parameters
5201 ----------
5202 deep : bool, default False
5203 Introspect the data deeply, interrogate
5204 `object` dtypes for system-level memory consumption.
5205
5206 Returns
5207 -------
5208 bytes used
5209 Returns memory usage of the values in the Index in bytes.
5210
5211 See Also
5212 --------
5213 numpy.ndarray.nbytes : Total bytes consumed by the elements of the
5214 array.
5215
5216 Notes
5217 -----
5218 Memory usage does not include memory consumed by elements that
5219 are not components of the array if deep=False or if used on PyPy
5220
5221 Examples
5222 --------
5223 >>> idx = pd.Index([1, 2, 3])
5224 >>> idx.memory_usage()
5225 24
5226 """
5227 result = self._memory_usage(deep=deep)
5228
5229 # include our engine hashtable, only if it's already cached
5230 if "_engine" in self._cache:
5231 result += self._engine.sizeof(deep=deep)
5232 return result
5233
5234 @final
5235 def where(self, cond, other=None) -> Index:
5236 """
5237 Replace values where the condition is False.
5238
5239 The replacement is taken from other.
5240
5241 Parameters
5242 ----------
5243 cond : bool array-like with the same length as self
5244 Condition to select the values on.
5245 other : scalar, or array-like, default None
5246 Replacement if the condition is False.
5247
5248 Returns
5249 -------
5250 pandas.Index
5251 A copy of self with values replaced from other
5252 where the condition is False.
5253
5254 See Also
5255 --------
5256 Series.where : Same method for Series.
5257 DataFrame.where : Same method for DataFrame.
5258
5259 Examples
5260 --------
5261 >>> idx = pd.Index(["car", "bike", "train", "tractor"])
5262 >>> idx
5263 Index(['car', 'bike', 'train', 'tractor'], dtype='str')
5264 >>> idx.where(idx.isin(["car", "train"]), "other")
5265 Index(['car', 'other', 'train', 'other'], dtype='str')
5266 """
5267 if isinstance(self, ABCMultiIndex):
5268 raise NotImplementedError(
5269 ".where is not supported for MultiIndex operations"
5270 )
5271 cond = np.asarray(cond, dtype=bool)
5272 return self.putmask(~cond, other)
5273
5274 # construction helpers
5275 @final
5276 @classmethod
5277 def _raise_scalar_data_error(cls, data):
5278 # We return the TypeError so that we can raise it from the constructor
5279 # in order to keep mypy happy
5280 raise TypeError(
5281 f"{cls.__name__}(...) must be called with a collection of some "
5282 f"kind, {repr(data) if not isinstance(data, np.generic) else str(data)} "
5283 "was passed"
5284 )
5285
5286 @classmethod
5287 def _maybe_copy_array_input(
5288 cls, data, copy: bool | None, dtype
5289 ) -> tuple[Any, bool]:
5290 """
5291 Ensure that the input data is copied if necessary.
5292 GH#63388
5293 """
5294 if isinstance(data, (ExtensionArray, np.ndarray)):
5295 if copy is not False:
5296 if dtype is None or astype_is_view(data.dtype, pandas_dtype(dtype)):
5297 data = data.copy()
5298 copy = False
5299 return data, bool(copy)
5300
5301 def _validate_fill_value(self, value):
5302 """
5303 Check if the value can be inserted into our array without casting,
5304 and convert it to an appropriate native type if necessary.
5305
5306 Raises
5307 ------
5308 TypeError
5309 If the value cannot be inserted into an array of this dtype.
5310 """
5311 dtype = self.dtype
5312 if isinstance(dtype, np.dtype) and dtype.kind not in "mM":
5313 if isinstance(value, tuple) and dtype != object:
5314 # GH#54385
5315 raise TypeError
5316 try:
5317 return np_can_hold_element(dtype, value)
5318 except LossySetitemError as err:
5319 # re-raise as TypeError for consistency
5320 raise TypeError from err
5321 elif not can_hold_element(self._values, value):
5322 raise TypeError
5323 return value
5324
5325 @cache_readonly
5326 def _is_memory_usage_qualified(self) -> bool:
5327 """
5328 Return a boolean if we need a qualified .info display.
5329 """
5330 return is_object_dtype(self.dtype) or (
5331 is_string_dtype(self.dtype) and self.dtype.storage == "python" # type: ignore[union-attr]
5332 )
5333
5334 def __contains__(self, key: Any) -> bool:
5335 """
5336 Return a boolean indicating whether the provided key is in the index.
5337
5338 Parameters
5339 ----------
5340 key : label
5341 The key to check if it is present in the index.
5342
5343 Returns
5344 -------
5345 bool
5346 Whether the key search is in the index.
5347
5348 Raises
5349 ------
5350 TypeError
5351 If the key is not hashable.
5352
5353 See Also
5354 --------
5355 Index.isin : Returns an ndarray of boolean dtype indicating whether the
5356 list-like key is in the index.
5357
5358 Examples
5359 --------
5360 >>> idx = pd.Index([1, 2, 3, 4])
5361 >>> idx
5362 Index([1, 2, 3, 4], dtype='int64')
5363
5364 >>> 2 in idx
5365 True
5366 >>> 6 in idx
5367 False
5368 """
5369 hash(key)
5370 try:
5371 return key in self._engine
5372 except (OverflowError, TypeError, ValueError):
5373 return False
5374
5375 # https://github.com/python/typeshed/issues/2148#issuecomment-520783318
5376 # Incompatible types in assignment (expression has type "None", base class
5377 # "object" defined the type as "Callable[[object], int]")
5378 __hash__: ClassVar[None] # type: ignore[assignment]
5379
5380 @final
5381 def __setitem__(self, key, value) -> None:
5382 raise TypeError("Index does not support mutable operations")
5383
5384 def __getitem__(self, key):
5385 """
5386 Override numpy.ndarray's __getitem__ method to work as desired.
5387
5388 This function adds lists and Series as valid boolean indexers
5389 (ndarrays only supports ndarray with dtype=bool).
5390
5391 If resulting ndim != 1, plain ndarray is returned instead of
5392 corresponding `Index` subclass.
5393
5394 """
5395 getitem = self._data.__getitem__
5396
5397 key = lib.item_from_zerodim(key)
5398 if is_integer(key) or is_float(key):
5399 # GH#44051 exclude bool, which would return a 2d ndarray
5400 key = com.cast_scalar_indexer(key)
5401 return getitem(key)
5402
5403 if isinstance(key, slice):
5404 # This case is separated from the conditional above to avoid
5405 # pessimization com.is_bool_indexer and ndim checks.
5406 return self._getitem_slice(key)
5407
5408 if com.is_bool_indexer(key):
5409 # if we have list[bools, length=1e5] then doing this check+convert
5410 # takes 166 µs + 2.1 ms and cuts the ndarray.__getitem__
5411 # time below from 3.8 ms to 496 µs
5412 # if we already have ndarray[bool], the overhead is 1.4 µs or .25%
5413 if isinstance(getattr(key, "dtype", None), ExtensionDtype):
5414 key = key.to_numpy(dtype=bool, na_value=False)
5415 else:
5416 key = np.asarray(key, dtype=bool)
5417
5418 if not isinstance(self.dtype, ExtensionDtype):
5419 if len(key) == 0 and len(key) != len(self):
5420 raise ValueError(
5421 "The length of the boolean indexer cannot be 0 "
5422 "when the Index has length greater than 0."
5423 )
5424
5425 result = getitem(key)
5426 # Because we ruled out integer above, we always get an arraylike here
5427 if result.ndim > 1:
5428 disallow_ndim_indexing(result)
5429
5430 # NB: Using _constructor._simple_new would break if MultiIndex
5431 # didn't override __getitem__
5432 return self._constructor._simple_new(result, name=self._name)
5433
5434 def _getitem_slice(self, slobj: slice) -> Self:
5435 """
5436 Fastpath for __getitem__ when we know we have a slice.
5437 """
5438 res = self._data[slobj]
5439 result = type(self)._simple_new(res, name=self._name, refs=self._references)
5440 if "_engine" in self._cache:
5441 reverse = slobj.step is not None and slobj.step < 0
5442 result._engine._update_from_sliced(self._engine, reverse=reverse) # type: ignore[union-attr]
5443
5444 return result
5445
5446 @final
5447 def _can_hold_identifiers_and_holds_name(self, name) -> bool:
5448 """
5449 Faster check for ``name in self`` when we know `name` is a Python
5450 identifier (e.g. in NDFrame.__getattr__, which hits this to support
5451 . key lookup). For indexes that can't hold identifiers (everything
5452 but object & categorical) we just return False.
5453
5454 https://github.com/pandas-dev/pandas/issues/19764
5455 """
5456 if (
5457 is_object_dtype(self.dtype)
5458 or is_string_dtype(self.dtype)
5459 or isinstance(self.dtype, CategoricalDtype)
5460 ):
5461 return name in self
5462 return False
5463
5464 def append(self, other: Index | Sequence[Index]) -> Index:
5465 """
5466 Append a collection of Index options together.
5467
5468 Parameters
5469 ----------
5470 other : Index or list/tuple of indices
5471 Single Index or a collection of indices, which can be either a list or a
5472 tuple.
5473
5474 Returns
5475 -------
5476 Index
5477 Returns a new Index object resulting from appending the provided other
5478 indices to the original Index.
5479
5480 See Also
5481 --------
5482 Index.insert : Make new Index inserting new item at location.
5483
5484 Examples
5485 --------
5486 >>> idx = pd.Index([1, 2, 3])
5487 >>> idx.append(pd.Index([4]))
5488 Index([1, 2, 3, 4], dtype='int64')
5489 """
5490 to_concat = [self]
5491
5492 if isinstance(other, (list, tuple)):
5493 to_concat += list(other)
5494 else:
5495 # error: Argument 1 to "append" of "list" has incompatible type
5496 # "Union[Index, Sequence[Index]]"; expected "Index"
5497 to_concat.append(other) # type: ignore[arg-type]
5498
5499 for obj in to_concat:
5500 if not isinstance(obj, Index):
5501 raise TypeError("all inputs must be Index")
5502
5503 names = {obj.name for obj in to_concat}
5504 name = None if len(names) > 1 else self.name
5505
5506 return self._concat(to_concat, name)
5507
5508 def _concat(self, to_concat: list[Index], name: Hashable) -> Index:
5509 """
5510 Concatenate multiple Index objects.
5511 """
5512 to_concat_vals = [x._values for x in to_concat]
5513
5514 result = concat_compat(to_concat_vals)
5515
5516 return Index._with_infer(result, name=name, copy=False)
5517
5518 def putmask(self, mask, value) -> Index:
5519 """
5520 Return a new Index of the values set with the mask.
5521
5522 Parameters
5523 ----------
5524 mask : array-like of bool
5525 Array of booleans denoting where values should be replaced.
5526 value : scalar
5527 Scalar value to use to fill holes (e.g. 0).
5528 This value cannot be a list-likes.
5529
5530 Returns
5531 -------
5532 Index
5533 A new Index of the values set with the mask.
5534
5535 See Also
5536 --------
5537 numpy.putmask : Changes elements of an array
5538 based on conditional and input values.
5539
5540 Examples
5541 --------
5542 >>> idx1 = pd.Index([1, 2, 3])
5543 >>> idx2 = pd.Index([5, 6, 7])
5544 >>> idx1.putmask([True, False, False], idx2)
5545 Index([5, 2, 3], dtype='int64')
5546 """
5547 mask, noop = validate_putmask(self._values, mask)
5548 if noop:
5549 return self.copy()
5550
5551 if self.dtype != object and is_valid_na_for_dtype(value, self.dtype):
5552 # e.g. None -> np.nan, see also Block._standardize_fill_value
5553 value = self._na_value
5554
5555 try:
5556 converted = self._validate_fill_value(value)
5557 except (LossySetitemError, ValueError, TypeError) as err:
5558 if is_object_dtype(self.dtype): # pragma: no cover
5559 raise err
5560
5561 # See also: Block.coerce_to_target_dtype
5562 dtype = self._find_common_type_compat(value)
5563 if dtype == self.dtype:
5564 # GH#56376 avoid RecursionError
5565 raise AssertionError(
5566 "Something has gone wrong. Please report a bug at "
5567 "github.com/pandas-dev/pandas"
5568 ) from err
5569 return self.astype(dtype).putmask(mask, value)
5570
5571 values = self._values.copy()
5572
5573 if isinstance(values, np.ndarray):
5574 converted = setitem_datetimelike_compat(values, mask.sum(), converted)
5575 np.putmask(values, mask, converted)
5576
5577 else:
5578 # Note: we use the original value here, not converted, as
5579 # _validate_fill_value is not idempotent
5580 values._putmask(mask, value)
5581
5582 return self._shallow_copy(values)
5583
5584 def equals(self, other: Any) -> bool:
5585 """
5586 Determine if two Index object are equal.
5587
5588 The things that are being compared are:
5589
5590 * The elements inside the Index object.
5591 * The order of the elements inside the Index object.
5592
5593 Parameters
5594 ----------
5595 other : Any
5596 The other object to compare against.
5597
5598 Returns
5599 -------
5600 bool
5601 True if "other" is an Index and it has the same elements and order
5602 as the calling index; False otherwise.
5603
5604 See Also
5605 --------
5606 Index.identical: Checks that object attributes and types are also equal.
5607 Index.has_duplicates: Check if the Index has duplicate values.
5608 Index.is_unique: Return if the index has unique values.
5609
5610 Examples
5611 --------
5612 >>> idx1 = pd.Index([1, 2, 3])
5613 >>> idx1
5614 Index([1, 2, 3], dtype='int64')
5615 >>> idx1.equals(pd.Index([1, 2, 3]))
5616 True
5617
5618 The elements inside are compared
5619
5620 >>> idx2 = pd.Index(["1", "2", "3"])
5621 >>> idx2
5622 Index(['1', '2', '3'], dtype='str')
5623
5624 >>> idx1.equals(idx2)
5625 False
5626
5627 The order is compared
5628
5629 >>> ascending_idx = pd.Index([1, 2, 3])
5630 >>> ascending_idx
5631 Index([1, 2, 3], dtype='int64')
5632 >>> descending_idx = pd.Index([3, 2, 1])
5633 >>> descending_idx
5634 Index([3, 2, 1], dtype='int64')
5635 >>> ascending_idx.equals(descending_idx)
5636 False
5637
5638 The dtype is *not* compared
5639
5640 >>> int64_idx = pd.Index([1, 2, 3], dtype="int64")
5641 >>> int64_idx
5642 Index([1, 2, 3], dtype='int64')
5643 >>> uint64_idx = pd.Index([1, 2, 3], dtype="uint64")
5644 >>> uint64_idx
5645 Index([1, 2, 3], dtype='uint64')
5646 >>> int64_idx.equals(uint64_idx)
5647 True
5648 """
5649 if self.is_(other):
5650 return True
5651
5652 if not isinstance(other, Index):
5653 return False
5654
5655 if len(self) != len(other):
5656 # quickly return if the lengths are different
5657 return False
5658
5659 if isinstance(self.dtype, StringDtype) and other.dtype != self.dtype:
5660 # TODO(infer_string) can we avoid this special case?
5661 # special case for object behavior
5662 return other.equals(self.astype(object))
5663
5664 if is_object_dtype(self.dtype) and not is_object_dtype(other.dtype):
5665 # if other is not object, use other's logic for coercion
5666 return other.equals(self)
5667
5668 if isinstance(other, ABCMultiIndex):
5669 # d-level MultiIndex can equal d-tuple Index
5670 return other.equals(self)
5671
5672 if isinstance(self._values, ExtensionArray):
5673 # Dispatch to the ExtensionArray's .equals method.
5674 if not isinstance(other, type(self)):
5675 return False
5676
5677 earr = cast(ExtensionArray, self._data)
5678 return earr.equals(other._data)
5679
5680 if isinstance(other.dtype, ExtensionDtype):
5681 # All EA-backed Index subclasses override equals
5682 return other.equals(self)
5683
5684 return array_equivalent(self._values, other._values)
5685
5686 @final
5687 def identical(self, other) -> bool:
5688 """
5689 Similar to equals, but checks that object attributes and types are also equal.
5690
5691 Parameters
5692 ----------
5693 other : Index
5694 The Index object you want to compare with the current Index object.
5695
5696 Returns
5697 -------
5698 bool
5699 If two Index objects have equal elements and same type True,
5700 otherwise False.
5701
5702 See Also
5703 --------
5704 Index.equals: Determine if two Index object are equal.
5705 Index.has_duplicates: Check if the Index has duplicate values.
5706 Index.is_unique: Return if the index has unique values.
5707
5708 Examples
5709 --------
5710 >>> idx1 = pd.Index(["1", "2", "3"])
5711 >>> idx2 = pd.Index(["1", "2", "3"])
5712 >>> idx2.identical(idx1)
5713 True
5714
5715 >>> idx1 = pd.Index(["1", "2", "3"], name="A")
5716 >>> idx2 = pd.Index(["1", "2", "3"], name="B")
5717 >>> idx2.identical(idx1)
5718 False
5719 """
5720 return (
5721 self.equals(other)
5722 and all(
5723 getattr(self, c, None) == getattr(other, c, None)
5724 for c in self._comparables
5725 )
5726 and type(self) == type(other)
5727 and self.dtype == other.dtype
5728 )
5729
5730 @final
5731 def asof(self, label):
5732 """
5733 Return the label from the index, or, if not present, the previous one.
5734
5735 Assuming that the index is sorted, return the passed index label if it
5736 is in the index, or return the previous index label if the passed one
5737 is not in the index.
5738
5739 Parameters
5740 ----------
5741 label : object
5742 The label up to which the method returns the latest index label.
5743
5744 Returns
5745 -------
5746 object
5747 The passed label if it is in the index. The previous label if the
5748 passed label is not in the sorted index or `NaN` if there is no
5749 such label.
5750
5751 See Also
5752 --------
5753 Series.asof : Return the latest value in a Series up to the
5754 passed index.
5755 merge_asof : Perform an asof merge (similar to left join but it
5756 matches on nearest key rather than equal key).
5757 Index.get_loc : An `asof` is a thin wrapper around `get_loc`
5758 with method='pad'.
5759
5760 Examples
5761 --------
5762 `Index.asof` returns the latest index label up to the passed label.
5763
5764 >>> idx = pd.Index(["2013-12-31", "2014-01-02", "2014-01-03"])
5765 >>> idx.asof("2014-01-01")
5766 '2013-12-31'
5767
5768 If the label is in the index, the method returns the passed label.
5769
5770 >>> idx.asof("2014-01-02")
5771 '2014-01-02'
5772
5773 If all of the labels in the index are later than the passed label,
5774 NaN is returned.
5775
5776 >>> idx.asof("1999-01-02")
5777 nan
5778
5779 If the index is not sorted, an error is raised.
5780
5781 >>> idx_not_sorted = pd.Index(["2013-12-31", "2015-01-02", "2014-01-03"])
5782 >>> idx_not_sorted.asof("2013-12-31")
5783 Traceback (most recent call last):
5784 ValueError: index must be monotonic increasing or decreasing
5785 """
5786 self._searchsorted_monotonic(label) # validate sortedness
5787 try:
5788 loc = self.get_loc(label)
5789 except (KeyError, TypeError) as err:
5790 # KeyError -> No exact match, try for padded
5791 # TypeError -> passed e.g. non-hashable, fall through to get
5792 # the tested exception message
5793 indexer = self.get_indexer([label], method="pad")
5794 if indexer.ndim > 1 or indexer.size > 1:
5795 raise TypeError("asof requires scalar valued input") from err
5796 loc = indexer.item()
5797 if loc == -1:
5798 return self._na_value
5799 else:
5800 if isinstance(loc, slice):
5801 return self[loc][-1]
5802
5803 return self[loc]
5804
5805 def asof_locs(
5806 self, where: Index, mask: npt.NDArray[np.bool_]
5807 ) -> npt.NDArray[np.intp]:
5808 """
5809 Return the locations (indices) of labels in the index.
5810
5811 As in the :meth:`pandas.Index.asof`, if the label (a particular entry in
5812 ``where``) is not in the index, the latest index label up to the
5813 passed label is chosen and its index returned.
5814
5815 If all of the labels in the index are later than a label in ``where``,
5816 -1 is returned.
5817
5818 ``mask`` is used to ignore ``NA`` values in the index during calculation.
5819
5820 Parameters
5821 ----------
5822 where : Index
5823 An Index consisting of an array of timestamps.
5824 mask : np.ndarray[bool]
5825 Array of booleans denoting where values in the original
5826 data are not ``NA``.
5827
5828 Returns
5829 -------
5830 np.ndarray[np.intp]
5831 An array of locations (indices) of the labels from the index
5832 which correspond to the return values of :meth:`pandas.Index.asof`
5833 for every element in ``where``.
5834
5835 See Also
5836 --------
5837 Index.asof : Return the label from the index, or, if not present, the
5838 previous one.
5839
5840 Examples
5841 --------
5842 >>> idx = pd.date_range("2023-06-01", periods=3, freq="D")
5843 >>> where = pd.DatetimeIndex(
5844 ... ["2023-05-30 00:12:00", "2023-06-01 00:00:00", "2023-06-02 23:59:59"]
5845 ... )
5846 >>> mask = np.ones(3, dtype=bool)
5847 >>> idx.asof_locs(where, mask)
5848 array([-1, 0, 1])
5849
5850 We can use ``mask`` to ignore certain values in the index during calculation.
5851
5852 >>> mask[1] = False
5853 >>> idx.asof_locs(where, mask)
5854 array([-1, 0, 0])
5855 """
5856 # error: No overload variant of "searchsorted" of "ndarray" matches argument
5857 # types "Union[ExtensionArray, ndarray[Any, Any]]", "str"
5858 # TODO: will be fixed when ExtensionArray.searchsorted() is fixed
5859 locs = self._values[mask].searchsorted(
5860 where._values,
5861 side="right", # type: ignore[call-overload]
5862 )
5863 locs = np.where(locs > 0, locs - 1, 0)
5864
5865 result = np.arange(len(self), dtype=np.intp)[mask].take(locs)
5866
5867 first_value = self._values[mask.argmax()]
5868 result[(locs == 0) & (where._values < first_value)] = -1
5869
5870 return result
5871
5872 @overload
5873 def sort_values(
5874 self,
5875 *,
5876 return_indexer: Literal[False] = ...,
5877 ascending: bool = ...,
5878 na_position: NaPosition = ...,
5879 key: Callable | None = ...,
5880 ) -> Self: ...
5881
5882 @overload
5883 def sort_values(
5884 self,
5885 *,
5886 return_indexer: Literal[True],
5887 ascending: bool = ...,
5888 na_position: NaPosition = ...,
5889 key: Callable | None = ...,
5890 ) -> tuple[Self, np.ndarray]: ...
5891
5892 @overload
5893 def sort_values(
5894 self,
5895 *,
5896 return_indexer: bool = ...,
5897 ascending: bool = ...,
5898 na_position: NaPosition = ...,
5899 key: Callable | None = ...,
5900 ) -> Self | tuple[Self, np.ndarray]: ...
5901
5902 def sort_values(
5903 self,
5904 *,
5905 return_indexer: bool = False,
5906 ascending: bool = True,
5907 na_position: NaPosition = "last",
5908 key: Callable | None = None,
5909 ) -> Self | tuple[Self, np.ndarray]:
5910 """
5911 Return a sorted copy of the index.
5912
5913 Return a sorted copy of the index, and optionally return the indices
5914 that sorted the index itself.
5915
5916 Parameters
5917 ----------
5918 return_indexer : bool, default False
5919 Should the indices that would sort the index be returned.
5920 ascending : bool, default True
5921 Should the index values be sorted in an ascending order.
5922 na_position : {'first' or 'last'}, default 'last'
5923 Argument 'first' puts NaNs at the beginning, 'last' puts NaNs at
5924 the end.
5925 key : callable, optional
5926 If not None, apply the key function to the index values
5927 before sorting. This is similar to the `key` argument in the
5928 builtin :meth:`sorted` function, with the notable difference that
5929 this `key` function should be *vectorized*. It should expect an
5930 ``Index`` and return an ``Index`` of the same shape.
5931
5932 Returns
5933 -------
5934 sorted_index : pandas.Index
5935 Sorted copy of the index.
5936 indexer : numpy.ndarray, optional
5937 The indices that the index itself was sorted by.
5938
5939 See Also
5940 --------
5941 Series.sort_values : Sort values of a Series.
5942 DataFrame.sort_values : Sort values in a DataFrame.
5943
5944 Examples
5945 --------
5946 >>> idx = pd.Index([10, 100, 1, 1000])
5947 >>> idx
5948 Index([10, 100, 1, 1000], dtype='int64')
5949
5950 Sort values in ascending order (default behavior).
5951
5952 >>> idx.sort_values()
5953 Index([1, 10, 100, 1000], dtype='int64')
5954
5955 Sort values in descending order, and also get the indices `idx` was
5956 sorted by.
5957
5958 >>> idx.sort_values(ascending=False, return_indexer=True)
5959 (Index([1000, 100, 10, 1], dtype='int64'), array([3, 1, 0, 2]))
5960 """
5961 if key is None and (
5962 (ascending and self.is_monotonic_increasing)
5963 or (not ascending and self.is_monotonic_decreasing)
5964 ):
5965 if return_indexer:
5966 indexer = np.arange(len(self), dtype=np.intp)
5967 return self.copy(), indexer
5968 else:
5969 return self.copy()
5970
5971 # GH 35584. Sort missing values according to na_position kwarg
5972 # ignore na_position for MultiIndex
5973 if not isinstance(self, ABCMultiIndex):
5974 _as = nargsort(
5975 items=self, ascending=ascending, na_position=na_position, key=key
5976 )
5977 else:
5978 idx = cast(Index, ensure_key_mapped(self, key))
5979 _as = idx.argsort(na_position=na_position)
5980 if not ascending:
5981 _as = _as[::-1]
5982
5983 sorted_index = self.take(_as)
5984
5985 if return_indexer:
5986 return sorted_index, _as
5987 else:
5988 return sorted_index
5989
5990 def shift(self, periods: int = 1, freq=None) -> Self:
5991 """
5992 Shift index by desired number of time frequency increments.
5993
5994 This method is for shifting the values of datetime-like indexes
5995 by a specified time increment a given number of times.
5996
5997 Parameters
5998 ----------
5999 periods : int, default 1
6000 Number of periods (or increments) to shift by,
6001 can be positive or negative.
6002 freq : pandas.DateOffset, pandas.Timedelta or str, optional
6003 Frequency increment to shift by.
6004 If None, the index is shifted by its own `freq` attribute.
6005 Offset aliases are valid strings, e.g., 'D', 'W', 'M' etc.
6006
6007 Returns
6008 -------
6009 pandas.Index
6010 Shifted index.
6011
6012 See Also
6013 --------
6014 Series.shift : Shift values of Series.
6015
6016 Notes
6017 -----
6018 This method is only implemented for datetime-like index classes,
6019 i.e., DatetimeIndex, PeriodIndex and TimedeltaIndex.
6020
6021 Examples
6022 --------
6023 Put the first 5 month starts of 2011 into an index.
6024
6025 >>> month_starts = pd.date_range("1/1/2011", periods=5, freq="MS")
6026 >>> month_starts
6027 DatetimeIndex(['2011-01-01', '2011-02-01', '2011-03-01', '2011-04-01',
6028 '2011-05-01'],
6029 dtype='datetime64[us]', freq='MS')
6030
6031 Shift the index by 10 days.
6032
6033 >>> month_starts.shift(10, freq="D")
6034 DatetimeIndex(['2011-01-11', '2011-02-11', '2011-03-11', '2011-04-11',
6035 '2011-05-11'],
6036 dtype='datetime64[us]', freq=None)
6037
6038 The default value of `freq` is the `freq` attribute of the index,
6039 which is 'MS' (month start) in this example.
6040
6041 >>> month_starts.shift(10)
6042 DatetimeIndex(['2011-11-01', '2011-12-01', '2012-01-01', '2012-02-01',
6043 '2012-03-01'],
6044 dtype='datetime64[us]', freq='MS')
6045 """
6046 raise NotImplementedError(
6047 f"This method is only implemented for DatetimeIndex, PeriodIndex and "
6048 f"TimedeltaIndex; Got type {type(self).__name__}"
6049 )
6050
6051 def argsort(self, *args, **kwargs) -> npt.NDArray[np.intp]:
6052 """
6053 Return the integer indices that would sort the index.
6054
6055 Parameters
6056 ----------
6057 *args
6058 Passed to `numpy.ndarray.argsort`.
6059 **kwargs
6060 Passed to `numpy.ndarray.argsort`.
6061
6062 Returns
6063 -------
6064 np.ndarray[np.intp]
6065 Integer indices that would sort the index if used as
6066 an indexer.
6067
6068 See Also
6069 --------
6070 numpy.argsort : Similar method for NumPy arrays.
6071 Index.sort_values : Return sorted copy of Index.
6072
6073 Examples
6074 --------
6075 >>> idx = pd.Index(["b", "a", "d", "c"])
6076 >>> idx
6077 Index(['b', 'a', 'd', 'c'], dtype='str')
6078
6079 >>> order = idx.argsort()
6080 >>> order
6081 array([1, 0, 3, 2])
6082
6083 >>> idx[order]
6084 Index(['a', 'b', 'c', 'd'], dtype='str')
6085 """
6086 # This works for either ndarray or EA, is overridden
6087 # by RangeIndex, MultIIndex
6088 return self._data.argsort(*args, **kwargs)
6089
6090 def _check_indexing_error(self, key) -> None:
6091 if not is_scalar(key):
6092 # if key is not a scalar, directly raise an error (the code below
6093 # would convert to numpy arrays and raise later any way) - GH29926
6094 raise InvalidIndexError(key)
6095
6096 @cache_readonly
6097 def _should_fallback_to_positional(self) -> bool:
6098 """
6099 Should an integer key be treated as positional?
6100 """
6101 return self.inferred_type not in {
6102 "integer",
6103 "mixed-integer",
6104 "floating",
6105 "complex",
6106 }
6107
6108 _index_shared_docs["get_indexer_non_unique"] = """
6109 Compute indexer and mask for new index given the current index.
6110
6111 The indexer should be then used as an input to ndarray.take to align the
6112 current data to the new index.
6113
6114 Parameters
6115 ----------
6116 target : %(target_klass)s
6117 An iterable containing the values to be used for computing indexer.
6118
6119 Returns
6120 -------
6121 indexer : np.ndarray[np.intp]
6122 Integers from 0 to n - 1 indicating that the index at these
6123 positions matches the corresponding target values. Missing values
6124 in the target are marked by -1.
6125 missing : np.ndarray[np.intp]
6126 An indexer into the target of the values not found.
6127 These correspond to the -1 in the indexer array.
6128
6129 See Also
6130 --------
6131 Index.get_indexer : Computes indexer and mask for new index given
6132 the current index.
6133 Index.get_indexer_for : Returns an indexer even when non-unique.
6134
6135 Examples
6136 --------
6137 >>> index = pd.Index(['c', 'b', 'a', 'b', 'b'])
6138 >>> index.get_indexer_non_unique(['b', 'b'])
6139 (array([1, 3, 4, 1, 3, 4]), array([], dtype=int64))
6140
6141 In the example below there are no matched values.
6142
6143 >>> index = pd.Index(['c', 'b', 'a', 'b', 'b'])
6144 >>> index.get_indexer_non_unique(['q', 'r', 't'])
6145 (array([-1, -1, -1]), array([0, 1, 2]))
6146
6147 For this reason, the returned ``indexer`` contains only integers equal to -1.
6148 It demonstrates that there's no match between the index and the ``target``
6149 values at these positions. The mask [0, 1, 2] in the return value shows that
6150 the first, second, and third elements are missing.
6151
6152 Notice that the return value is a tuple contains two items. In the example
6153 below the first item is an array of locations in ``index``. The second
6154 item is a mask shows that the first and third elements are missing.
6155
6156 >>> index = pd.Index(['c', 'b', 'a', 'b', 'b'])
6157 >>> index.get_indexer_non_unique(['f', 'b', 's'])
6158 (array([-1, 1, 3, 4, -1]), array([0, 2]))
6159 """
6160
6161 def get_indexer_non_unique(
6162 self, target
6163 ) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]:
6164 """
6165 Compute indexer and mask for new index given the current index.
6166
6167 The indexer should be then used as an input to ndarray.take to align the
6168 current data to the new index.
6169
6170 Parameters
6171 ----------
6172 target : Index
6173 An iterable containing the values to be used for computing indexer.
6174
6175 Returns
6176 -------
6177 indexer : np.ndarray[np.intp]
6178 Integers from 0 to n - 1 indicating that the index at these
6179 positions matches the corresponding target values. Missing values
6180 in the target are marked by -1.
6181 missing : np.ndarray[np.intp]
6182 An indexer into the target of the values not found.
6183 These correspond to the -1 in the indexer array.
6184
6185 See Also
6186 --------
6187 Index.get_indexer : Computes indexer and mask for new index given
6188 the current index.
6189 Index.get_indexer_for : Returns an indexer even when non-unique.
6190
6191 Examples
6192 --------
6193 >>> index = pd.Index(["c", "b", "a", "b", "b"])
6194 >>> index.get_indexer_non_unique(["b", "b"])
6195 (array([1, 3, 4, 1, 3, 4]), array([], dtype=int64))
6196
6197 In the example below there are no matched values.
6198
6199 >>> index = pd.Index(["c", "b", "a", "b", "b"])
6200 >>> index.get_indexer_non_unique(["q", "r", "t"])
6201 (array([-1, -1, -1]), array([0, 1, 2]))
6202
6203 For this reason, the returned ``indexer`` contains only integers equal to -1.
6204 It demonstrates that there's no match between the index and the ``target``
6205 values at these positions. The mask [0, 1, 2] in the return value shows that
6206 the first, second, and third elements are missing.
6207
6208 Notice that the return value is a tuple contains two items. In the example
6209 below the first item is an array of locations in ``index``. The second
6210 item is a mask shows that the first and third elements are missing.
6211
6212 >>> index = pd.Index(["c", "b", "a", "b", "b"])
6213 >>> index.get_indexer_non_unique(["f", "b", "s"])
6214 (array([-1, 1, 3, 4, -1]), array([0, 2]))
6215 """
6216 target = self._maybe_cast_listlike_indexer(target)
6217
6218 if not self._should_compare(target) and not self._should_partial_index(target):
6219 # _should_partial_index e.g. IntervalIndex with numeric scalars
6220 # that can be matched to Interval scalars.
6221 return self._get_indexer_non_comparable(target, method=None, unique=False)
6222
6223 pself, ptarget = self._maybe_downcast_for_indexing(target)
6224 if pself is not self or ptarget is not target:
6225 return pself.get_indexer_non_unique(ptarget)
6226
6227 if self.dtype != target.dtype:
6228 # TODO: if object, could use infer_dtype to preempt costly
6229 # conversion if still non-comparable?
6230 dtype = self._find_common_type_compat(target)
6231
6232 this = self.astype(dtype, copy=False)
6233 that = target.astype(dtype, copy=False)
6234 return this.get_indexer_non_unique(that)
6235
6236 # TODO: get_indexer has fastpaths for both Categorical-self and
6237 # Categorical-target. Can we do something similar here?
6238
6239 # Note: _maybe_downcast_for_indexing ensures we never get here
6240 # with MultiIndex self and non-Multi target
6241 if self._is_multi and target._is_multi:
6242 engine = self._engine
6243 # Item "IndexEngine" of "Union[IndexEngine, ExtensionEngine]" has
6244 # no attribute "_extract_level_codes"
6245 tgt_values = engine._extract_level_codes(target) # type: ignore[union-attr]
6246 else:
6247 tgt_values = target._get_engine_target()
6248
6249 indexer, missing = self._engine.get_indexer_non_unique(tgt_values)
6250 return ensure_platform_int(indexer), ensure_platform_int(missing)
6251
6252 @final
6253 def get_indexer_for(self, target) -> npt.NDArray[np.intp]:
6254 """
6255 Guaranteed return of an indexer even when non-unique.
6256
6257 This dispatches to get_indexer or get_indexer_non_unique
6258 as appropriate.
6259
6260 Parameters
6261 ----------
6262 target : Index
6263 An iterable containing the values to be used for computing indexer.
6264
6265 Returns
6266 -------
6267 np.ndarray[np.intp]
6268 List of indices.
6269
6270 See Also
6271 --------
6272 Index.get_indexer : Computes indexer and mask for new index given
6273 the current index.
6274 Index.get_non_unique : Returns indexer and masks for new index given
6275 the current index.
6276
6277 Examples
6278 --------
6279 >>> idx = pd.Index([np.nan, "var1", np.nan])
6280 >>> idx.get_indexer_for([np.nan])
6281 array([0, 2])
6282 """
6283 if self._index_as_unique:
6284 return self.get_indexer(target)
6285 indexer, _ = self.get_indexer_non_unique(target)
6286 return indexer
6287
6288 def _get_indexer_strict(self, key, axis_name: str_t) -> tuple[Index, np.ndarray]:
6289 """
6290 Analogue to get_indexer that raises if any elements are missing.
6291 """
6292 keyarr = key
6293 if not isinstance(keyarr, Index):
6294 keyarr = com.asarray_tuplesafe(keyarr)
6295
6296 if self._index_as_unique:
6297 indexer = self.get_indexer_for(keyarr)
6298 keyarr = self.reindex(keyarr)[0]
6299 else:
6300 keyarr, indexer, new_indexer = self._reindex_non_unique(keyarr)
6301
6302 self._raise_if_missing(keyarr, indexer, axis_name)
6303
6304 keyarr = self.take(indexer)
6305 if isinstance(key, Index):
6306 # GH 42790 - Preserve name from an Index
6307 keyarr.name = key.name
6308 if lib.is_np_dtype(keyarr.dtype, "mM") or isinstance(
6309 keyarr.dtype, DatetimeTZDtype
6310 ):
6311 # DTI/TDI.take can infer a freq in some cases when we dont want one
6312 if isinstance(key, list) or (
6313 isinstance(key, type(self))
6314 # "Index" has no attribute "freq"
6315 and key.freq is None # type: ignore[attr-defined]
6316 ):
6317 # error: "Index" has no attribute "_with_freq"; maybe "_with_infer"?
6318 keyarr = keyarr._with_freq(None) # type: ignore[attr-defined]
6319
6320 return keyarr, indexer
6321
6322 def _raise_if_missing(self, key, indexer, axis_name: str_t) -> None:
6323 """
6324 Check that indexer can be used to return a result.
6325
6326 e.g. at least one element was found,
6327 unless the list of keys was actually empty.
6328
6329 Parameters
6330 ----------
6331 key : list-like
6332 Targeted labels (only used to show correct error message).
6333 indexer: array-like of booleans
6334 Indices corresponding to the key,
6335 (with -1 indicating not found).
6336 axis_name : str
6337
6338 Raises
6339 ------
6340 KeyError
6341 If at least one key was requested but none was found.
6342 """
6343 if len(key) == 0:
6344 return
6345
6346 # Count missing values
6347 missing_mask = indexer < 0
6348 nmissing = missing_mask.sum()
6349
6350 if nmissing:
6351 if nmissing == len(indexer):
6352 raise KeyError(f"None of [{key}] are in the [{axis_name}]")
6353
6354 not_found = list(ensure_index(key)[missing_mask.nonzero()[0]].unique())
6355 raise KeyError(f"{not_found} not in index")
6356
6357 @overload
6358 def _get_indexer_non_comparable(
6359 self, target: Index, method, unique: Literal[True] = ...
6360 ) -> npt.NDArray[np.intp]: ...
6361
6362 @overload
6363 def _get_indexer_non_comparable(
6364 self, target: Index, method, unique: Literal[False]
6365 ) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]: ...
6366
6367 @overload
6368 def _get_indexer_non_comparable(
6369 self, target: Index, method, unique: bool = True
6370 ) -> npt.NDArray[np.intp] | tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]: ...
6371
6372 @final
6373 def _get_indexer_non_comparable(
6374 self, target: Index, method, unique: bool = True
6375 ) -> npt.NDArray[np.intp] | tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]:
6376 """
6377 Called from get_indexer or get_indexer_non_unique when the target
6378 is of a non-comparable dtype.
6379
6380 For get_indexer lookups with method=None, get_indexer is an _equality_
6381 check, so non-comparable dtypes mean we will always have no matches.
6382
6383 For get_indexer lookups with a method, get_indexer is an _inequality_
6384 check, so non-comparable dtypes mean we will always raise TypeError.
6385
6386 Parameters
6387 ----------
6388 target : Index
6389 method : str or None
6390 unique : bool, default True
6391 * True if called from get_indexer.
6392 * False if called from get_indexer_non_unique.
6393
6394 Raises
6395 ------
6396 TypeError
6397 If doing an inequality check, i.e. method is not None.
6398 """
6399 if method is not None:
6400 other_dtype = _unpack_nested_dtype(target)
6401 raise TypeError(f"Cannot compare dtypes {self.dtype} and {other_dtype}")
6402
6403 no_matches = -1 * np.ones(target.shape, dtype=np.intp)
6404 if unique:
6405 # This is for get_indexer
6406 return no_matches
6407 else:
6408 # This is for get_indexer_non_unique
6409 missing = np.arange(len(target), dtype=np.intp)
6410 return no_matches, missing
6411
6412 @property
6413 def _index_as_unique(self) -> bool:
6414 """
6415 Whether we should treat this as unique for the sake of
6416 get_indexer vs get_indexer_non_unique.
6417
6418 For IntervalIndex compat.
6419 """
6420 return self.is_unique
6421
6422 _requires_unique_msg = "Reindexing only valid with uniquely valued Index objects"
6423
6424 @final
6425 def _maybe_downcast_for_indexing(self, other: Index) -> tuple[Index, Index]:
6426 """
6427 When dealing with an object-dtype Index and a non-object Index, see
6428 if we can upcast the object-dtype one to improve performance.
6429 """
6430
6431 if isinstance(self, ABCDatetimeIndex) and isinstance(other, ABCDatetimeIndex):
6432 if (
6433 self.tz is not None
6434 and other.tz is not None
6435 and not tz_compare(self.tz, other.tz)
6436 ):
6437 # standardize on UTC
6438 return self.tz_convert("UTC"), other.tz_convert("UTC")
6439
6440 elif self.inferred_type == "date" and isinstance(other, ABCDatetimeIndex):
6441 try:
6442 return type(other)(self), other
6443 except OutOfBoundsDatetime:
6444 return self, other
6445 elif self.inferred_type == "timedelta" and isinstance(other, ABCTimedeltaIndex):
6446 # TODO: we dont have tests that get here
6447 return type(other)(self), other
6448
6449 elif self.dtype.kind == "u" and other.dtype.kind == "i":
6450 # GH#41873
6451 if other.min() >= 0:
6452 # lookup min as it may be cached
6453 # TODO: may need itemsize check if we have non-64-bit Indexes
6454 return self, other.astype(self.dtype)
6455
6456 elif self._is_multi and not other._is_multi:
6457 try:
6458 # "Type[Index]" has no attribute "from_tuples"
6459 other = type(self).from_tuples(other) # type: ignore[attr-defined]
6460 except (TypeError, ValueError):
6461 # let's instead try with a straight Index
6462 self = Index(self._values, copy=False)
6463
6464 if not is_object_dtype(self.dtype) and is_object_dtype(other.dtype):
6465 # Reverse op so we dont need to re-implement on the subclasses
6466 other, self = other._maybe_downcast_for_indexing(self)
6467
6468 return self, other
6469
6470 @final
6471 def _find_common_type_compat(self, target) -> DtypeObj:
6472 """
6473 Implementation of find_common_type that adjusts for Index-specific
6474 special cases.
6475 """
6476 target_dtype, _ = infer_dtype_from(target)
6477
6478 if isinstance(target, tuple):
6479 # GH#54385
6480 return np.dtype(object)
6481
6482 if using_string_dtype():
6483 # special case: if left or right is a zero-length RangeIndex or
6484 # Index[object], those can be created by the default empty constructors
6485 # -> for that case ignore this dtype and always return the other
6486 # (https://github.com/pandas-dev/pandas/pull/60797)
6487 from pandas.core.indexes.range import RangeIndex
6488
6489 if len(self) == 0 and (
6490 isinstance(self, RangeIndex) or self.dtype == np.object_
6491 ):
6492 return target_dtype
6493 if (
6494 isinstance(target, Index)
6495 and len(target) == 0
6496 and (isinstance(target, RangeIndex) or target_dtype == np.object_)
6497 ):
6498 return self.dtype
6499
6500 # special case: if one dtype is uint64 and the other a signed int, return object
6501 # See https://github.com/pandas-dev/pandas/issues/26778 for discussion
6502 # Now it's:
6503 # * float | [u]int -> float
6504 # * uint64 | signed int -> object
6505 # We may change union(float | [u]int) to go to object.
6506 if self.dtype == "uint64" or target_dtype == "uint64":
6507 if is_signed_integer_dtype(self.dtype) or is_signed_integer_dtype(
6508 target_dtype
6509 ):
6510 return _dtype_obj
6511
6512 dtype = find_result_type(self.dtype, target)
6513 dtype = common_dtype_categorical_compat([self, target], dtype)
6514 return dtype
6515
6516 @final
6517 def _should_compare(self, other: Index) -> bool:
6518 """
6519 Check if `self == other` can ever have non-False entries.
6520 """
6521
6522 # NB: we use inferred_type rather than is_bool_dtype to catch
6523 # object_dtype_of_bool and categorical[object_dtype_of_bool] cases
6524 if (
6525 other.inferred_type == "boolean" and is_any_real_numeric_dtype(self.dtype)
6526 ) or (
6527 self.inferred_type == "boolean" and is_any_real_numeric_dtype(other.dtype)
6528 ):
6529 # GH#16877 Treat boolean labels passed to a numeric index as not
6530 # found. Without this fix False and True would be treated as 0 and 1
6531 # respectively.
6532 return False
6533
6534 dtype = _unpack_nested_dtype(other)
6535 return (
6536 self._is_comparable_dtype(dtype)
6537 or is_object_dtype(dtype)
6538 or is_string_dtype(dtype)
6539 )
6540
6541 def _is_comparable_dtype(self, dtype: DtypeObj) -> bool:
6542 """
6543 Can we compare values of the given dtype to our own?
6544 """
6545 if self.dtype.kind == "b":
6546 return dtype.kind == "b"
6547 elif is_numeric_dtype(self.dtype):
6548 return is_numeric_dtype(dtype)
6549 # TODO: this was written assuming we only get here with object-dtype,
6550 # which is no longer correct. Can we specialize for EA?
6551 return True
6552
6553 @final
6554 def groupby(self, values) -> PrettyDict[Hashable, Index]:
6555 """
6556 Group the index labels by a given array of values.
6557
6558 Parameters
6559 ----------
6560 values : array
6561 Values used to determine the groups.
6562
6563 Returns
6564 -------
6565 dict
6566 {group name -> group labels}
6567 """
6568 # TODO: if we are a MultiIndex, we can do better
6569 # that converting to tuples
6570 if isinstance(values, ABCMultiIndex):
6571 values = values._values
6572 values = Categorical(values)
6573 result = values._reverse_indexer()
6574
6575 # map to the label
6576 result = {k: self.take(v) for k, v in result.items()}
6577
6578 return PrettyDict(result)
6579
6580 def map(self, mapper, na_action: Literal["ignore"] | None = None):
6581 """
6582 Map values using an input mapping or function.
6583
6584 Parameters
6585 ----------
6586 mapper : function, dict, or Series
6587 Mapping correspondence.
6588 na_action : {None, 'ignore'}
6589 If 'ignore', propagate NA values, without passing them to the
6590 mapping correspondence.
6591
6592 Returns
6593 -------
6594 Union[Index, MultiIndex]
6595 The output of the mapping function applied to the index.
6596 If the function returns a tuple with more than one element
6597 a MultiIndex will be returned.
6598
6599 See Also
6600 --------
6601 Index.where : Replace values where the condition is False.
6602
6603 Examples
6604 --------
6605 >>> idx = pd.Index([1, 2, 3])
6606 >>> idx.map({1: "a", 2: "b", 3: "c"})
6607 Index(['a', 'b', 'c'], dtype='str')
6608
6609 Using `map` with a function:
6610
6611 >>> idx = pd.Index([1, 2, 3])
6612 >>> idx.map("I am a {}".format)
6613 Index(['I am a 1', 'I am a 2', 'I am a 3'], dtype='str')
6614
6615 >>> idx = pd.Index(["a", "b", "c"])
6616 >>> idx.map(lambda x: x.upper())
6617 Index(['A', 'B', 'C'], dtype='str')
6618 """
6619 from pandas.core.indexes.multi import MultiIndex
6620
6621 new_values = self._map_values(mapper, na_action=na_action)
6622
6623 # we can return a MultiIndex
6624 if new_values.size and isinstance(new_values[0], tuple):
6625 if isinstance(self, MultiIndex):
6626 names = self.names
6627 elif self.name:
6628 names = [self.name] * len(new_values[0])
6629 else:
6630 names = None
6631 return MultiIndex.from_tuples(new_values, names=names)
6632
6633 dtype = None
6634 if not new_values.size:
6635 # empty
6636 dtype = self.dtype
6637 elif isinstance(new_values, Categorical):
6638 # cast_pointwise_result is unnecessary
6639 dtype = new_values.dtype
6640 else:
6641 if isinstance(self, MultiIndex):
6642 arr = self[:0].to_flat_index().array
6643 else:
6644 arr = self[:0].array
6645 # e.g. if we are floating and new_values is all ints, then we
6646 # don't want to cast back to floating. But if we are UInt64
6647 # and new_values is all ints, we want to try.
6648 new_values = arr._cast_pointwise_result(new_values)
6649 dtype = new_values.dtype
6650 return Index(new_values, dtype=dtype, copy=False, name=self.name)
6651
6652 # TODO: De-duplicate with map, xref GH#32349
6653 @final
6654 def _transform_index(self, func, *, level=None) -> Index:
6655 """
6656 Apply function to all values found in index.
6657
6658 This includes transforming multiindex entries separately.
6659 Only apply function to one level of the MultiIndex if level is specified.
6660 """
6661 if isinstance(self, ABCMultiIndex):
6662 values = [
6663 (
6664 self.get_level_values(i).map(func)
6665 if i == level or level is None
6666 else self.get_level_values(i)
6667 )
6668 for i in range(self.nlevels)
6669 ]
6670 return type(self).from_arrays(values)
6671 else:
6672 items = [func(x) for x in self]
6673 return Index(items, name=self.name, tupleize_cols=False)
6674
6675 def isin(self, values, level: str_t | int | None = None) -> npt.NDArray[np.bool_]:
6676 """
6677 Return a boolean array where the index values are in `values`.
6678
6679 Compute boolean array of whether each index value is found in the
6680 passed set of values. The length of the returned boolean array matches
6681 the length of the index.
6682
6683 Parameters
6684 ----------
6685 values : set or list-like
6686 Sought values.
6687 level : str or int, optional
6688 Name or position of the index level to use (if the index is a
6689 `MultiIndex`).
6690
6691 Returns
6692 -------
6693 np.ndarray[bool]
6694 NumPy array of boolean values.
6695
6696 See Also
6697 --------
6698 Series.isin : Same for Series.
6699 DataFrame.isin : Same method for DataFrames.
6700
6701 Notes
6702 -----
6703 In the case of `MultiIndex` you must either specify `values` as a
6704 list-like object containing tuples that are the same length as the
6705 number of levels, or specify `level`. Otherwise it will raise a
6706 ``ValueError``.
6707
6708 If `level` is specified:
6709
6710 - if it is the name of one *and only one* index level, use that level;
6711 - otherwise it should be a number indicating level position.
6712
6713 Examples
6714 --------
6715 >>> idx = pd.Index([1, 2, 3])
6716 >>> idx
6717 Index([1, 2, 3], dtype='int64')
6718
6719 Check whether each index value in a list of values.
6720
6721 >>> idx.isin([1, 4])
6722 array([ True, False, False])
6723
6724 >>> midx = pd.MultiIndex.from_arrays(
6725 ... [[1, 2, 3], ["red", "blue", "green"]], names=["number", "color"]
6726 ... )
6727 >>> midx
6728 MultiIndex([(1, 'red'),
6729 (2, 'blue'),
6730 (3, 'green')],
6731 names=['number', 'color'])
6732
6733 Check whether the strings in the 'color' level of the MultiIndex
6734 are in a list of colors.
6735
6736 >>> midx.isin(["red", "orange", "yellow"], level="color")
6737 array([ True, False, False])
6738
6739 To check across the levels of a MultiIndex, pass a list of tuples:
6740
6741 >>> midx.isin([(1, "red"), (3, "red")])
6742 array([ True, False, False])
6743 """
6744 if level is not None:
6745 self._validate_index_level(level)
6746 return algos.isin(self._values, values)
6747
6748 def _get_string_slice(self, key: str_t):
6749 # this is for partial string indexing,
6750 # overridden in DatetimeIndex, TimedeltaIndex and PeriodIndex
6751 raise NotImplementedError
6752
6753 def slice_indexer(
6754 self,
6755 start: Hashable | None = None,
6756 end: Hashable | None = None,
6757 step: int | None = None,
6758 ) -> slice:
6759 """
6760 Compute the slice indexer for input labels and step.
6761
6762 Index needs to be ordered and unique.
6763
6764 Parameters
6765 ----------
6766 start : label, default None
6767 If None, defaults to the beginning.
6768 end : label, default None
6769 If None, defaults to the end.
6770 step : int, default None
6771 If None, defaults to 1.
6772
6773 Returns
6774 -------
6775 slice
6776 A slice object.
6777
6778 Raises
6779 ------
6780 KeyError : If key does not exist, or key is not unique and index is
6781 not ordered.
6782
6783 See Also
6784 --------
6785 Index.slice_locs : Computes slice locations for input labels.
6786 Index.get_slice_bound : Retrieves slice bound that corresponds to given label.
6787
6788 Notes
6789 -----
6790 This function assumes that the data is sorted, so use at your own peril.
6791
6792 Examples
6793 --------
6794 This is a method on all index types. For example you can do:
6795
6796 >>> idx = pd.Index(list("abcd"))
6797 >>> idx.slice_indexer(start="b", end="c")
6798 slice(1, 3, None)
6799
6800 >>> idx = pd.MultiIndex.from_arrays([list("abcd"), list("efgh")])
6801 >>> idx.slice_indexer(start="b", end=("c", "g"))
6802 slice(1, 3, None)
6803 """
6804 start_slice, end_slice = self.slice_locs(start, end, step=step)
6805
6806 # return a slice
6807 if not is_scalar(start_slice):
6808 raise AssertionError("Start slice bound is non-scalar")
6809 if not is_scalar(end_slice):
6810 raise AssertionError("End slice bound is non-scalar")
6811
6812 return slice(start_slice, end_slice, step)
6813
6814 def _maybe_cast_indexer(self, key):
6815 """
6816 If we have a float key and are not a floating index, then try to cast
6817 to an int if equivalent.
6818 """
6819 if (
6820 is_float(key)
6821 and np.isnan(key)
6822 and isinstance(self.dtype, FloatingDtype)
6823 and is_nan_na()
6824 ):
6825 # TODO: better place to do this?
6826 key = self.dtype.na_value
6827 return key
6828
6829 def _maybe_cast_listlike_indexer(self, target) -> Index:
6830 """
6831 Analogue to maybe_cast_indexer for get_indexer instead of get_loc.
6832 """
6833 target_index = ensure_index(target)
6834 if (
6835 not hasattr(target, "dtype")
6836 and self.dtype == object
6837 and target_index.dtype == "string"
6838 ):
6839 # If we started with a list-like, avoid inference to string dtype if self
6840 # is object dtype (coercing to string dtype will alter the missing values)
6841 target_index = Index(target, dtype=self.dtype)
6842 elif (
6843 not hasattr(target, "dtype")
6844 and isinstance(self.dtype, StringDtype)
6845 and self.dtype.na_value is np.nan
6846 and using_string_dtype()
6847 ):
6848 # Fill missing values to ensure consistent missing value representation
6849 target_index = target_index.fillna(np.nan)
6850 return target_index
6851
6852 @final
6853 def _validate_indexer(
6854 self,
6855 form: Literal["positional", "slice"],
6856 key,
6857 kind: Literal["getitem", "iloc"],
6858 ) -> None:
6859 """
6860 If we are positional indexer, validate that we have appropriate
6861 typed bounds must be an integer.
6862 """
6863 if not lib.is_int_or_none(key):
6864 self._raise_invalid_indexer(form, key)
6865
6866 def _maybe_cast_slice_bound(self, label, side: str_t):
6867 """
6868 This function should be overloaded in subclasses that allow non-trivial
6869 casting on label-slice bounds, e.g. datetime-like indices allowing
6870 strings containing formatted datetimes.
6871
6872 Parameters
6873 ----------
6874 label : object
6875 side : {'left', 'right'}
6876
6877 Returns
6878 -------
6879 label : object
6880
6881 Notes
6882 -----
6883 Value of `side` parameter should be validated in caller.
6884 """
6885
6886 # We are a plain index here (sub-class override this method if they
6887 # wish to have special treatment for floats/ints, e.g. datetimelike Indexes
6888
6889 if is_numeric_dtype(self.dtype):
6890 return self._maybe_cast_indexer(label)
6891
6892 # reject them, if index does not contain label
6893 if (is_float(label) or is_integer(label)) and label not in self:
6894 self._raise_invalid_indexer("slice", label)
6895
6896 return label
6897
6898 def _searchsorted_monotonic(self, label, side: Literal["left", "right"] = "left"):
6899 if self.is_monotonic_increasing:
6900 return self.searchsorted(label, side=side)
6901 elif self.is_monotonic_decreasing:
6902 # np.searchsorted expects ascending sort order, have to reverse
6903 # everything for it to work (element ordering, search side and
6904 # resulting value).
6905 pos = self[::-1].searchsorted(
6906 label, side="right" if side == "left" else "left"
6907 )
6908 return maybe_unbox_numpy_scalar(len(self) - pos)
6909
6910 raise ValueError("index must be monotonic increasing or decreasing")
6911
6912 def get_slice_bound(self, label, side: Literal["left", "right"]) -> int:
6913 """
6914 Calculate slice bound that corresponds to given label.
6915
6916 Returns leftmost (one-past-the-rightmost if ``side=='right'``) position
6917 of given label.
6918
6919 Parameters
6920 ----------
6921 label : object
6922 The label for which to calculate the slice bound.
6923 side : {'left', 'right'}
6924 if 'left' return leftmost position of given label.
6925 if 'right' return one-past-the-rightmost position of given label.
6926
6927 Returns
6928 -------
6929 int
6930 Index of label.
6931
6932 See Also
6933 --------
6934 Index.get_loc : Get integer location, slice or boolean mask for requested
6935 label.
6936
6937 Examples
6938 --------
6939 >>> idx = pd.RangeIndex(5)
6940 >>> idx.get_slice_bound(3, "left")
6941 3
6942
6943 >>> idx.get_slice_bound(3, "right")
6944 4
6945
6946 If ``label`` is non-unique in the index, an error will be raised.
6947
6948 >>> idx_duplicate = pd.Index(["a", "b", "a", "c", "d"])
6949 >>> idx_duplicate.get_slice_bound("a", "left")
6950 Traceback (most recent call last):
6951 KeyError: Cannot get left slice bound for non-unique label: 'a'
6952 """
6953
6954 if side not in ("left", "right"):
6955 raise ValueError(
6956 "Invalid value for side kwarg, must be either "
6957 f"'left' or 'right': {side}"
6958 )
6959
6960 original_label = label
6961
6962 # For datetime indices label may be a string that has to be converted
6963 # to datetime boundary according to its resolution.
6964 label = self._maybe_cast_slice_bound(label, side)
6965
6966 # we need to look up the label
6967 try:
6968 slc = self.get_loc(label)
6969 except KeyError:
6970 try:
6971 return self._searchsorted_monotonic(label, side)
6972 except ValueError:
6973 raise KeyError(
6974 f"Cannot get {side} slice bound for non-monotonic index "
6975 f"with a missing label {original_label!r}. "
6976 "Either sort the index or specify an existing label."
6977 ) from None
6978
6979 if isinstance(slc, np.ndarray):
6980 # get_loc may return a boolean array, which
6981 # is OK as long as they are representable by a slice.
6982 assert is_bool_dtype(slc.dtype)
6983 slc = lib.maybe_booleans_to_slice(slc.view("u1"))
6984 if isinstance(slc, np.ndarray):
6985 raise KeyError(
6986 f"Cannot get {side} slice bound for non-unique "
6987 f"label: {original_label!r}"
6988 )
6989
6990 if isinstance(slc, slice):
6991 if side == "left":
6992 return slc.start
6993 else:
6994 return slc.stop
6995 elif side == "right":
6996 return slc + 1
6997 else:
6998 return slc
6999
7000 def slice_locs(
7001 self,
7002 start: SliceType = None,
7003 end: SliceType = None,
7004 step: int | None = None,
7005 ) -> tuple[int, int]:
7006 """
7007 Compute slice locations for input labels.
7008
7009 Parameters
7010 ----------
7011 start : label, default None
7012 If None, defaults to the beginning.
7013 end : label, default None
7014 If None, defaults to the end.
7015 step : int, defaults None
7016 If None, defaults to 1.
7017
7018 Returns
7019 -------
7020 tuple[int, int]
7021 Returns a tuple of two integers representing the slice locations for the
7022 input labels within the index.
7023
7024 See Also
7025 --------
7026 Index.get_loc : Get location for a single label.
7027
7028 Notes
7029 -----
7030 This method only works if the index is monotonic or unique.
7031
7032 Examples
7033 --------
7034 >>> idx = pd.Index(list("abcd"))
7035 >>> idx.slice_locs(start="b", end="c")
7036 (1, 3)
7037
7038 >>> idx = pd.Index(list("bcde"))
7039 >>> idx.slice_locs(start="a", end="c")
7040 (0, 2)
7041 """
7042 inc = step is None or step >= 0
7043
7044 if not inc:
7045 # If it's a reverse slice, temporarily swap bounds.
7046 start, end = end, start
7047
7048 # GH 16785: If start and end happen to be date strings with UTC offsets
7049 # attempt to parse and check that the offsets are the same
7050 if isinstance(start, (str, datetime)) and isinstance(end, (str, datetime)):
7051 try:
7052 ts_start = Timestamp(start)
7053 ts_end = Timestamp(end)
7054 except (ValueError, TypeError):
7055 pass
7056 else:
7057 if not tz_compare(ts_start.tzinfo, ts_end.tzinfo):
7058 raise ValueError("Both dates must have the same UTC offset")
7059
7060 start_slice = None
7061 if start is not None:
7062 start_slice = self.get_slice_bound(start, "left")
7063 if start_slice is None:
7064 start_slice = 0
7065
7066 end_slice = None
7067 if end is not None:
7068 end_slice = self.get_slice_bound(end, "right")
7069 if end_slice is None:
7070 end_slice = len(self)
7071
7072 if not inc:
7073 # Bounds at this moment are swapped, swap them back and shift by 1.
7074 #
7075 # slice_locs('B', 'A', step=-1): s='B', e='A'
7076 #
7077 # s='A' e='B'
7078 # AFTER SWAP: | |
7079 # v ------------------> V
7080 # -----------------------------------
7081 # | | |A|A|A|A| | | | | |B|B| | | | |
7082 # -----------------------------------
7083 # ^ <------------------ ^
7084 # SHOULD BE: | |
7085 # end=s-1 start=e-1
7086 #
7087 end_slice, start_slice = start_slice - 1, end_slice - 1
7088
7089 # i == -1 triggers ``len(self) + i`` selection that points to the
7090 # last element, not before-the-first one, subtracting len(self)
7091 # compensates that.
7092 if end_slice == -1:
7093 end_slice -= len(self)
7094 if start_slice == -1:
7095 start_slice -= len(self)
7096
7097 start_slice = maybe_unbox_numpy_scalar(start_slice)
7098 end_slice = maybe_unbox_numpy_scalar(end_slice)
7099 return start_slice, end_slice
7100
7101 def delete(
7102 self, loc: int | np.integer | list[int] | npt.NDArray[np.integer]
7103 ) -> Self:
7104 """
7105 Make new Index with passed location(-s) deleted.
7106
7107 Parameters
7108 ----------
7109 loc : int or list of int
7110 Location of item(-s) which will be deleted.
7111 Use a list of locations to delete more than one value at the same time.
7112
7113 Returns
7114 -------
7115 Index
7116 Will be same type as self, except for RangeIndex.
7117
7118 See Also
7119 --------
7120 numpy.delete : Delete any rows and column from NumPy array (ndarray).
7121
7122 Examples
7123 --------
7124 >>> idx = pd.Index(["a", "b", "c"])
7125 >>> idx.delete(1)
7126 Index(['a', 'c'], dtype='str')
7127
7128 >>> idx = pd.Index(["a", "b", "c"])
7129 >>> idx.delete([0, 2])
7130 Index(['b'], dtype='str')
7131 """
7132 values = self._values
7133 res_values: ArrayLike
7134 if isinstance(values, np.ndarray):
7135 # TODO(__array_function__): special casing will be unnecessary
7136 res_values = np.delete(values, loc)
7137 else:
7138 res_values = values.delete(loc)
7139
7140 # _constructor so RangeIndex-> Index with an int64 dtype
7141 return self._constructor._simple_new(res_values, name=self.name)
7142
7143 def insert(self, loc: int, item) -> Index:
7144 """
7145 Make new Index inserting new item at location.
7146
7147 Follows Python numpy.insert semantics for negative values.
7148
7149 Parameters
7150 ----------
7151 loc : int
7152 The integer location where the new item will be inserted.
7153 item : object
7154 The new item to be inserted into the Index.
7155
7156 Returns
7157 -------
7158 Index
7159 Returns a new Index object resulting from inserting the specified item at
7160 the specified location within the original Index.
7161
7162 See Also
7163 --------
7164 Index.append : Append a collection of Indexes together.
7165
7166 Examples
7167 --------
7168 >>> idx = pd.Index(["a", "b", "c"])
7169 >>> idx.insert(1, "x")
7170 Index(['a', 'x', 'b', 'c'], dtype='str')
7171 """
7172 item = lib.item_from_zerodim(item)
7173 if is_valid_na_for_dtype(item, self.dtype) and self.dtype != object:
7174 item = self._na_value
7175
7176 arr = self._values
7177
7178 if using_string_dtype() and len(self) == 0 and self.dtype == np.object_:
7179 # special case: if we are an empty object-dtype Index, also
7180 # take into account the inserted item for the resulting dtype
7181 # (https://github.com/pandas-dev/pandas/pull/60797)
7182 dtype = self._find_common_type_compat(item)
7183 if dtype != self.dtype:
7184 return self.astype(dtype).insert(loc, item)
7185
7186 try:
7187 if isinstance(arr, ExtensionArray):
7188 res_values = arr.insert(loc, item)
7189 return type(self)._simple_new(res_values, name=self.name)
7190 else:
7191 item = self._validate_fill_value(item)
7192 except (TypeError, ValueError, LossySetitemError):
7193 # e.g. trying to insert an integer into a DatetimeIndex
7194 # We cannot keep the same dtype, so cast to the (often object)
7195 # minimal shared dtype before doing the insert.
7196 dtype = self._find_common_type_compat(item)
7197 if dtype == self.dtype:
7198 # EA's might run into recursion errors if loc is invalid
7199 raise
7200 return self.astype(dtype).insert(loc, item)
7201
7202 if arr.dtype != object or not isinstance(
7203 item, (tuple, np.datetime64, np.timedelta64)
7204 ):
7205 # with object-dtype we need to worry about numpy incorrectly casting
7206 # dt64/td64 to integer, also about treating tuples as sequences
7207 # special-casing dt64/td64 https://github.com/numpy/numpy/issues/12550
7208 casted = arr.dtype.type(item)
7209 new_values = np.insert(arr, loc, casted)
7210
7211 else:
7212 # error: No overload variant of "insert" matches argument types
7213 # "ndarray[Any, Any]", "int", "None"
7214 new_values = np.insert(arr, loc, None) # type: ignore[call-overload]
7215 loc = loc if loc >= 0 else loc - 1
7216 new_values[loc] = item
7217
7218 # GH#51363 stopped doing dtype inference here
7219 out = Index(new_values, dtype=new_values.dtype, name=self.name, copy=False)
7220 return out
7221
7222 def drop(
7223 self,
7224 labels: Index | np.ndarray | Iterable[Hashable],
7225 errors: IgnoreRaise = "raise",
7226 ) -> Index:
7227 """
7228 Make new Index with passed list of labels deleted.
7229
7230 Parameters
7231 ----------
7232 labels : array-like or scalar
7233 Array-like object or a scalar value, representing the labels to be removed
7234 from the Index.
7235 errors : {'ignore', 'raise'}, default 'raise'
7236 If 'ignore', suppress error and existing labels are dropped.
7237
7238 Returns
7239 -------
7240 Index
7241 Will be same type as self, except for RangeIndex.
7242
7243 Raises
7244 ------
7245 KeyError
7246 If not all of the labels are found in the selected axis
7247
7248 See Also
7249 --------
7250 Index.dropna : Return Index without NA/NaN values.
7251 Index.drop_duplicates : Return Index with duplicate values removed.
7252
7253 Examples
7254 --------
7255 >>> idx = pd.Index(["a", "b", "c"])
7256 >>> idx.drop(["a"])
7257 Index(['b', 'c'], dtype='str')
7258 """
7259 if not isinstance(labels, Index):
7260 # avoid materializing e.g. RangeIndex
7261 arr_dtype = "object" if self.dtype == "object" else None
7262 labels = com.index_labels_to_array(labels, dtype=arr_dtype)
7263
7264 indexer = self.get_indexer_for(labels)
7265 mask = indexer == -1
7266 if mask.any():
7267 if errors != "ignore":
7268 raise KeyError(f"{labels[mask].tolist()} not found in axis")
7269 indexer = indexer[~mask]
7270 return self.delete(indexer)
7271
7272 @final
7273 def infer_objects(self, copy: bool = True) -> Index:
7274 """
7275 If we have an object dtype, try to infer a non-object dtype.
7276
7277 Parameters
7278 ----------
7279 copy : bool, default True
7280 Whether to make a copy in cases where no inference occurs.
7281
7282 Returns
7283 -------
7284 Index
7285 An Index with a new dtype if the dtype was inferred
7286 or a shallow copy if the dtype could not be inferred.
7287
7288 See Also
7289 --------
7290 Index.inferred_type: Return a string of the type inferred from the values.
7291
7292 Examples
7293 --------
7294 >>> pd.Index(["a", 1]).infer_objects()
7295 Index(['a', 1], dtype='object')
7296 >>> pd.Index([1, 2], dtype="object").infer_objects()
7297 Index([1, 2], dtype='int64')
7298 """
7299 if self._is_multi:
7300 raise NotImplementedError(
7301 "infer_objects is not implemented for MultiIndex. "
7302 "Use index.to_frame().infer_objects() instead."
7303 )
7304 if self.dtype != object:
7305 return self.copy() if copy else self
7306
7307 values = self._values
7308 values = cast("npt.NDArray[np.object_]", values)
7309 res_values = lib.maybe_convert_objects(
7310 values,
7311 convert_non_numeric=True,
7312 )
7313 if copy and res_values is values:
7314 return self.copy()
7315 result = Index(res_values, name=self.name, copy=False)
7316 if not copy and res_values is values and self._references is not None:
7317 result._references = self._references
7318 result._references.add_index_reference(result)
7319 return result
7320
7321 @final
7322 def diff(self, periods: int = 1) -> Index:
7323 """
7324 Computes the difference between consecutive values in the Index object.
7325
7326 If periods is greater than 1, computes the difference between values that
7327 are `periods` number of positions apart.
7328
7329 Parameters
7330 ----------
7331 periods : int, optional
7332 The number of positions between the current and previous
7333 value to compute the difference with. Default is 1.
7334
7335 Returns
7336 -------
7337 Index
7338 A new Index object with the computed differences.
7339
7340 Examples
7341 --------
7342 >>> import pandas as pd
7343 >>> idx = pd.Index([10, 20, 30, 40, 50])
7344 >>> idx.diff()
7345 Index([nan, 10.0, 10.0, 10.0, 10.0], dtype='float64')
7346
7347 """
7348 return Index(self.to_series().diff(periods))
7349
7350 def round(self, decimals: int = 0) -> Self:
7351 """
7352 Round each value in the Index to the given number of decimals.
7353
7354 Parameters
7355 ----------
7356 decimals : int, optional
7357 Number of decimal places to round to. If decimals is negative,
7358 it specifies the number of positions to the left of the decimal point.
7359
7360 Returns
7361 -------
7362 Index
7363 A new Index with the rounded values.
7364
7365 Examples
7366 --------
7367 >>> import pandas as pd
7368 >>> idx = pd.Index([10.1234, 20.5678, 30.9123, 40.4567, 50.7890])
7369 >>> idx.round(decimals=2)
7370 Index([10.12, 20.57, 30.91, 40.46, 50.79], dtype='float64')
7371
7372 """
7373 return self._constructor(self.to_series().round(decimals))
7374
7375 # --------------------------------------------------------------------
7376 # Generated Arithmetic, Comparison, and Unary Methods
7377
7378 def _cmp_method(self, other, op):
7379 """
7380 Wrapper used to dispatch comparison operations.
7381 """
7382 if self.is_(other):
7383 # fastpath
7384 if op in {operator.eq, operator.le, operator.ge}:
7385 arr = np.ones(len(self), dtype=bool)
7386 if self._can_hold_na and not isinstance(self, ABCMultiIndex):
7387 # TODO: should set MultiIndex._can_hold_na = False?
7388 arr[self.isna()] = False
7389 return arr
7390 elif op is operator.ne:
7391 arr = np.zeros(len(self), dtype=bool)
7392 if self._can_hold_na and not isinstance(self, ABCMultiIndex):
7393 arr[self.isna()] = True
7394 return arr
7395
7396 if isinstance(other, (np.ndarray, Index, ABCSeries, ExtensionArray)) and len(
7397 self
7398 ) != len(other):
7399 raise ValueError("Lengths must match to compare")
7400
7401 if not isinstance(other, ABCMultiIndex):
7402 other = extract_array(other, extract_numpy=True)
7403 else:
7404 other = np.asarray(other)
7405
7406 result = ops.comparison_op(self._values, other, op)
7407
7408 return result
7409
7410 @final
7411 def _logical_method(self, other, op):
7412 res_name = ops.get_op_result_name(self, other)
7413
7414 lvalues = self._values
7415 rvalues = extract_array(other, extract_numpy=True, extract_range=True)
7416
7417 res_values = ops.logical_op(lvalues, rvalues, op)
7418 return self._construct_result(res_values, name=res_name, other=other)
7419
7420 @final
7421 def _construct_result(self, result, name, other):
7422 if isinstance(result, tuple):
7423 return (
7424 Index(result[0], name=name, dtype=result[0].dtype, copy=False),
7425 Index(result[1], name=name, dtype=result[1].dtype, copy=False),
7426 )
7427 return Index(result, name=name, dtype=result.dtype, copy=False)
7428
7429 def _arith_method(self, other, op):
7430 if (
7431 isinstance(other, Index)
7432 and is_object_dtype(other.dtype)
7433 and type(other) is not Index
7434 ):
7435 # We return NotImplemented for object-dtype index *subclasses* so they have
7436 # a chance to implement ops before we unwrap them.
7437 # See https://github.com/pandas-dev/pandas/issues/31109
7438 return NotImplemented
7439
7440 return super()._arith_method(other, op)
7441
7442 @final
7443 def _unary_method(self, op):
7444 result = op(self._values)
7445 return Index(result, name=self.name, copy=False)
7446
7447 def __abs__(self) -> Index:
7448 return self._unary_method(operator.abs)
7449
7450 def __neg__(self) -> Index:
7451 return self._unary_method(operator.neg)
7452
7453 def __pos__(self) -> Index:
7454 return self._unary_method(operator.pos)
7455
7456 def __invert__(self) -> Index:
7457 # GH#8875
7458 return self._unary_method(operator.inv)
7459
7460 # --------------------------------------------------------------------
7461 # Reductions
7462
7463 def any(self, *args, **kwargs):
7464 """
7465 Return whether any element is Truthy.
7466
7467 Parameters
7468 ----------
7469 *args
7470 Required for compatibility with numpy.
7471 **kwargs
7472 Required for compatibility with numpy.
7473
7474 Returns
7475 -------
7476 bool or array-like (if axis is specified)
7477 A single element array-like may be converted to bool.
7478
7479 See Also
7480 --------
7481 Index.all : Return whether all elements are True.
7482 Series.all : Return whether all elements are True.
7483
7484 Notes
7485 -----
7486 Not a Number (NaN), positive infinity and negative infinity
7487 evaluate to True because these are not equal to zero.
7488
7489 Examples
7490 --------
7491 >>> index = pd.Index([0, 1, 2])
7492 >>> index.any()
7493 True
7494
7495 >>> index = pd.Index([0, 0, 0])
7496 >>> index.any()
7497 False
7498 """
7499 nv.validate_any(args, kwargs)
7500 self._maybe_disable_logical_methods("any")
7501 vals = self._values
7502 if not isinstance(vals, np.ndarray):
7503 # i.e. EA, call _reduce instead of "any" to get TypeError instead
7504 # of AttributeError
7505 return vals._reduce("any")
7506 return maybe_unbox_numpy_scalar(np.any(vals))
7507
7508 def all(self, *args, **kwargs):
7509 """
7510 Return whether all elements are Truthy.
7511
7512 Parameters
7513 ----------
7514 *args
7515 Required for compatibility with numpy.
7516 **kwargs
7517 Required for compatibility with numpy.
7518
7519 Returns
7520 -------
7521 bool or array-like (if axis is specified)
7522 A single element array-like may be converted to bool.
7523
7524 See Also
7525 --------
7526 Index.any : Return whether any element in an Index is True.
7527 Series.any : Return whether any element in a Series is True.
7528 Series.all : Return whether all elements in a Series are True.
7529
7530 Notes
7531 -----
7532 Not a Number (NaN), positive infinity and negative infinity
7533 evaluate to True because these are not equal to zero.
7534
7535 Examples
7536 --------
7537 True, because nonzero integers are considered True.
7538
7539 >>> pd.Index([1, 2, 3]).all()
7540 True
7541
7542 False, because ``0`` is considered False.
7543
7544 >>> pd.Index([0, 1, 2]).all()
7545 False
7546 """
7547 nv.validate_all(args, kwargs)
7548 self._maybe_disable_logical_methods("all")
7549 vals = self._values
7550 if not isinstance(vals, np.ndarray):
7551 # i.e. EA, call _reduce instead of "all" to get TypeError instead
7552 # of AttributeError
7553 return vals._reduce("all")
7554 return maybe_unbox_numpy_scalar(np.all(vals))
7555
7556 @final
7557 def _maybe_disable_logical_methods(self, opname: str_t) -> None:
7558 """
7559 raise if this Index subclass does not support any or all.
7560 """
7561 if isinstance(self, ABCMultiIndex):
7562 raise TypeError(f"cannot perform {opname} with {type(self).__name__}")
7563
7564 def argmin(
7565 self, axis: AxisInt | None = None, skipna: bool = True, *args, **kwargs
7566 ) -> int:
7567 """
7568 Return int position of the smallest value in the Index.
7569
7570 If the minimum is achieved in multiple locations,
7571 the first row position is returned.
7572
7573 Parameters
7574 ----------
7575 axis : None
7576 Unused. Parameter needed for compatibility with DataFrame.
7577 skipna : bool, default True
7578 Exclude NA/null values. If the entire Series is NA, or if ``skipna=False``
7579 and there is an NA value, this method will raise a ``ValueError``.
7580 *args, **kwargs
7581 Additional arguments and keywords for compatibility with NumPy.
7582
7583 Returns
7584 -------
7585 int
7586 Row position of the minimum value.
7587
7588 See Also
7589 --------
7590 Series.argmin : Return position of the minimum value.
7591 Series.argmax : Return position of the maximum value.
7592 numpy.ndarray.argmin : Equivalent method for numpy arrays.
7593 Series.idxmin : Return index label of the minimum values.
7594 Series.idxmax : Return index label of the maximum values.
7595
7596 Examples
7597 --------
7598 Consider dataset containing cereal calories
7599
7600 >>> idx = pd.Index([100.0, 110.0, 120.0, 110.0])
7601 >>> idx
7602 Index([100.0, 110.0, 120.0, 110.0], dtype='float64')
7603
7604 >>> idx.argmax()
7605 np.int64(2)
7606 >>> idx.argmin()
7607 np.int64(0)
7608
7609 The maximum cereal calories is the third element and
7610 the minimum cereal calories is the first element,
7611 since index is zero-indexed.
7612 """
7613 nv.validate_argmin(args, kwargs)
7614 nv.validate_minmax_axis(axis)
7615
7616 if not self._is_multi and self.hasnans:
7617 if not skipna:
7618 raise ValueError("Encountered an NA value with skipna=False")
7619 elif self._isnan.all():
7620 raise ValueError("Encountered all NA values")
7621
7622 return super().argmin(skipna=skipna)
7623
7624 def argmax(
7625 self, axis: AxisInt | None = None, skipna: bool = True, *args, **kwargs
7626 ) -> int:
7627 """
7628 Return int position of the largest value in the Index.
7629
7630 If the maximum is achieved in multiple locations,
7631 the first row position is returned.
7632
7633 Parameters
7634 ----------
7635 axis : None
7636 Unused. Parameter needed for compatibility with DataFrame.
7637 skipna : bool, default True
7638 Exclude NA/null values. If the entire Series is NA, or if ``skipna=False``
7639 and there is an NA value, this method will raise a ``ValueError``.
7640 *args, **kwargs
7641 Additional arguments and keywords for compatibility with NumPy.
7642
7643 Returns
7644 -------
7645 int
7646 Row position of the maximum value.
7647
7648 See Also
7649 --------
7650 Series.argmax : Return position of the maximum value.
7651 Series.argmin : Return position of the minimum value.
7652 numpy.ndarray.argmax : Equivalent method for numpy arrays.
7653 Series.idxmax : Return index label of the maximum values.
7654 Series.idxmin : Return index label of the minimum values.
7655
7656 Examples
7657 --------
7658 Consider dataset containing cereal calories
7659
7660 >>> idx = pd.Index([100.0, 110.0, 120.0, 110.0])
7661 >>> idx
7662 Index([100.0, 110.0, 120.0, 110.0], dtype='float64')
7663
7664 >>> idx.argmax()
7665 np.int64(2)
7666 >>> idx.argmin()
7667 np.int64(0)
7668
7669 The maximum cereal calories is the third element and
7670 the minimum cereal calories is the first element,
7671 since index is zero-indexed.
7672 """
7673 nv.validate_argmax(args, kwargs)
7674 nv.validate_minmax_axis(axis)
7675
7676 if not self._is_multi and self.hasnans:
7677 if not skipna:
7678 raise ValueError("Encountered an NA value with skipna=False")
7679 elif self._isnan.all():
7680 raise ValueError("Encountered all NA values")
7681 return super().argmax(skipna=skipna)
7682
7683 def min(self, axis: AxisInt | None = None, skipna: bool = True, *args, **kwargs):
7684 """
7685 Return the minimum value of the Index.
7686
7687 Parameters
7688 ----------
7689 axis : {None}
7690 Dummy argument for consistency with Series.
7691 skipna : bool, default True
7692 Exclude NA/null values when showing the result.
7693 *args, **kwargs
7694 Additional arguments and keywords for compatibility with NumPy.
7695
7696 Returns
7697 -------
7698 scalar
7699 Minimum value.
7700
7701 See Also
7702 --------
7703 Index.max : Return the maximum value of the object.
7704 Series.min : Return the minimum value in a Series.
7705 DataFrame.min : Return the minimum values in a DataFrame.
7706
7707 Examples
7708 --------
7709 >>> idx = pd.Index([3, 2, 1])
7710 >>> idx.min()
7711 1
7712
7713 >>> idx = pd.Index(["c", "b", "a"])
7714 >>> idx.min()
7715 'a'
7716
7717 For a MultiIndex, the minimum is determined lexicographically.
7718
7719 >>> idx = pd.MultiIndex.from_product([("a", "b"), (2, 1)])
7720 >>> idx.min()
7721 ('a', 1)
7722 """
7723 nv.validate_min(args, kwargs)
7724 nv.validate_minmax_axis(axis)
7725
7726 if not len(self):
7727 return self._na_value
7728
7729 if len(self) and self.is_monotonic_increasing:
7730 # quick check
7731 first = self[0]
7732 if not isna(first):
7733 return maybe_unbox_numpy_scalar(first)
7734
7735 if not self._is_multi and self.hasnans:
7736 # Take advantage of cache
7737 mask = self._isnan
7738 if not skipna or mask.all():
7739 return self._na_value
7740
7741 if not self._is_multi and not isinstance(self._values, np.ndarray):
7742 return self._values._reduce(name="min", skipna=skipna)
7743
7744 return maybe_unbox_numpy_scalar(nanops.nanmin(self._values, skipna=skipna))
7745
7746 def max(self, axis: AxisInt | None = None, skipna: bool = True, *args, **kwargs):
7747 """
7748 Return the maximum value of the Index.
7749
7750 Parameters
7751 ----------
7752 axis : int, optional
7753 For compatibility with NumPy. Only 0 or None are allowed.
7754 skipna : bool, default True
7755 Exclude NA/null values when showing the result.
7756 *args, **kwargs
7757 Additional arguments and keywords for compatibility with NumPy.
7758
7759 Returns
7760 -------
7761 scalar
7762 Maximum value.
7763
7764 See Also
7765 --------
7766 Index.min : Return the minimum value in an Index.
7767 Series.max : Return the maximum value in a Series.
7768 DataFrame.max : Return the maximum values in a DataFrame.
7769
7770 Examples
7771 --------
7772 >>> idx = pd.Index([3, 2, 1])
7773 >>> idx.max()
7774 3
7775
7776 >>> idx = pd.Index(["c", "b", "a"])
7777 >>> idx.max()
7778 'c'
7779
7780 For a MultiIndex, the maximum is determined lexicographically.
7781
7782 >>> idx = pd.MultiIndex.from_product([("a", "b"), (2, 1)])
7783 >>> idx.max()
7784 ('b', 2)
7785 """
7786
7787 nv.validate_max(args, kwargs)
7788 nv.validate_minmax_axis(axis)
7789
7790 if not len(self):
7791 return self._na_value
7792
7793 if len(self) and self.is_monotonic_increasing:
7794 # quick check
7795 last = self[-1]
7796 if not isna(last):
7797 return maybe_unbox_numpy_scalar(last)
7798
7799 if not self._is_multi and self.hasnans:
7800 # Take advantage of cache
7801 mask = self._isnan
7802 if not skipna or mask.all():
7803 return maybe_unbox_numpy_scalar(self._na_value)
7804
7805 if not self._is_multi and not isinstance(self._values, np.ndarray):
7806 return self._values._reduce(name="max", skipna=skipna)
7807
7808 return maybe_unbox_numpy_scalar(nanops.nanmax(self._values, skipna=skipna))
7809
7810 # --------------------------------------------------------------------
7811
7812 @final
7813 @property
7814 def shape(self) -> Shape:
7815 """
7816 Return a tuple of the shape of the underlying data.
7817
7818 See Also
7819 --------
7820 Index.size: Return the number of elements in the underlying data.
7821 Index.ndim: Number of dimensions of the underlying data, by definition 1.
7822 Index.dtype: Return the dtype object of the underlying data.
7823 Index.values: Return an array representing the data in the Index.
7824
7825 Examples
7826 --------
7827 >>> idx = pd.Index([1, 2, 3])
7828 >>> idx
7829 Index([1, 2, 3], dtype='int64')
7830 >>> idx.shape
7831 (3,)
7832 """
7833 # See GH#27775, GH#27384 for history/reasoning in how this is defined.
7834 return (len(self),)
7835
7836
7837def maybe_sequence_to_range(sequence) -> Any | range:
7838 """
7839 Convert a 1D, non-pandas sequence to a range if possible.
7840
7841 Returns the input if not possible.
7842
7843 Parameters
7844 ----------
7845 sequence : 1D sequence
7846 names : sequence of str
7847
7848 Returns
7849 -------
7850 Any : input or range
7851 """
7852 if isinstance(sequence, (range, ExtensionArray)):
7853 return sequence
7854 elif len(sequence) == 1 or lib.infer_dtype(sequence, skipna=False) != "integer":
7855 return sequence
7856 elif isinstance(sequence, (ABCSeries, Index)) and not (
7857 isinstance(sequence.dtype, np.dtype) and sequence.dtype.kind == "i"
7858 ):
7859 return sequence
7860 if len(sequence) == 0:
7861 return range(0)
7862 try:
7863 np_sequence = np.asarray(sequence, dtype=np.int64)
7864 except OverflowError:
7865 return sequence
7866 diff = np_sequence[1] - np_sequence[0]
7867 if diff == 0:
7868 return sequence
7869 elif len(sequence) == 2 or lib.is_sequence_range(np_sequence, diff):
7870 return range(np_sequence[0], np_sequence[-1] + diff, diff)
7871 else:
7872 return sequence
7873
7874
7875def ensure_index_from_sequences(sequences, names=None) -> Index:
7876 """
7877 Construct an index from sequences of data.
7878
7879 A single sequence returns an Index. Many sequences returns a
7880 MultiIndex.
7881
7882 Parameters
7883 ----------
7884 sequences : sequence of sequences
7885 names : sequence of str
7886
7887 Returns
7888 -------
7889 index : Index or MultiIndex
7890
7891 Examples
7892 --------
7893 >>> ensure_index_from_sequences([[1, 2, 4]], names=["name"])
7894 Index([1, 2, 4], dtype='int64', name='name')
7895
7896 >>> ensure_index_from_sequences([["a", "a"], ["a", "b"]], names=["L1", "L2"])
7897 MultiIndex([('a', 'a'),
7898 ('a', 'b')],
7899 names=['L1', 'L2'])
7900
7901 See Also
7902 --------
7903 ensure_index
7904 """
7905 from pandas.core.indexes.api import default_index
7906 from pandas.core.indexes.multi import MultiIndex
7907
7908 if len(sequences) == 0:
7909 return default_index(0)
7910 elif len(sequences) == 1:
7911 if names is not None:
7912 names = names[0]
7913 return Index(maybe_sequence_to_range(sequences[0]), name=names)
7914 else:
7915 # TODO: Apply maybe_sequence_to_range to sequences?
7916 return MultiIndex.from_arrays(sequences, names=names)
7917
7918
7919def ensure_index(index_like: Axes, copy: bool = False) -> Index:
7920 """
7921 Ensure that we have an index from some index-like object.
7922
7923 Parameters
7924 ----------
7925 index_like : sequence
7926 An Index or other sequence
7927 copy : bool, default False
7928
7929 Returns
7930 -------
7931 index : Index or MultiIndex
7932
7933 See Also
7934 --------
7935 ensure_index_from_sequences
7936
7937 Examples
7938 --------
7939 >>> ensure_index(["a", "b"])
7940 Index(['a', 'b'], dtype='str')
7941
7942 >>> ensure_index([("a", "a"), ("b", "c")])
7943 Index([('a', 'a'), ('b', 'c')], dtype='object')
7944
7945 >>> ensure_index([["a", "a"], ["b", "c"]])
7946 MultiIndex([('a', 'b'),
7947 ('a', 'c')],
7948 )
7949 """
7950 if isinstance(index_like, Index):
7951 if copy:
7952 index_like = index_like.copy()
7953 return index_like
7954
7955 if isinstance(index_like, ABCSeries):
7956 name = index_like.name
7957 return Index(index_like, name=name, copy=copy)
7958
7959 if is_iterator(index_like):
7960 index_like = list(index_like)
7961
7962 if isinstance(index_like, list):
7963 if type(index_like) is not list:
7964 # must check for exactly list here because of strict type
7965 # check in clean_index_list
7966 index_like = list(index_like)
7967
7968 if index_like and lib.is_all_arraylike(index_like):
7969 from pandas.core.indexes.multi import MultiIndex
7970
7971 return MultiIndex.from_arrays(index_like)
7972 else:
7973 return Index(index_like, copy=copy, tupleize_cols=False)
7974 else:
7975 return Index(index_like, copy=copy)
7976
7977
7978def trim_front(strings: list[str]) -> list[str]:
7979 """
7980 Trims leading spaces evenly among all strings.
7981
7982 Examples
7983 --------
7984 >>> trim_front([" a", " b"])
7985 ['a', 'b']
7986
7987 >>> trim_front([" a", " "])
7988 ['a', '']
7989 """
7990 if not strings:
7991 return strings
7992 smallest_leading_space = min(len(x) - len(x.lstrip()) for x in strings)
7993 if smallest_leading_space > 0:
7994 strings = [x[smallest_leading_space:] for x in strings]
7995 return strings
7996
7997
7998def _validate_join_method(method: str) -> None:
7999 if method not in ["left", "right", "inner", "outer"]:
8000 raise ValueError(f"do not recognize join method {method}")
8001
8002
8003def maybe_extract_name(name, obj, cls) -> Hashable:
8004 """
8005 If no name is passed, then extract it from data, validating hashability.
8006 """
8007 if name is None and isinstance(obj, (Index, ABCSeries)):
8008 # Note we don't just check for "name" attribute since that would
8009 # pick up e.g. dtype.name
8010 name = obj.name
8011
8012 # GH#29069
8013 if not is_hashable(name):
8014 raise TypeError(f"{cls.__name__}.name must be a hashable type")
8015
8016 return name
8017
8018
8019def get_unanimous_names(*indexes: Index) -> tuple[Hashable, ...]:
8020 """
8021 Return common name if all indices agree, otherwise None (level-by-level).
8022
8023 Parameters
8024 ----------
8025 indexes : list of Index objects
8026
8027 Returns
8028 -------
8029 list
8030 A list representing the unanimous 'names' found.
8031 """
8032 name_tups = (tuple(i.names) for i in indexes)
8033 name_sets = ({*ns} for ns in zip_longest(*name_tups))
8034 names = tuple(ns.pop() if len(ns) == 1 else None for ns in name_sets)
8035 return names
8036
8037
8038def _unpack_nested_dtype(other: Index) -> DtypeObj:
8039 """
8040 When checking if our dtype is comparable with another, we need
8041 to unpack CategoricalDtype to look at its categories.dtype.
8042
8043 Parameters
8044 ----------
8045 other : Index
8046
8047 Returns
8048 -------
8049 np.dtype or ExtensionDtype
8050 """
8051 dtype = other.dtype
8052 if isinstance(dtype, CategoricalDtype):
8053 # If there is ever a SparseIndex, this could get dispatched
8054 # here too.
8055 return dtype.categories.dtype
8056 elif isinstance(dtype, ArrowDtype):
8057 # GH 53617
8058 import pyarrow as pa
8059
8060 if pa.types.is_dictionary(dtype.pyarrow_dtype):
8061 other = other[:0].astype(ArrowDtype(dtype.pyarrow_dtype.value_type))
8062 return other.dtype
8063
8064
8065def _maybe_try_sort(result: Index | ArrayLike, sort: bool | None):
8066 if sort is not False:
8067 try:
8068 # error: Incompatible types in assignment (expression has type
8069 # "Union[ExtensionArray, ndarray[Any, Any], Index, Series,
8070 # Tuple[Union[Union[ExtensionArray, ndarray[Any, Any]], Index, Series],
8071 # ndarray[Any, Any]]]", variable has type "Union[Index,
8072 # Union[ExtensionArray, ndarray[Any, Any]]]")
8073 result = algos.safe_sort(result) # type: ignore[assignment]
8074 except TypeError as err:
8075 if sort is True:
8076 raise
8077 warnings.warn(
8078 f"{err}, sort order is undefined for incomparable objects.",
8079 RuntimeWarning,
8080 stacklevel=find_stack_level(),
8081 )
8082 return result
8083
8084
8085def get_values_for_csv(
8086 values: ArrayLike,
8087 *,
8088 date_format,
8089 na_rep: str = "nan",
8090 quoting=None,
8091 float_format=None,
8092 decimal: str = ".",
8093) -> npt.NDArray[np.object_]:
8094 """
8095 Convert to types which can be consumed by the standard library's
8096 csv.writer.writerows.
8097 """
8098 if isinstance(values, Categorical) and values.categories.dtype.kind in "Mm":
8099 # GH#40754 Convert categorical datetimes to datetime array
8100 values = algos.take_nd(
8101 values.categories._values,
8102 ensure_platform_int(values._codes),
8103 fill_value=na_rep,
8104 )
8105
8106 values = ensure_wrapped_if_datetimelike(values)
8107
8108 if isinstance(values, (DatetimeArray, TimedeltaArray)):
8109 if values.ndim == 1:
8110 result = values._format_native_types(na_rep=na_rep, date_format=date_format)
8111 result = result.astype(object, copy=False)
8112 return result
8113
8114 # GH#21734 Process every column separately, they might have different formats
8115 results_converted = []
8116 for i in range(len(values)):
8117 result = values[i, :]._format_native_types(
8118 na_rep=na_rep, date_format=date_format
8119 )
8120 results_converted.append(result.astype(object, copy=False))
8121 return np.vstack(results_converted)
8122
8123 elif isinstance(values.dtype, PeriodDtype):
8124 # TODO: tests that get here in column path
8125 values = cast("PeriodArray", values)
8126 res = values._format_native_types(na_rep=na_rep, date_format=date_format)
8127 return res
8128
8129 elif isinstance(values.dtype, IntervalDtype):
8130 # TODO: tests that get here in column path
8131 values = cast("IntervalArray", values)
8132 mask = values.isna()
8133 if not quoting:
8134 result = np.asarray(values).astype(str)
8135 else:
8136 result = np.array(values, dtype=object, copy=True)
8137
8138 result[mask] = na_rep
8139 return result
8140
8141 elif values.dtype.kind == "f" and not isinstance(values.dtype, SparseDtype):
8142 # see GH#13418: no special formatting is desired at the
8143 # output (important for appropriate 'quoting' behaviour),
8144 # so do not pass it through the FloatArrayFormatter
8145 if float_format is None and decimal == ".":
8146 mask = isna(values)
8147
8148 if not quoting:
8149 values = values.astype(str)
8150 else:
8151 values = np.array(values, dtype="object")
8152
8153 values[mask] = na_rep
8154 values = values.astype(object, copy=False)
8155 return values
8156
8157 from pandas.io.formats.format import FloatArrayFormatter
8158
8159 formatter = FloatArrayFormatter(
8160 values,
8161 na_rep=na_rep,
8162 float_format=float_format,
8163 decimal=decimal,
8164 quoting=quoting,
8165 fixed_width=False,
8166 )
8167 res = formatter.get_result_as_array()
8168 res = res.astype(object, copy=False)
8169 return res
8170
8171 elif isinstance(values, ExtensionArray):
8172 mask = isna(values)
8173
8174 new_values = np.asarray(values.astype(object))
8175 new_values[mask] = na_rep
8176 return new_values
8177
8178 else:
8179 mask = isna(values)
8180 itemsize = writers.word_len(na_rep)
8181
8182 if values.dtype != _dtype_obj and not quoting and itemsize:
8183 values = values.astype(str)
8184 if values.dtype.itemsize / np.dtype("U1").itemsize < itemsize:
8185 # enlarge for the na_rep
8186 values = values.astype(f"<U{itemsize}")
8187 else:
8188 values = np.array(values, dtype="object")
8189
8190 values[mask] = na_rep
8191 values = values.astype(object, copy=False)
8192 return values