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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

379 statements  

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 )