1"""define the IntervalIndex"""
2
3from __future__ import annotations
4
5from operator import (
6 le,
7 lt,
8)
9from typing import (
10 TYPE_CHECKING,
11 Any,
12 Literal,
13 Self,
14)
15
16import numpy as np
17
18from pandas._libs import lib
19from pandas._libs.interval import (
20 Interval,
21 IntervalMixin,
22 IntervalTree,
23)
24from pandas._libs.tslibs import (
25 BaseOffset,
26 Period,
27 Timedelta,
28 Timestamp,
29 to_offset,
30)
31from pandas.errors import InvalidIndexError
32from pandas.util._decorators import (
33 cache_readonly,
34 set_module,
35)
36from pandas.util._exceptions import rewrite_exception
37
38from pandas.core.dtypes.cast import (
39 find_common_type,
40 infer_dtype_from_scalar,
41 maybe_box_datetimelike,
42 maybe_downcast_numeric,
43 maybe_unbox_numpy_scalar,
44 maybe_upcast_numeric_to_64bit,
45)
46from pandas.core.dtypes.common import (
47 ensure_platform_int,
48 is_float_dtype,
49 is_integer,
50 is_integer_dtype,
51 is_list_like,
52 is_number,
53 is_object_dtype,
54 is_scalar,
55 is_string_dtype,
56 pandas_dtype,
57)
58from pandas.core.dtypes.dtypes import (
59 DatetimeTZDtype,
60 IntervalDtype,
61)
62from pandas.core.dtypes.missing import is_valid_na_for_dtype
63
64from pandas.core.algorithms import unique
65from pandas.core.arrays.datetimelike import validate_periods
66from pandas.core.arrays.interval import (
67 IntervalArray,
68)
69import pandas.core.common as com
70from pandas.core.indexers import is_valid_positional_slice
71from pandas.core.indexes.base import (
72 Index,
73 ensure_index,
74 maybe_extract_name,
75)
76from pandas.core.indexes.datetimes import (
77 DatetimeIndex,
78 date_range,
79)
80from pandas.core.indexes.extension import (
81 ExtensionIndex,
82 inherit_names,
83)
84from pandas.core.indexes.multi import MultiIndex
85from pandas.core.indexes.timedeltas import (
86 TimedeltaIndex,
87 timedelta_range,
88)
89
90if TYPE_CHECKING:
91 from collections.abc import Hashable
92
93 from pandas._typing import (
94 Dtype,
95 DtypeObj,
96 IntervalClosedType,
97 npt,
98 )
99
100
101def _get_next_label(label):
102 # see test_slice_locs_with_ints_and_floats_succeeds
103 dtype = getattr(label, "dtype", type(label))
104 if isinstance(label, (Timestamp, Timedelta)):
105 dtype = "datetime64[ns]"
106 dtype = pandas_dtype(dtype)
107
108 if lib.is_np_dtype(dtype, "mM") or isinstance(dtype, DatetimeTZDtype):
109 return label + np.timedelta64(1, "ns")
110 elif is_integer_dtype(dtype):
111 return label + 1
112 elif is_float_dtype(dtype):
113 return np.nextafter(label, np.inf)
114 else:
115 raise TypeError(f"cannot determine next label for type {type(label)!r}")
116
117
118def _get_prev_label(label):
119 # see test_slice_locs_with_ints_and_floats_succeeds
120 dtype = getattr(label, "dtype", type(label))
121 if isinstance(label, (Timestamp, Timedelta)):
122 dtype = "datetime64[ns]"
123 dtype = pandas_dtype(dtype)
124
125 if lib.is_np_dtype(dtype, "mM") or isinstance(dtype, DatetimeTZDtype):
126 return label - np.timedelta64(1, "ns")
127 elif is_integer_dtype(dtype):
128 return label - 1
129 elif is_float_dtype(dtype):
130 return np.nextafter(label, -np.inf)
131 else:
132 raise TypeError(f"cannot determine next label for type {type(label)!r}")
133
134
135def _new_IntervalIndex(cls, d):
136 """
137 This is called upon unpickling, rather than the default which doesn't have
138 arguments and breaks __new__.
139 """
140 return cls.from_arrays(**d)
141
142
143@inherit_names(["set_closed", "to_tuples"], IntervalArray, wrap=True)
144@inherit_names(
145 [
146 "__array__",
147 "overlaps",
148 "contains",
149 "closed_left",
150 "closed_right",
151 "open_left",
152 "open_right",
153 "is_empty",
154 ],
155 IntervalArray,
156)
157@inherit_names(["is_non_overlapping_monotonic", "closed"], IntervalArray, cache=True)
158@set_module("pandas")
159class IntervalIndex(ExtensionIndex):
160 """
161 Immutable index of intervals that are closed on the same side.
162
163 Parameters
164 ----------
165 data : array-like (1-dimensional)
166 Array-like (ndarray, :class:`DateTimeArray`, :class:`TimeDeltaArray`) containing
167 Interval objects from which to build the IntervalIndex.
168 closed : {'left', 'right', 'both', 'neither'}, default 'right'
169 Whether the intervals are closed on the left-side, right-side, both or
170 neither.
171 dtype : dtype or None, default None
172 If None, dtype will be inferred.
173 copy : bool, default None
174 Whether to copy input data, only relevant for array, Series, and Index
175 inputs (for other input, e.g. a list, a new array is created anyway).
176 Defaults to True for array input and False for Index/Series.
177 Set to False to avoid copying array input at your own risk (if you
178 know the input data won't be modified elsewhere).
179 Set to True to force copying Series/Index input up front.
180 name : object, optional
181 Name to be stored in the index.
182 verify_integrity : bool, default True
183 Verify that the IntervalIndex is valid.
184
185 Attributes
186 ----------
187 left
188 right
189 closed
190 mid
191 length
192 is_empty
193 is_non_overlapping_monotonic
194 is_overlapping
195 values
196
197 Methods
198 -------
199 from_arrays
200 from_tuples
201 from_breaks
202 contains
203 overlaps
204 set_closed
205 to_tuples
206
207 See Also
208 --------
209 Index : The base pandas Index type.
210 Interval : A bounded slice-like interval; the elements of an IntervalIndex.
211 interval_range : Function to create a fixed frequency IntervalIndex.
212 cut : Bin values into discrete Intervals.
213 qcut : Bin values into equal-sized Intervals based on rank or sample quantiles.
214
215 Notes
216 -----
217 See the `user guide
218 <https://pandas.pydata.org/pandas-docs/stable/user_guide/advanced.html#intervalindex>`__
219 for more.
220
221 Examples
222 --------
223 A new ``IntervalIndex`` is typically constructed using
224 :func:`interval_range`:
225
226 >>> pd.interval_range(start=0, end=5)
227 IntervalIndex([(0, 1], (1, 2], (2, 3], (3, 4], (4, 5]],
228 dtype='interval[int64, right]')
229
230 It may also be constructed using one of the constructor
231 methods: :meth:`IntervalIndex.from_arrays`,
232 :meth:`IntervalIndex.from_breaks`, and :meth:`IntervalIndex.from_tuples`.
233
234 See further examples in the doc strings of ``interval_range`` and the
235 mentioned constructor methods.
236 """
237
238 _typ = "intervalindex"
239
240 # annotate properties pinned via inherit_names
241 closed: IntervalClosedType
242 is_non_overlapping_monotonic: bool
243 closed_left: bool
244 closed_right: bool
245 open_left: bool
246 open_right: bool
247
248 _data: IntervalArray
249 _values: IntervalArray
250 _can_hold_strings = False
251 _data_cls = IntervalArray
252
253 # --------------------------------------------------------------------
254 # Constructors
255
256 def __new__(
257 cls,
258 data,
259 closed: IntervalClosedType | None = None,
260 dtype: Dtype | None = None,
261 copy: bool | None = None,
262 name: Hashable | None = None,
263 verify_integrity: bool = True,
264 ) -> Self:
265 name = maybe_extract_name(name, data, cls)
266
267 # GH#63388
268 data, copy = cls._maybe_copy_array_input(data, copy, dtype)
269
270 with rewrite_exception("IntervalArray", cls.__name__):
271 array = IntervalArray(
272 data,
273 closed=closed,
274 copy=copy,
275 dtype=dtype,
276 verify_integrity=verify_integrity,
277 )
278
279 return cls._simple_new(array, name)
280
281 @classmethod
282 def from_breaks(
283 cls,
284 breaks,
285 closed: IntervalClosedType | None = "right",
286 name: Hashable | None = None,
287 copy: bool = False,
288 dtype: Dtype | None = None,
289 ) -> IntervalIndex:
290 """
291 Construct an IntervalIndex from an array of splits.
292
293 Parameters
294 ----------
295 breaks : array-like (1-dimensional)
296 Left and right bounds for each interval.
297 closed : {'left', 'right', 'both', 'neither'}, default 'right'
298 Whether the intervals are closed on the left-side, right-side, both
299 or neither.
300 name : str, optional
301 Name of the resulting IntervalIndex.
302 copy : bool, default False
303 Copy the data.
304 dtype : dtype or None, default None
305 If None, dtype will be inferred.
306
307 Returns
308 -------
309 IntervalIndex
310
311 See Also
312 --------
313 interval_range : Function to create a fixed frequency IntervalIndex.
314 IntervalIndex.from_arrays : Construct from a left and right array.
315 IntervalIndex.from_tuples : Construct from a sequence of tuples.
316
317 Examples
318 --------
319 >>> pd.IntervalIndex.from_breaks([0, 1, 2, 3])
320 IntervalIndex([(0, 1], (1, 2], (2, 3]],
321 dtype='interval[int64, right]')
322 """
323 with rewrite_exception("IntervalArray", cls.__name__):
324 array = IntervalArray.from_breaks(
325 breaks, closed=closed, copy=copy, dtype=dtype
326 )
327 return cls._simple_new(array, name=name)
328
329 @classmethod
330 def from_arrays(
331 cls,
332 left,
333 right,
334 closed: IntervalClosedType = "right",
335 name: Hashable | None = None,
336 copy: bool = False,
337 dtype: Dtype | None = None,
338 ) -> IntervalIndex:
339 """
340 Construct from two arrays defining the left and right bounds.
341
342 Parameters
343 ----------
344 left : array-like (1-dimensional)
345 Left bounds for each interval.
346 right : array-like (1-dimensional)
347 Right bounds for each interval.
348 closed : {'left', 'right', 'both', 'neither'}, default 'right'
349 Whether the intervals are closed on the left-side, right-side, both
350 or neither.
351 name : str, optional
352 Name of the resulting IntervalIndex.
353 copy : bool, default False
354 Copy the data.
355 dtype : dtype, optional
356 If None, dtype will be inferred.
357
358 Returns
359 -------
360 IntervalIndex
361
362 Raises
363 ------
364 ValueError
365 When a value is missing in only one of `left` or `right`.
366 When a value in `left` is greater than the corresponding value
367 in `right`.
368
369 See Also
370 --------
371 interval_range : Function to create a fixed frequency IntervalIndex.
372 IntervalIndex.from_breaks : Construct an IntervalIndex from an array of
373 splits.
374 IntervalIndex.from_tuples : Construct an IntervalIndex from an
375 array-like of tuples.
376
377 Notes
378 -----
379 Each element of `left` must be less than or equal to the `right`
380 element at the same position. If an element is missing, it must be
381 missing in both `left` and `right`. A TypeError is raised when
382 using an unsupported type for `left` or `right`. At the moment,
383 'category', 'object', and 'string' subtypes are not supported.
384
385 Examples
386 --------
387 >>> pd.IntervalIndex.from_arrays([0, 1, 2], [1, 2, 3])
388 IntervalIndex([(0, 1], (1, 2], (2, 3]],
389 dtype='interval[int64, right]')
390 """
391 with rewrite_exception("IntervalArray", cls.__name__):
392 array = IntervalArray.from_arrays(
393 left, right, closed, copy=copy, dtype=dtype
394 )
395 return cls._simple_new(array, name=name)
396
397 @classmethod
398 def from_tuples(
399 cls,
400 data,
401 closed: IntervalClosedType = "right",
402 name: Hashable | None = None,
403 copy: bool = False,
404 dtype: Dtype | None = None,
405 ) -> IntervalIndex:
406 """
407 Construct an IntervalIndex from an array-like of tuples.
408
409 Parameters
410 ----------
411 data : array-like (1-dimensional)
412 Array of tuples.
413 closed : {'left', 'right', 'both', 'neither'}, default 'right'
414 Whether the intervals are closed on the left-side, right-side, both
415 or neither.
416 name : str, optional
417 Name of the resulting IntervalIndex.
418 copy : bool, default False
419 By-default copy the data, this is compat only and ignored.
420 dtype : dtype or None, default None
421 If None, dtype will be inferred.
422
423 Returns
424 -------
425 IntervalIndex
426
427 See Also
428 --------
429 interval_range : Function to create a fixed frequency IntervalIndex.
430 IntervalIndex.from_arrays : Construct an IntervalIndex from a left and
431 right array.
432 IntervalIndex.from_breaks : Construct an IntervalIndex from an array of
433 splits.
434
435 Examples
436 --------
437 >>> pd.IntervalIndex.from_tuples([(0, 1), (1, 2)])
438 IntervalIndex([(0, 1], (1, 2]],
439 dtype='interval[int64, right]')
440 """
441 with rewrite_exception("IntervalArray", cls.__name__):
442 arr = IntervalArray.from_tuples(data, closed=closed, copy=copy, dtype=dtype)
443 return cls._simple_new(arr, name=name)
444
445 # --------------------------------------------------------------------
446 # error: Return type "IntervalTree" of "_engine" incompatible with return type
447 # "Union[IndexEngine, ExtensionEngine]" in supertype "Index"
448 @cache_readonly
449 def _engine(self) -> IntervalTree: # type: ignore[override]
450 # IntervalTree does not supports numpy array unless they are 64 bit
451 left = self._maybe_convert_i8(self.left)
452 left = maybe_upcast_numeric_to_64bit(left)
453 right = self._maybe_convert_i8(self.right)
454 right = maybe_upcast_numeric_to_64bit(right)
455 return IntervalTree(left, right, closed=self.closed)
456
457 def __contains__(self, key: Any) -> bool:
458 """
459 return a boolean if this key is IN the index
460 We *only* accept an Interval
461
462 Parameters
463 ----------
464 key : Interval
465
466 Returns
467 -------
468 bool
469 """
470 hash(key)
471 if not isinstance(key, Interval):
472 if is_valid_na_for_dtype(key, self.dtype):
473 return self.hasnans
474 return False
475
476 try:
477 self.get_loc(key)
478 return True
479 except KeyError:
480 return False
481
482 def _getitem_slice(self, slobj: slice) -> IntervalIndex:
483 """
484 Fastpath for __getitem__ when we know we have a slice.
485 """
486 res = self._data[slobj]
487 return type(self)._simple_new(res, name=self._name)
488
489 @cache_readonly
490 def _multiindex(self) -> MultiIndex:
491 return MultiIndex.from_arrays([self.left, self.right], names=["left", "right"])
492
493 def __reduce__(self):
494 d = {
495 "left": self.left,
496 "right": self.right,
497 "closed": self.closed,
498 "name": self.name,
499 }
500 return _new_IntervalIndex, (type(self), d), None
501
502 @property
503 def inferred_type(self) -> str:
504 """Return a string of the type inferred from the values"""
505 return "interval"
506
507 def memory_usage(self, deep: bool = False) -> int:
508 """
509 Memory usage of the values.
510
511 Parameters
512 ----------
513 deep : bool, default False
514 Introspect the data deeply, interrogate
515 `object` dtypes for system-level memory consumption.
516
517 Returns
518 -------
519 bytes used
520 Returns memory usage of the values in the Index in bytes.
521
522 See Also
523 --------
524 numpy.ndarray.nbytes : Total bytes consumed by the elements of the
525 array.
526
527 Notes
528 -----
529 Memory usage does not include memory consumed by elements that
530 are not components of the array if deep=False or if used on PyPy
531
532 Examples
533 --------
534 >>> idx = pd.Index([1, 2, 3])
535 >>> idx.memory_usage()
536 24
537 """
538 # we don't use an explicit engine
539 # so return the bytes here
540 return self.left.memory_usage(deep=deep) + self.right.memory_usage(deep=deep)
541
542 # IntervalTree doesn't have a is_monotonic_decreasing, so have to override
543 # the Index implementation
544 @cache_readonly
545 def is_monotonic_decreasing(self) -> bool:
546 """
547 Return True if the IntervalIndex is monotonic decreasing (only equal or
548 decreasing values), else False
549 """
550 return self[::-1].is_monotonic_increasing
551
552 @cache_readonly
553 def is_unique(self) -> bool:
554 """
555 Return True if the IntervalIndex contains unique elements, else False.
556 """
557 left = self.left
558 right = self.right
559
560 if self.isna().sum() > 1:
561 return False
562
563 if left.is_unique or right.is_unique:
564 return True
565
566 seen_pairs = set()
567 check_idx = np.where(left.duplicated(keep=False))[0]
568 for idx in check_idx:
569 pair = (left[idx], right[idx])
570 if pair in seen_pairs:
571 return False
572 seen_pairs.add(pair)
573
574 return True
575
576 @property
577 def is_overlapping(self) -> bool:
578 """
579 Return True if the IntervalIndex has overlapping intervals, else False.
580
581 Two intervals overlap if they share a common point, including closed
582 endpoints. Intervals that only have an open endpoint in common do not
583 overlap.
584
585 Returns
586 -------
587 bool
588 Boolean indicating if the IntervalIndex has overlapping intervals.
589
590 See Also
591 --------
592 Interval.overlaps : Check whether two Interval objects overlap.
593 IntervalIndex.overlaps : Check an IntervalIndex elementwise for
594 overlaps.
595
596 Examples
597 --------
598 >>> index = pd.IntervalIndex.from_tuples([(0, 2), (1, 3), (4, 5)])
599 >>> index
600 IntervalIndex([(0, 2], (1, 3], (4, 5]],
601 dtype='interval[int64, right]')
602 >>> index.is_overlapping
603 True
604
605 Intervals that share closed endpoints overlap:
606
607 >>> index = pd.interval_range(0, 3, closed="both")
608 >>> index
609 IntervalIndex([[0, 1], [1, 2], [2, 3]],
610 dtype='interval[int64, both]')
611 >>> index.is_overlapping
612 True
613
614 Intervals that only have an open endpoint in common do not overlap:
615
616 >>> index = pd.interval_range(0, 3, closed="left")
617 >>> index
618 IntervalIndex([[0, 1), [1, 2), [2, 3)],
619 dtype='interval[int64, left]')
620 >>> index.is_overlapping
621 False
622 """
623 # GH 23309
624 return self._engine.is_overlapping
625
626 def _needs_i8_conversion(self, key) -> bool:
627 """
628 Check if a given key needs i8 conversion. Conversion is necessary for
629 Timestamp, Timedelta, DatetimeIndex, and TimedeltaIndex keys. An
630 Interval-like requires conversion if its endpoints are one of the
631 aforementioned types.
632
633 Assumes that any list-like data has already been cast to an Index.
634
635 Parameters
636 ----------
637 key : scalar or Index-like
638 The key that should be checked for i8 conversion
639
640 Returns
641 -------
642 bool
643 """
644 key_dtype = getattr(key, "dtype", None)
645 if isinstance(key_dtype, IntervalDtype) or isinstance(key, Interval):
646 return self._needs_i8_conversion(key.left)
647
648 i8_types = (Timestamp, Timedelta, DatetimeIndex, TimedeltaIndex)
649 return isinstance(key, i8_types)
650
651 def _maybe_convert_i8(self, key):
652 """
653 Maybe convert a given key to its equivalent i8 value(s). Used as a
654 preprocessing step prior to IntervalTree queries (self._engine), which
655 expects numeric data.
656
657 Parameters
658 ----------
659 key : scalar or list-like
660 The key that should maybe be converted to i8.
661
662 Returns
663 -------
664 scalar or list-like
665 The original key if no conversion occurred, int if converted scalar,
666 Index with an int64 dtype if converted list-like.
667 """
668 if is_list_like(key):
669 key = ensure_index(key)
670 key = maybe_upcast_numeric_to_64bit(key)
671
672 if not self._needs_i8_conversion(key):
673 return key
674
675 scalar = is_scalar(key)
676 key_dtype = getattr(key, "dtype", None)
677 if isinstance(key_dtype, IntervalDtype) or isinstance(key, Interval):
678 # convert left/right and reconstruct
679 left = self._maybe_convert_i8(key.left)
680 right = self._maybe_convert_i8(key.right)
681 constructor = Interval if scalar else IntervalIndex.from_arrays
682 return constructor(left, right, closed=self.closed)
683
684 if scalar:
685 # Timestamp/Timedelta
686 key_dtype, key_i8 = infer_dtype_from_scalar(key)
687 if isinstance(key, Period):
688 key_i8 = key.ordinal
689 elif isinstance(key_i8, Timestamp):
690 key_i8 = key_i8._value
691 elif isinstance(key_i8, (np.datetime64, np.timedelta64)):
692 key_i8 = key_i8.view("i8")
693 else:
694 # DatetimeIndex/TimedeltaIndex
695 key_dtype, key_i8 = key.dtype, Index(key.asi8, copy=False)
696 if key.hasnans:
697 # convert NaT from its i8 value to np.nan so it's not viewed
698 # as a valid value, maybe causing errors (e.g. is_overlapping)
699 key_i8 = key_i8.where(~key._isnan)
700
701 # ensure consistency with IntervalIndex subtype
702 # error: Item "ExtensionDtype"/"dtype[Any]" of "Union[dtype[Any],
703 # ExtensionDtype]" has no attribute "subtype"
704 subtype = self.dtype.subtype # type: ignore[union-attr]
705
706 if subtype != key_dtype:
707 raise ValueError(
708 f"Cannot index an IntervalIndex of subtype {subtype} with "
709 f"values of dtype {key_dtype}"
710 )
711
712 return key_i8
713
714 def _searchsorted_monotonic(self, label, side: Literal["left", "right"] = "left"):
715 if not self.is_non_overlapping_monotonic:
716 raise KeyError(
717 "can only get slices from an IntervalIndex if bounds are "
718 "non-overlapping and all monotonic increasing or decreasing"
719 )
720
721 if isinstance(label, (IntervalMixin, IntervalIndex)):
722 raise NotImplementedError("Interval objects are not currently supported")
723
724 # GH 20921: "not is_monotonic_increasing" for the second condition
725 # instead of "is_monotonic_decreasing" to account for single element
726 # indexes being both increasing and decreasing
727 if (side == "left" and self.left.is_monotonic_increasing) or (
728 side == "right" and not self.left.is_monotonic_increasing
729 ):
730 sub_idx = self.right
731 if self.open_right:
732 label = _get_next_label(label)
733 else:
734 sub_idx = self.left
735 if self.open_left:
736 label = _get_prev_label(label)
737
738 return sub_idx._searchsorted_monotonic(label, side)
739
740 # --------------------------------------------------------------------
741 # Indexing Methods
742
743 def get_loc(self, key) -> int | slice | np.ndarray:
744 """
745 Get integer location, slice or boolean mask for requested label.
746
747 The `get_loc` method is used to retrieve the integer index, a slice for
748 slicing objects, or a boolean mask indicating the presence of the label
749 in the `IntervalIndex`.
750
751 Parameters
752 ----------
753 key : label
754 The value or range to find in the IntervalIndex.
755
756 Returns
757 -------
758 int if unique index, slice if monotonic index, else mask
759 The position or positions found. This could be a single
760 number, a range, or an array of true/false values
761 indicating the position(s) of the label.
762
763 See Also
764 --------
765 IntervalIndex.get_indexer_non_unique : Compute indexer and
766 mask for new index given the current index.
767 Index.get_loc : Similar method in the base Index class.
768
769 Examples
770 --------
771 >>> i1, i2 = pd.Interval(0, 1), pd.Interval(1, 2)
772 >>> index = pd.IntervalIndex([i1, i2])
773 >>> index.get_loc(1)
774 0
775
776 You can also supply a point inside an interval.
777
778 >>> index.get_loc(1.5)
779 1
780
781 If a label is in several intervals, you get the locations of all the
782 relevant intervals.
783
784 >>> i3 = pd.Interval(0, 2)
785 >>> overlapping_index = pd.IntervalIndex([i1, i2, i3])
786 >>> overlapping_index.get_loc(0.5)
787 array([ True, False, True])
788
789 Only exact matches will be returned if an interval is provided.
790
791 >>> index.get_loc(pd.Interval(0, 1))
792 0
793 """
794 self._check_indexing_error(key)
795
796 if isinstance(key, Interval):
797 if self.closed != key.closed:
798 raise KeyError(key)
799 mask = (self.left == key.left) & (self.right == key.right)
800 elif is_valid_na_for_dtype(key, self.dtype):
801 mask = self.isna()
802 else:
803 # assume scalar
804 op_left = le if self.closed_left else lt
805 op_right = le if self.closed_right else lt
806 try:
807 mask = op_left(self.left, key) & op_right(key, self.right)
808 except TypeError as err:
809 # scalar is not comparable to II subtype --> invalid label
810 raise KeyError(key) from err
811
812 matches = mask.sum()
813 if matches == 0:
814 raise KeyError(key)
815 if matches == 1:
816 return maybe_unbox_numpy_scalar(mask.argmax())
817
818 res = lib.maybe_booleans_to_slice(mask.view("u1"))
819 if isinstance(res, slice) and res.stop is None:
820 # TODO: DO this in maybe_booleans_to_slice?
821 res = slice(res.start, len(self), res.step)
822 return res
823
824 def _get_indexer(
825 self,
826 target: Index,
827 method: str | None = None,
828 limit: int | None = None,
829 tolerance: Any | None = None,
830 ) -> npt.NDArray[np.intp]:
831 if isinstance(target, IntervalIndex):
832 # We only get here with not self.is_overlapping
833 # -> at most one match per interval in target
834 # want exact matches -> need both left/right to match, so defer to
835 # left/right get_indexer, compare elementwise, equality -> match
836 if self.left.is_unique and self.right.is_unique:
837 indexer = self._get_indexer_unique_sides(target)
838 else:
839 indexer = self._get_indexer_pointwise(target)[0]
840
841 elif not (is_object_dtype(target.dtype) or is_string_dtype(target.dtype)):
842 # homogeneous scalar index: use IntervalTree
843 # we should always have self._should_partial_index(target) here
844 target = self._maybe_convert_i8(target)
845 indexer = self._engine.get_indexer(target.values)
846 else:
847 # heterogeneous scalar index: defer elementwise to get_loc
848 # we should always have self._should_partial_index(target) here
849 return self._get_indexer_pointwise(target)[0]
850
851 return ensure_platform_int(indexer)
852
853 def get_indexer_non_unique(
854 self, target: Index
855 ) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]:
856 """
857 Compute indexer and mask for new index given the current index.
858
859 The indexer should be then used as an input to ndarray.take to align the
860 current data to the new index.
861
862 Parameters
863 ----------
864 target : IntervalIndex or list of Intervals
865 An iterable containing the values to be used for computing indexer.
866
867 Returns
868 -------
869 indexer : np.ndarray[np.intp]
870 Integers from 0 to n - 1 indicating that the index at these
871 positions matches the corresponding target values. Missing values
872 in the target are marked by -1.
873 missing : np.ndarray[np.intp]
874 An indexer into the target of the values not found.
875 These correspond to the -1 in the indexer array.
876
877 See Also
878 --------
879 Index.get_indexer : Computes indexer and mask for new index given
880 the current index.
881 Index.get_indexer_for : Returns an indexer even when non-unique.
882
883 Examples
884 --------
885 >>> index = pd.Index(["c", "b", "a", "b", "b"])
886 >>> index.get_indexer_non_unique(["b", "b"])
887 (array([1, 3, 4, 1, 3, 4]), array([], dtype=int64))
888
889 In the example below there are no matched values.
890
891 >>> index = pd.Index(["c", "b", "a", "b", "b"])
892 >>> index.get_indexer_non_unique(["q", "r", "t"])
893 (array([-1, -1, -1]), array([0, 1, 2]))
894
895 For this reason, the returned ``indexer`` contains only integers equal to -1.
896 It demonstrates that there's no match between the index and the ``target``
897 values at these positions. The mask [0, 1, 2] in the return value shows that
898 the first, second, and third elements are missing.
899
900 Notice that the return value is a tuple contains two items. In the example
901 below the first item is an array of locations in ``index``. The second
902 item is a mask shows that the first and third elements are missing.
903
904 >>> index = pd.Index(["c", "b", "a", "b", "b"])
905 >>> index.get_indexer_non_unique(["f", "b", "s"])
906 (array([-1, 1, 3, 4, -1]), array([0, 2]))
907 """
908 target = ensure_index(target)
909
910 if not self._should_compare(target) and not self._should_partial_index(target):
911 # e.g. IntervalIndex with different closed or incompatible subtype
912 # -> no matches
913 return self._get_indexer_non_comparable(target, None, unique=False)
914
915 elif isinstance(target, IntervalIndex):
916 if self.left.is_unique and self.right.is_unique:
917 # fastpath available even if we don't have self._index_as_unique
918 indexer = self._get_indexer_unique_sides(target)
919 missing = (indexer == -1).nonzero()[0]
920 else:
921 return self._get_indexer_pointwise(target)
922
923 elif is_object_dtype(target.dtype) or not self._should_partial_index(target):
924 # target might contain intervals: defer elementwise to get_loc
925 return self._get_indexer_pointwise(target)
926
927 else:
928 # Note: this case behaves differently from other Index subclasses
929 # because IntervalIndex does partial-int indexing
930 target = self._maybe_convert_i8(target)
931 indexer, missing = self._engine.get_indexer_non_unique(target.values)
932
933 return ensure_platform_int(indexer), ensure_platform_int(missing)
934
935 def _get_indexer_unique_sides(self, target: IntervalIndex) -> npt.NDArray[np.intp]:
936 """
937 _get_indexer specialized to the case where both of our sides are unique.
938 """
939 # Caller is responsible for checking
940 # `self.left.is_unique and self.right.is_unique`
941
942 left_indexer = self.left.get_indexer(target.left)
943 right_indexer = self.right.get_indexer(target.right)
944 indexer = np.where(left_indexer == right_indexer, left_indexer, -1)
945 return indexer
946
947 def _get_indexer_pointwise(
948 self, target: Index
949 ) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]:
950 """
951 pointwise implementation for get_indexer and get_indexer_non_unique.
952 """
953 indexer, missing = [], []
954 for i, key in enumerate(target):
955 try:
956 locs = self.get_loc(key)
957 if isinstance(locs, slice):
958 # Only needed for get_indexer_non_unique
959 locs = np.arange(locs.start, locs.stop, locs.step, dtype="intp")
960 elif lib.is_integer(locs):
961 locs = np.array(locs, ndmin=1)
962 else:
963 # otherwise we have ndarray[bool]
964 locs = np.where(locs)[0]
965 except KeyError:
966 missing.append(i)
967 locs = np.array([-1])
968 except InvalidIndexError:
969 # i.e. non-scalar key e.g. a tuple.
970 # see test_append_different_columns_types_raises
971 missing.append(i)
972 locs = np.array([-1])
973
974 indexer.append(locs)
975
976 concatenated_indexer = np.concatenate(indexer)
977 return ensure_platform_int(concatenated_indexer), ensure_platform_int(missing)
978
979 @cache_readonly
980 def _index_as_unique(self) -> bool:
981 return not self.is_overlapping and self._engine._na_count < 2
982
983 _requires_unique_msg = (
984 "cannot handle overlapping indices; use IntervalIndex.get_indexer_non_unique"
985 )
986
987 def _convert_slice_indexer(self, key: slice, kind: Literal["loc", "getitem"]):
988 if not (key.step is None or key.step == 1):
989 # GH#31658 if label-based, we require step == 1,
990 # if positional, we disallow float start/stop
991 msg = "label-based slicing with step!=1 is not supported for IntervalIndex"
992 if kind == "loc":
993 raise ValueError(msg)
994 if kind == "getitem":
995 if not is_valid_positional_slice(key):
996 # i.e. this cannot be interpreted as a positional slice
997 raise ValueError(msg)
998
999 return super()._convert_slice_indexer(key, kind)
1000
1001 @cache_readonly
1002 def _should_fallback_to_positional(self) -> bool:
1003 # integer lookups in Series.__getitem__ are unambiguously
1004 # positional in this case
1005 # error: Item "ExtensionDtype"/"dtype[Any]" of "Union[dtype[Any],
1006 # ExtensionDtype]" has no attribute "subtype"
1007 return self.dtype.subtype.kind in "mM" # type: ignore[union-attr]
1008
1009 def _maybe_cast_slice_bound(self, label, side: str):
1010 return getattr(self, side)._maybe_cast_slice_bound(label, side)
1011
1012 def _is_comparable_dtype(self, dtype: DtypeObj) -> bool:
1013 if not isinstance(dtype, IntervalDtype):
1014 return False
1015 common_subtype = find_common_type([self.dtype, dtype])
1016 return not is_object_dtype(common_subtype)
1017
1018 # --------------------------------------------------------------------
1019
1020 @cache_readonly
1021 def left(self) -> Index:
1022 """
1023 Return left bounds of the intervals in the IntervalIndex.
1024
1025 The left bounds of each interval in the IntervalIndex are
1026 returned as an Index. The datatype of the left bounds is the
1027 same as the datatype of the endpoints of the intervals.
1028
1029 Returns
1030 -------
1031 Index
1032 An Index containing the left bounds of the intervals.
1033
1034 See Also
1035 --------
1036 IntervalIndex.right : Return the right bounds of the intervals
1037 in the IntervalIndex.
1038 IntervalIndex.mid : Return the mid-point of the intervals in
1039 the IntervalIndex.
1040 IntervalIndex.length : Return the length of the intervals in
1041 the IntervalIndex.
1042
1043 Examples
1044 --------
1045 >>> iv_idx = pd.IntervalIndex.from_arrays([1, 2, 3], [4, 5, 6], closed="right")
1046 >>> iv_idx.left
1047 Index([1, 2, 3], dtype='int64')
1048
1049 >>> iv_idx = pd.IntervalIndex.from_tuples(
1050 ... [(1, 4), (2, 5), (3, 6)], closed="left"
1051 ... )
1052 >>> iv_idx.left
1053 Index([1, 2, 3], dtype='int64')
1054 """
1055 return Index(self._data.left, copy=False)
1056
1057 @cache_readonly
1058 def right(self) -> Index:
1059 """
1060 Return right bounds of the intervals in the IntervalIndex.
1061
1062 The right bounds of each interval in the IntervalIndex are
1063 returned as an Index. The datatype of the right bounds is the
1064 same as the datatype of the endpoints of the intervals.
1065
1066 Returns
1067 -------
1068 Index
1069 An Index containing the right bounds of the intervals.
1070
1071 See Also
1072 --------
1073 IntervalIndex.left : Return the left bounds of the intervals
1074 in the IntervalIndex.
1075 IntervalIndex.mid : Return the mid-point of the intervals in
1076 the IntervalIndex.
1077 IntervalIndex.length : Return the length of the intervals in
1078 the IntervalIndex.
1079
1080 Examples
1081 --------
1082 >>> iv_idx = pd.IntervalIndex.from_arrays([1, 2, 3], [4, 5, 6], closed="right")
1083 >>> iv_idx.right
1084 Index([4, 5, 6], dtype='int64')
1085
1086 >>> iv_idx = pd.IntervalIndex.from_tuples(
1087 ... [(1, 4), (2, 5), (3, 6)], closed="left"
1088 ... )
1089 >>> iv_idx.right
1090 Index([4, 5, 6], dtype='int64')
1091 """
1092 return Index(self._data.right, copy=False)
1093
1094 @cache_readonly
1095 def mid(self) -> Index:
1096 """
1097 Return the midpoint of each interval in the IntervalIndex as an Index.
1098
1099 Each midpoint is calculated as the average of the left and right bounds
1100 of each interval. The midpoints are returned as a pandas Index object.
1101
1102 Returns
1103 -------
1104 pandas.Index
1105 An Index containing the midpoints of each interval.
1106
1107 See Also
1108 --------
1109 IntervalIndex.left : Return the left bounds of the intervals
1110 in the IntervalIndex.
1111 IntervalIndex.right : Return the right bounds of the intervals
1112 in the IntervalIndex.
1113 IntervalIndex.length : Return the length of the intervals in
1114 the IntervalIndex.
1115
1116 Notes
1117 -----
1118 The midpoint is the average of the interval bounds, potentially resulting
1119 in a floating-point number even if bounds are integers. The returned Index
1120 will have a dtype that accurately holds the midpoints. This computation is
1121 the same regardless of whether intervals are open or closed.
1122
1123 Examples
1124 --------
1125 >>> iv_idx = pd.IntervalIndex.from_arrays([1, 2, 3], [4, 5, 6])
1126 >>> iv_idx.mid
1127 Index([2.5, 3.5, 4.5], dtype='float64')
1128
1129 >>> iv_idx = pd.IntervalIndex.from_tuples([(1, 4), (2, 5), (3, 6)])
1130 >>> iv_idx.mid
1131 Index([2.5, 3.5, 4.5], dtype='float64')
1132 """
1133 return Index(self._data.mid, copy=False)
1134
1135 @property
1136 def length(self) -> Index:
1137 """
1138 Calculate the length of each interval in the IntervalIndex.
1139
1140 This method returns a new Index containing the lengths of each interval
1141 in the IntervalIndex. The length of an interval is defined as the difference
1142 between its end and its start.
1143
1144 Returns
1145 -------
1146 Index
1147 An Index containing the lengths of each interval.
1148
1149 See Also
1150 --------
1151 Interval.length : Return the length of the Interval.
1152
1153 Examples
1154 --------
1155 >>> intervals = pd.IntervalIndex.from_arrays(
1156 ... [1, 2, 3], [4, 5, 6], closed="right"
1157 ... )
1158 >>> intervals.length
1159 Index([3, 3, 3], dtype='int64')
1160
1161 >>> intervals = pd.IntervalIndex.from_tuples([(1, 5), (6, 10), (11, 15)])
1162 >>> intervals.length
1163 Index([4, 4, 4], dtype='int64')
1164 """
1165 return Index(self._data.length, copy=False)
1166
1167 # --------------------------------------------------------------------
1168 # Set Operations
1169
1170 def _intersection(self, other, sort: bool = False):
1171 """
1172 intersection specialized to the case with matching dtypes.
1173 """
1174 # For IntervalIndex we also know other.closed == self.closed
1175 if self.left.is_unique and self.right.is_unique:
1176 taken = self._intersection_unique(other)
1177 elif other.left.is_unique and other.right.is_unique and self.isna().sum() <= 1:
1178 # Swap other/self if other is unique and self does not have
1179 # multiple NaNs
1180 taken = other._intersection_unique(self)
1181 else:
1182 # duplicates
1183 taken = self._intersection_non_unique(other)
1184
1185 if sort:
1186 taken = taken.sort_values()
1187
1188 return taken
1189
1190 def _intersection_unique(self, other: IntervalIndex) -> IntervalIndex:
1191 """
1192 Used when the IntervalIndex does not have any common endpoint,
1193 no matter left or right.
1194 Return the intersection with another IntervalIndex.
1195 Parameters
1196 ----------
1197 other : IntervalIndex
1198 Returns
1199 -------
1200 IntervalIndex
1201 """
1202 # Note: this is much more performant than super()._intersection(other)
1203 lindexer = self.left.get_indexer(other.left)
1204 rindexer = self.right.get_indexer(other.right)
1205
1206 match = (lindexer == rindexer) & (lindexer != -1)
1207 indexer = lindexer.take(match.nonzero()[0])
1208 indexer = unique(indexer)
1209
1210 return self.take(indexer)
1211
1212 def _intersection_non_unique(self, other: IntervalIndex) -> IntervalIndex:
1213 """
1214 Used when the IntervalIndex does have some common endpoints,
1215 on either sides.
1216 Return the intersection with another IntervalIndex.
1217
1218 Parameters
1219 ----------
1220 other : IntervalIndex
1221
1222 Returns
1223 -------
1224 IntervalIndex
1225 """
1226 # Note: this is about 3.25x faster than super()._intersection(other)
1227 # in IntervalIndexMethod.time_intersection_both_duplicate(1000)
1228 mask = np.zeros(len(self), dtype=bool)
1229
1230 if self.hasnans and other.hasnans:
1231 first_nan_loc = np.arange(len(self))[self.isna()][0]
1232 mask[first_nan_loc] = True
1233
1234 other_tups = set(zip(other.left, other.right, strict=True))
1235 for i, tup in enumerate(zip(self.left, self.right, strict=True)):
1236 if tup in other_tups:
1237 mask[i] = True
1238
1239 return self[mask]
1240
1241 # --------------------------------------------------------------------
1242
1243 def _get_engine_target(self) -> np.ndarray:
1244 # Note: we _could_ use libjoin functions by either casting to object
1245 # dtype or constructing tuples (faster than constructing Intervals)
1246 # but the libjoin fastpaths are no longer fast in these cases.
1247 raise NotImplementedError(
1248 "IntervalIndex does not use libjoin fastpaths or pass values to "
1249 "IndexEngine objects"
1250 )
1251
1252 def _from_join_target(self, result):
1253 raise NotImplementedError("IntervalIndex does not use libjoin fastpaths")
1254
1255 # TODO: arithmetic operations
1256
1257
1258def _is_valid_endpoint(endpoint) -> bool:
1259 """
1260 Helper for interval_range to check if start/end are valid types.
1261 """
1262 return any(
1263 [
1264 is_number(endpoint),
1265 isinstance(endpoint, Timestamp),
1266 isinstance(endpoint, Timedelta),
1267 endpoint is None,
1268 ]
1269 )
1270
1271
1272def _is_type_compatible(a, b) -> bool:
1273 """
1274 Helper for interval_range to check type compat of start/end/freq.
1275 """
1276 is_ts_compat = lambda x: isinstance(x, (Timestamp, BaseOffset))
1277 is_td_compat = lambda x: isinstance(x, (Timedelta, BaseOffset))
1278 return (
1279 (is_number(a) and is_number(b))
1280 or (is_ts_compat(a) and is_ts_compat(b))
1281 or (is_td_compat(a) and is_td_compat(b))
1282 or com.any_none(a, b)
1283 )
1284
1285
1286@set_module("pandas")
1287def interval_range(
1288 start=None,
1289 end=None,
1290 periods=None,
1291 freq=None,
1292 name: Hashable | None = None,
1293 closed: IntervalClosedType = "right",
1294) -> IntervalIndex:
1295 """
1296 Return a fixed frequency IntervalIndex.
1297
1298 Parameters
1299 ----------
1300 start : numeric or datetime-like, default None
1301 Left bound for generating intervals.
1302 end : numeric or datetime-like, default None
1303 Right bound for generating intervals.
1304 periods : int, default None
1305 Number of periods to generate.
1306 freq : numeric, str, Timedelta, datetime.timedelta, or DateOffset, default None
1307 The length of each interval. Must be consistent with the type of start
1308 and end, e.g. 2 for numeric, or '5H' for datetime-like. Default is 1
1309 for numeric and 'D' for datetime-like.
1310 name : str, default None
1311 Name of the resulting IntervalIndex.
1312 closed : {'left', 'right', 'both', 'neither'}, default 'right'
1313 Whether the intervals are closed on the left-side, right-side, both
1314 or neither.
1315
1316 Returns
1317 -------
1318 IntervalIndex
1319 Object with a fixed frequency.
1320
1321 See Also
1322 --------
1323 IntervalIndex : An Index of intervals that are all closed on the same side.
1324
1325 Notes
1326 -----
1327 Of the four parameters ``start``, ``end``, ``periods``, and ``freq``,
1328 exactly three must be specified. If ``freq`` is omitted, the resulting
1329 ``IntervalIndex`` will have ``periods`` linearly spaced elements between
1330 ``start`` and ``end``, inclusively.
1331
1332 To learn more about datetime-like frequency strings, please see
1333 :ref:`this link<timeseries.offset_aliases>`.
1334
1335 Examples
1336 --------
1337 Numeric ``start`` and ``end`` is supported.
1338
1339 >>> pd.interval_range(start=0, end=5)
1340 IntervalIndex([(0, 1], (1, 2], (2, 3], (3, 4], (4, 5]],
1341 dtype='interval[int64, right]')
1342
1343 Additionally, datetime-like input is also supported.
1344
1345 >>> pd.interval_range(
1346 ... start=pd.Timestamp("2017-01-01"), end=pd.Timestamp("2017-01-04")
1347 ... )
1348 IntervalIndex([(2017-01-01 00:00:00, 2017-01-02 00:00:00],
1349 (2017-01-02 00:00:00, 2017-01-03 00:00:00],
1350 (2017-01-03 00:00:00, 2017-01-04 00:00:00]],
1351 dtype='interval[datetime64[us], right]')
1352
1353 The ``freq`` parameter specifies the frequency between the left and right.
1354 endpoints of the individual intervals within the ``IntervalIndex``. For
1355 numeric ``start`` and ``end``, the frequency must also be numeric.
1356
1357 >>> pd.interval_range(start=0, periods=4, freq=1.5)
1358 IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0]],
1359 dtype='interval[float64, right]')
1360
1361 Similarly, for datetime-like ``start`` and ``end``, the frequency must be
1362 convertible to a DateOffset.
1363
1364 >>> pd.interval_range(start=pd.Timestamp("2017-01-01"), periods=3, freq="MS")
1365 IntervalIndex([(2017-01-01 00:00:00, 2017-02-01 00:00:00],
1366 (2017-02-01 00:00:00, 2017-03-01 00:00:00],
1367 (2017-03-01 00:00:00, 2017-04-01 00:00:00]],
1368 dtype='interval[datetime64[us], right]')
1369
1370 Specify ``start``, ``end``, and ``periods``; the frequency is generated
1371 automatically (linearly spaced).
1372
1373 >>> pd.interval_range(start=0, end=6, periods=4)
1374 IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0]],
1375 dtype='interval[float64, right]')
1376
1377 The ``closed`` parameter specifies which endpoints of the individual
1378 intervals within the ``IntervalIndex`` are closed.
1379
1380 >>> pd.interval_range(end=5, periods=4, closed="both")
1381 IntervalIndex([[1, 2], [2, 3], [3, 4], [4, 5]],
1382 dtype='interval[int64, both]')
1383 """
1384 start = maybe_box_datetimelike(start)
1385 end = maybe_box_datetimelike(end)
1386 endpoint = start if start is not None else end
1387
1388 if freq is None and com.any_none(periods, start, end):
1389 freq = 1 if is_number(endpoint) else "D"
1390
1391 if com.count_not_none(start, end, periods, freq) != 3:
1392 raise ValueError(
1393 "Of the four parameters: start, end, periods, and "
1394 "freq, exactly three must be specified"
1395 )
1396
1397 if not _is_valid_endpoint(start):
1398 raise ValueError(f"start must be numeric or datetime-like, got {start}")
1399 if not _is_valid_endpoint(end):
1400 raise ValueError(f"end must be numeric or datetime-like, got {end}")
1401
1402 periods = validate_periods(periods)
1403
1404 if freq is not None and not is_number(freq):
1405 try:
1406 freq = to_offset(freq)
1407 except ValueError as err:
1408 raise ValueError(
1409 f"freq must be numeric or convertible to DateOffset, got {freq}"
1410 ) from err
1411
1412 # verify type compatibility
1413 if not all(
1414 [
1415 _is_type_compatible(start, end),
1416 _is_type_compatible(start, freq),
1417 _is_type_compatible(end, freq),
1418 ]
1419 ):
1420 raise TypeError("start, end, freq need to be type compatible")
1421
1422 # +1 to convert interval count to breaks count (n breaks = n-1 intervals)
1423 if periods is not None:
1424 periods += 1
1425
1426 breaks: np.ndarray | TimedeltaIndex | DatetimeIndex
1427
1428 if is_number(endpoint):
1429 dtype: np.dtype = np.dtype("int64")
1430 if com.all_not_none(start, end, freq):
1431 if (
1432 isinstance(start, (np.integer, np.floating))
1433 and isinstance(end, (np.integer, np.floating))
1434 and start.dtype == end.dtype
1435 ):
1436 dtype = start.dtype
1437 elif (
1438 isinstance(start, (float, np.floating))
1439 or isinstance(end, (float, np.floating))
1440 or isinstance(freq, (float, np.floating))
1441 ):
1442 dtype = np.dtype("float64")
1443 # 0.1 ensures we capture end
1444 breaks = np.arange(start, end + (freq * 0.1), freq)
1445 breaks = maybe_downcast_numeric(breaks, dtype)
1446 else:
1447 # compute the period/start/end if unspecified (at most one)
1448 if periods is None:
1449 periods = int((end - start) // freq) + 1
1450 elif start is None:
1451 start = end - (periods - 1) * freq
1452 elif end is None:
1453 end = start + (periods - 1) * freq
1454
1455 breaks = np.linspace(start, end, periods)
1456 if all(is_integer(x) for x in com.not_none(start, end, freq)):
1457 # np.linspace always produces float output
1458 breaks = maybe_downcast_numeric(breaks, dtype)
1459 # delegate to the appropriate range function
1460 elif isinstance(endpoint, Timestamp):
1461 breaks = date_range(start=start, end=end, periods=periods, freq=freq)
1462 else:
1463 breaks = timedelta_range(start=start, end=end, periods=periods, freq=freq)
1464
1465 return IntervalIndex.from_breaks(
1466 breaks,
1467 name=name,
1468 closed=closed,
1469 dtype=IntervalDtype(subtype=breaks.dtype, closed=closed),
1470 )