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