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

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

419 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 cast, 

15) 

16 

17import numpy as np 

18 

19from pandas._libs import lib 

20from pandas._libs.interval import ( 

21 Interval, 

22 IntervalMixin, 

23 IntervalTree, 

24) 

25from pandas._libs.tslibs import ( 

26 BaseOffset, 

27 Period, 

28 Timedelta, 

29 Timestamp, 

30 to_offset, 

31) 

32from pandas.errors import InvalidIndexError 

33from pandas.util._decorators import ( 

34 cache_readonly, 

35 set_module, 

36) 

37from pandas.util._exceptions import rewrite_exception 

38 

39from pandas.core.dtypes.cast import ( 

40 find_common_type, 

41 infer_dtype_from_scalar, 

42 maybe_box_datetimelike, 

43 maybe_downcast_numeric, 

44 maybe_unbox_numpy_scalar, 

45 maybe_upcast_numeric_to_64bit, 

46) 

47from pandas.core.dtypes.common import ( 

48 ensure_platform_int, 

49 is_float_dtype, 

50 is_integer, 

51 is_integer_dtype, 

52 is_list_like, 

53 is_number, 

54 is_object_dtype, 

55 is_scalar, 

56 is_string_dtype, 

57 pandas_dtype, 

58) 

59from pandas.core.dtypes.dtypes import ( 

60 DatetimeTZDtype, 

61 IntervalDtype, 

62) 

63from pandas.core.dtypes.missing import ( 

64 is_valid_na_for_dtype, 

65 isna, 

66) 

67 

68from pandas.core.algorithms import unique 

69from pandas.core.arrays.datetimelike import validate_periods 

70from pandas.core.arrays.interval import ( 

71 IntervalArray, 

72) 

73import pandas.core.common as com 

74from pandas.core.indexers import is_valid_positional_slice 

75from pandas.core.indexes.base import ( 

76 Index, 

77 ensure_index, 

78 maybe_extract_name, 

79) 

80from pandas.core.indexes.datetimes import ( 

81 DatetimeIndex, 

82 date_range, 

83) 

84from pandas.core.indexes.extension import ExtensionIndex 

85from pandas.core.indexes.multi import MultiIndex 

86from pandas.core.indexes.timedeltas import ( 

87 TimedeltaIndex, 

88 timedelta_range, 

89) 

90 

91if TYPE_CHECKING: 

92 from collections.abc import Hashable 

93 

94 from pandas._typing import ( 

95 Axes, 

96 Dtype, 

97 DtypeObj, 

98 IntervalClosedType, 

99 npt, 

100 ) 

101 

102 

103def _get_next_label(label): 

104 # see test_slice_locs_with_ints_and_floats_succeeds 

105 dtype = getattr(label, "dtype", type(label)) 

106 if isinstance(label, (Timestamp, Timedelta)): 

107 dtype = "datetime64[ns]" 

108 dtype = pandas_dtype(dtype) 

109 

110 if lib.is_np_dtype(dtype, "mM") or isinstance(dtype, DatetimeTZDtype): 

111 return label + np.timedelta64(1, "ns") 

112 elif is_integer_dtype(dtype): 

113 return label + 1 

114 elif is_float_dtype(dtype): 

115 return np.nextafter(label, np.inf) 

116 else: 

117 raise TypeError(f"cannot determine next label for type {type(label)!r}") 

118 

119 

120def _get_prev_label(label): 

121 # see test_slice_locs_with_ints_and_floats_succeeds 

122 dtype = getattr(label, "dtype", type(label)) 

123 if isinstance(label, (Timestamp, Timedelta)): 

124 dtype = "datetime64[ns]" 

125 dtype = pandas_dtype(dtype) 

126 

127 if lib.is_np_dtype(dtype, "mM") or isinstance(dtype, DatetimeTZDtype): 

128 return label - np.timedelta64(1, "ns") 

129 elif is_integer_dtype(dtype): 

130 return label - 1 

131 elif is_float_dtype(dtype): 

132 return np.nextafter(label, -np.inf) 

133 else: 

134 raise TypeError(f"cannot determine next label for type {type(label)!r}") 

135 

136 

137def _new_IntervalIndex(cls, d): 

138 """ 

139 This is called upon unpickling, rather than the default which doesn't have 

140 arguments and breaks __new__. 

141 """ 

142 return cls.from_arrays(**d) 

143 

144 

145@set_module("pandas") 

146class IntervalIndex(ExtensionIndex): 

147 """ 

148 Immutable index of intervals that are closed on the same side. 

149 

150 An ``IntervalIndex`` stores an array of :class:`Interval` objects, all 

151 sharing the same ``closed`` value, enabling vectorized operations such as 

152 overlap checks, containment tests, and set-like comparisons on intervals. 

153 It is commonly produced by :func:`cut`, :func:`qcut`, or 

154 :func:`interval_range`. 

155 

156 Parameters 

157 ---------- 

158 data : array-like (1-dimensional) 

159 Array-like (ndarray, :class:`DateTimeArray`, :class:`TimeDeltaArray`) containing 

160 Interval objects from which to build the IntervalIndex. 

161 closed : {'left', 'right', 'both', 'neither'}, default 'right' 

162 Whether the intervals are closed on the left-side, right-side, both or 

163 neither. 

164 dtype : dtype or None, default None 

165 If None, dtype will be inferred. 

166 copy : bool, default None 

167 Whether to copy input data, only relevant for array, Series, and Index 

168 inputs (for other input, e.g. a list, a new array is created anyway). 

169 Defaults to True for array input and False for Index/Series. 

170 Set to False to avoid copying array input at your own risk (if you 

171 know the input data won't be modified elsewhere). 

172 Set to True to force copying Series/Index input up front. 

173 name : object, optional 

174 Name to be stored in the index. 

175 verify_integrity : bool, default True 

176 Verify that the IntervalIndex is valid. 

177 

178 Attributes 

179 ---------- 

180 left 

181 right 

182 closed 

183 mid 

184 length 

185 is_empty 

186 is_non_overlapping_monotonic 

187 is_overlapping 

188 values 

189 

190 Methods 

191 ------- 

192 from_arrays 

193 from_tuples 

194 from_breaks 

195 get_loc 

196 get_indexer 

197 contains 

198 overlaps 

199 set_closed 

200 to_tuples 

201 

202 See Also 

203 -------- 

204 Index : The base pandas Index type. 

205 Interval : A bounded slice-like interval; the elements of an IntervalIndex. 

206 interval_range : Function to create a fixed frequency IntervalIndex. 

207 cut : Bin values into discrete Intervals. 

208 qcut : Bin values into equal-sized Intervals based on rank or sample quantiles. 

209 

210 Notes 

211 ----- 

212 See the `user guide 

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

214 for more. 

215 

216 Examples 

217 -------- 

218 A new ``IntervalIndex`` is typically constructed using 

219 :func:`interval_range`: 

220 

221 >>> pd.interval_range(start=0, end=5) 

222 IntervalIndex([(0, 1], (1, 2], (2, 3], (3, 4], (4, 5]], 

223 dtype='interval[int64, right]') 

224 

225 It may also be constructed using one of the constructor 

226 methods: :meth:`IntervalIndex.from_arrays`, 

227 :meth:`IntervalIndex.from_breaks`, and :meth:`IntervalIndex.from_tuples`. 

228 

229 See further examples in the doc strings of ``interval_range`` and the 

230 mentioned constructor methods. 

231 """ 

232 

233 _typ = "intervalindex" 

234 

235 _data: IntervalArray 

236 _values: IntervalArray 

237 _can_hold_strings = False 

238 _data_cls = IntervalArray 

239 

240 # -------------------------------------------------------------------- 

241 # Descriptors and methods dispatched to the underlying IntervalArray 

242 

243 @cache_readonly 

244 def closed(self) -> IntervalClosedType: 

245 """ 

246 String describing the inclusive side of the intervals. 

247 

248 Either ``left``, ``right``, ``both`` or ``neither``. 

249 

250 See Also 

251 -------- 

252 IntervalIndex.set_closed : Return an IntervalIndex identical to the 

253 current one, but closed on the specified side. 

254 Interval.closed : Returns inclusive side of the Interval. 

255 

256 Examples 

257 -------- 

258 >>> interv_idx = pd.interval_range(start=0, end=2) 

259 >>> interv_idx 

260 IntervalIndex([(0, 1], (1, 2]], dtype='interval[int64, right]') 

261 >>> interv_idx.closed 

262 'right' 

263 """ 

264 return self._data.closed 

265 

266 @cache_readonly 

267 def is_non_overlapping_monotonic(self) -> bool: 

268 """ 

269 Return True if the IntervalIndex is non-overlapping and monotonic. 

270 

271 Non-overlapping means no Intervals share points, and monotonic means 

272 either monotonic increasing or monotonic decreasing. 

273 

274 See Also 

275 -------- 

276 IntervalIndex.overlaps : Check an IntervalIndex elementwise for 

277 overlaps. 

278 IntervalIndex.is_overlapping : Return True if the IntervalIndex has 

279 overlapping intervals. 

280 

281 Examples 

282 -------- 

283 >>> interv_idx = pd.interval_range(start=0, end=2) 

284 >>> interv_idx 

285 IntervalIndex([(0, 1], (1, 2]], dtype='interval[int64, right]') 

286 >>> interv_idx.is_non_overlapping_monotonic 

287 True 

288 

289 >>> interv_idx = pd.interval_range(start=0, end=2, closed="both") 

290 >>> interv_idx 

291 IntervalIndex([[0, 1], [1, 2]], dtype='interval[int64, both]') 

292 >>> interv_idx.is_non_overlapping_monotonic 

293 False 

294 """ 

295 return self._data.is_non_overlapping_monotonic 

296 

297 @property 

298 def closed_left(self) -> bool: 

299 """ 

300 Check if the intervals are closed on the left side. 

301 

302 All intervals in an IntervalIndex share the same ``closed`` value, 

303 so this describes the index as a whole rather than element-wise. 

304 

305 See Also 

306 -------- 

307 IntervalIndex.closed_right : Check if the intervals are closed on 

308 the right side. 

309 IntervalIndex.open_left : Boolean inverse of closed_left. 

310 

311 Examples 

312 -------- 

313 >>> iv_idx = pd.interval_range(start=0, end=2, closed="left") 

314 >>> iv_idx.closed_left 

315 True 

316 

317 >>> iv_idx = pd.interval_range(start=0, end=2, closed="right") 

318 >>> iv_idx.closed_left 

319 False 

320 """ 

321 return self._data.closed_left 

322 

323 @property 

324 def closed_right(self) -> bool: 

325 """ 

326 Check if the intervals are closed on the right side. 

327 

328 All intervals in an IntervalIndex share the same ``closed`` value, 

329 so this describes the index as a whole rather than element-wise. 

330 

331 See Also 

332 -------- 

333 IntervalIndex.closed_left : Check if the intervals are closed on 

334 the left side. 

335 IntervalIndex.open_right : Boolean inverse of closed_right. 

336 

337 Examples 

338 -------- 

339 >>> iv_idx = pd.interval_range(start=0, end=2, closed="right") 

340 >>> iv_idx.closed_right 

341 True 

342 

343 >>> iv_idx = pd.interval_range(start=0, end=2, closed="left") 

344 >>> iv_idx.closed_right 

345 False 

346 """ 

347 return self._data.closed_right 

348 

349 @property 

350 def open_left(self) -> bool: 

351 """ 

352 Check if the intervals are open on the left side. 

353 

354 All intervals in an IntervalIndex share the same ``closed`` value, 

355 so this describes the index as a whole rather than element-wise. 

356 

357 See Also 

358 -------- 

359 IntervalIndex.open_right : Check if the intervals are open on the 

360 right side. 

361 IntervalIndex.closed_left : Boolean inverse of open_left. 

362 

363 Examples 

364 -------- 

365 >>> iv_idx = pd.interval_range(start=0, end=2, closed="left") 

366 >>> iv_idx.open_left 

367 False 

368 

369 >>> iv_idx = pd.interval_range(start=0, end=2, closed="right") 

370 >>> iv_idx.open_left 

371 True 

372 """ 

373 return self._data.open_left 

374 

375 @property 

376 def open_right(self) -> bool: 

377 """ 

378 Check if the intervals are open on the right side. 

379 

380 All intervals in an IntervalIndex share the same ``closed`` value, 

381 so this describes the index as a whole rather than element-wise. 

382 

383 See Also 

384 -------- 

385 IntervalIndex.open_left : Check if the intervals are open on the 

386 left side. 

387 IntervalIndex.closed_right : Boolean inverse of open_right. 

388 

389 Examples 

390 -------- 

391 >>> iv_idx = pd.interval_range(start=0, end=2, closed="right") 

392 >>> iv_idx.open_right 

393 False 

394 

395 >>> iv_idx = pd.interval_range(start=0, end=2, closed="left") 

396 >>> iv_idx.open_right 

397 True 

398 """ 

399 return self._data.open_right 

400 

401 @property 

402 def is_empty(self) -> npt.NDArray[np.bool_]: 

403 """ 

404 Indicates if an interval is empty, meaning it contains no points. 

405 

406 An interval is considered empty if its left and right endpoints are 

407 equal, and it is not closed on both sides. Returns a boolean ndarray 

408 positionally indicating the emptiness of each interval. 

409 

410 Returns 

411 ------- 

412 numpy.ndarray[bool] 

413 A boolean ndarray positionally indicating if an Interval is empty. 

414 

415 See Also 

416 -------- 

417 Interval.is_empty : Indicates if a scalar Interval is empty. 

418 IntervalIndex.length : Return the length of each interval. 

419 

420 Examples 

421 -------- 

422 >>> ivs = [ 

423 ... pd.Interval(0, 0, closed="neither"), 

424 ... pd.Interval(1, 2, closed="neither"), 

425 ... ] 

426 >>> pd.IntervalIndex(ivs).is_empty 

427 array([ True, False]) 

428 

429 Missing values are not considered empty: 

430 

431 >>> ivs = [pd.Interval(0, 0, closed="neither"), np.nan] 

432 >>> pd.IntervalIndex(ivs).is_empty 

433 array([ True, False]) 

434 """ 

435 # error: Incompatible return value type (got "bool", expected 

436 # "ndarray[Any, dtype[bool_]]") -- the IntervalMixin stub only types 

437 # the Interval scalar case, not the IntervalArray/IntervalIndex case 

438 return self._data.is_empty # type: ignore[return-value] 

439 

440 def set_closed(self, closed: IntervalClosedType) -> Self: 

441 """ 

442 Return an identical IntervalIndex closed on the specified side. 

443 

444 This method creates a new IntervalIndex with the same bounds but with 

445 a different closure specification. 

446 

447 Parameters 

448 ---------- 

449 closed : {'left', 'right', 'both', 'neither'} 

450 Whether the intervals are closed on the left-side, right-side, both 

451 or neither. 

452 

453 Returns 

454 ------- 

455 IntervalIndex 

456 A new IntervalIndex with the specified side closures. 

457 

458 See Also 

459 -------- 

460 IntervalIndex.closed : Returns inclusive side of the IntervalIndex. 

461 

462 Examples 

463 -------- 

464 >>> index = pd.interval_range(0, 3) 

465 >>> index 

466 IntervalIndex([(0, 1], (1, 2], (2, 3]], 

467 dtype='interval[int64, right]') 

468 >>> index.set_closed("both") 

469 IntervalIndex([[0, 1], [1, 2], [2, 3]], 

470 dtype='interval[int64, both]') 

471 """ 

472 array = self._data.set_closed(closed) 

473 return type(self)._simple_new(array, name=self.name) 

474 

475 def to_tuples(self, na_tuple: bool = True) -> Index: 

476 """ 

477 Return an Index of tuples of the form (left, right). 

478 

479 This method extracts the bounds of each interval as a tuple, 

480 useful for iteration or conversion to other data structures. 

481 

482 Parameters 

483 ---------- 

484 na_tuple : bool, default True 

485 If ``True``, return ``NA`` as a tuple ``(nan, nan)``. If ``False``, 

486 just return ``NA`` as ``nan``. 

487 

488 Returns 

489 ------- 

490 Index 

491 An Index of tuples representing the intervals. 

492 

493 See Also 

494 -------- 

495 arrays.IntervalArray.to_tuples : Analogous method for IntervalArray. 

496 

497 Examples 

498 -------- 

499 >>> idx = pd.interval_range(start=0, end=2) 

500 >>> idx 

501 IntervalIndex([(0, 1], (1, 2]], dtype='interval[int64, right]') 

502 >>> idx.to_tuples() 

503 Index([(0, 1), (1, 2)], dtype='object') 

504 """ 

505 result = self._data.to_tuples(na_tuple=na_tuple) 

506 return Index(result, name=self.name, dtype=result.dtype, copy=False) 

507 

508 def overlaps(self, other: Interval) -> npt.NDArray[np.bool_]: 

509 """ 

510 Check elementwise if an Interval overlaps the values in the IntervalIndex. 

511 

512 Two intervals overlap if they share a common point, including closed 

513 endpoints. Intervals that only have an open endpoint in common do not 

514 overlap, and empty intervals contain no points at all, so they never 

515 overlap. 

516 

517 Parameters 

518 ---------- 

519 other : Interval 

520 Interval to check against for an overlap. 

521 

522 Returns 

523 ------- 

524 ndarray 

525 Boolean array positionally indicating where an overlap occurs. 

526 

527 See Also 

528 -------- 

529 Interval.overlaps : Check whether two Interval objects overlap. 

530 

531 Examples 

532 -------- 

533 >>> data = [(0, 1), (1, 3), (2, 4)] 

534 >>> intervals = pd.IntervalIndex.from_tuples(data) 

535 >>> intervals 

536 IntervalIndex([(0, 1], (1, 3], (2, 4]], dtype='interval[int64, right]') 

537 

538 >>> intervals.overlaps(pd.Interval(0.5, 1.5)) 

539 array([ True, True, False]) 

540 

541 Intervals that share closed endpoints overlap: 

542 

543 >>> intervals.overlaps(pd.Interval(1, 3, closed="left")) 

544 array([ True, True, True]) 

545 

546 Intervals that only have an open endpoint in common do not overlap: 

547 

548 >>> intervals.overlaps(pd.Interval(1, 2, closed="right")) 

549 array([False, True, False]) 

550 """ 

551 return self._data.overlaps(other) 

552 

553 def contains(self, other) -> npt.NDArray[np.bool_]: 

554 """ 

555 Check elementwise if the Intervals contain the value. 

556 

557 Return a boolean mask whether the value is contained in the Intervals 

558 of the IntervalIndex. 

559 

560 Parameters 

561 ---------- 

562 other : scalar 

563 The value to check whether it is contained in the Intervals. 

564 

565 Returns 

566 ------- 

567 numpy.ndarray[bool] 

568 A boolean mask whether the value is contained in the Intervals. 

569 

570 See Also 

571 -------- 

572 IntervalIndex.__contains__ : Check whether an Interval is one of the 

573 IntervalIndex's elements. 

574 IntervalIndex.overlaps : Check if an Interval overlaps the values in 

575 the IntervalIndex. 

576 

577 Examples 

578 -------- 

579 >>> intervals = pd.IntervalIndex.from_tuples([(0, 1), (1, 3), (2, 4)]) 

580 >>> intervals 

581 IntervalIndex([(0, 1], (1, 3], (2, 4]], dtype='interval[int64, right]') 

582 

583 >>> intervals.contains(0.5) 

584 array([ True, False, False]) 

585 """ 

586 return self._data.contains(other) 

587 

588 # -------------------------------------------------------------------- 

589 # Constructors 

590 

591 def __new__( 

592 cls, 

593 data, 

594 closed: IntervalClosedType | None = None, 

595 dtype: Dtype | None = None, 

596 copy: bool | None = None, 

597 name: Hashable | None = None, 

598 verify_integrity: bool = True, 

599 ) -> Self: 

600 name = maybe_extract_name(name, data, cls) 

601 

602 # GH#63388 

603 data, copy = cls._maybe_copy_array_input(data, copy, dtype) 

604 

605 with rewrite_exception("IntervalArray", cls.__name__): 

606 array = IntervalArray( 

607 data, 

608 closed=closed, 

609 copy=copy, 

610 dtype=dtype, 

611 verify_integrity=verify_integrity, 

612 ) 

613 

614 return cls._simple_new(array, name) 

615 

616 @classmethod 

617 def from_breaks( 

618 cls, 

619 breaks, 

620 closed: IntervalClosedType | None = "right", 

621 name: Hashable | None = None, 

622 copy: bool = False, 

623 dtype: Dtype | None = None, 

624 ) -> IntervalIndex: 

625 """ 

626 Construct an IntervalIndex from an array of splits. 

627 

628 The *breaks* array of length *N* is converted into *N-1* adjacent, 

629 non-overlapping intervals whose endpoints are consecutive pairs of 

630 break values. 

631 

632 Parameters 

633 ---------- 

634 breaks : array-like (1-dimensional) 

635 Left and right bounds for each interval. 

636 closed : {'left', 'right', 'both', 'neither'}, default 'right' 

637 Whether the intervals are closed on the left-side, right-side, both 

638 or neither. 

639 name : str, optional 

640 Name of the resulting IntervalIndex. 

641 copy : bool, default False 

642 Copy the data. 

643 dtype : dtype or None, default None 

644 If None, dtype will be inferred. 

645 

646 Returns 

647 ------- 

648 IntervalIndex 

649 

650 See Also 

651 -------- 

652 interval_range : Function to create a fixed frequency IntervalIndex. 

653 IntervalIndex.from_arrays : Construct from a left and right array. 

654 IntervalIndex.from_tuples : Construct from a sequence of tuples. 

655 

656 Examples 

657 -------- 

658 >>> pd.IntervalIndex.from_breaks([0, 1, 2, 3]) 

659 IntervalIndex([(0, 1], (1, 2], (2, 3]], 

660 dtype='interval[int64, right]') 

661 """ 

662 with rewrite_exception("IntervalArray", cls.__name__): 

663 array = IntervalArray.from_breaks( 

664 breaks, closed=closed, copy=copy, dtype=dtype 

665 ) 

666 return cls._simple_new(array, name=name) 

667 

668 @classmethod 

669 def from_arrays( 

670 cls, 

671 left, 

672 right, 

673 closed: IntervalClosedType = "right", 

674 name: Hashable | None = None, 

675 copy: bool = False, 

676 dtype: Dtype | None = None, 

677 ) -> IntervalIndex: 

678 """ 

679 Construct from two arrays defining the left and right bounds. 

680 

681 Each interval is formed by pairing the corresponding elements of 

682 *left* and *right*, producing an :class:`IntervalIndex` of the same 

683 length as the input arrays. 

684 

685 Parameters 

686 ---------- 

687 left : array-like (1-dimensional) 

688 Left bounds for each interval. 

689 right : array-like (1-dimensional) 

690 Right bounds for each interval. 

691 closed : {'left', 'right', 'both', 'neither'}, default 'right' 

692 Whether the intervals are closed on the left-side, right-side, both 

693 or neither. 

694 name : str, optional 

695 Name of the resulting IntervalIndex. 

696 copy : bool, default False 

697 Copy the data. 

698 dtype : dtype, optional 

699 If None, dtype will be inferred. 

700 

701 Returns 

702 ------- 

703 IntervalIndex 

704 

705 Raises 

706 ------ 

707 ValueError 

708 When a value is missing in only one of `left` or `right`. 

709 When a value in `left` is greater than the corresponding value 

710 in `right`. 

711 

712 See Also 

713 -------- 

714 interval_range : Function to create a fixed frequency IntervalIndex. 

715 IntervalIndex.from_breaks : Construct an IntervalIndex from an array of 

716 splits. 

717 IntervalIndex.from_tuples : Construct an IntervalIndex from an 

718 array-like of tuples. 

719 

720 Notes 

721 ----- 

722 Each element of `left` must be less than or equal to the `right` 

723 element at the same position. If an element is missing, it must be 

724 missing in both `left` and `right`. A TypeError is raised when 

725 using an unsupported type for `left` or `right`. At the moment, 

726 'category', 'object', and 'string' subtypes are not supported. 

727 

728 Examples 

729 -------- 

730 >>> pd.IntervalIndex.from_arrays([0, 1, 2], [1, 2, 3]) 

731 IntervalIndex([(0, 1], (1, 2], (2, 3]], 

732 dtype='interval[int64, right]') 

733 """ 

734 with rewrite_exception("IntervalArray", cls.__name__): 

735 array = IntervalArray.from_arrays( 

736 left, right, closed, copy=copy, dtype=dtype 

737 ) 

738 return cls._simple_new(array, name=name) 

739 

740 @classmethod 

741 def from_tuples( 

742 cls, 

743 data, 

744 closed: IntervalClosedType = "right", 

745 name: Hashable | None = None, 

746 copy: bool = False, 

747 dtype: Dtype | None = None, 

748 ) -> IntervalIndex: 

749 """ 

750 Construct an IntervalIndex from an array-like of tuples. 

751 

752 Each tuple in *data* should contain exactly two elements representing 

753 the left and right bounds of an interval. 

754 

755 Parameters 

756 ---------- 

757 data : array-like (1-dimensional) 

758 Array of tuples. 

759 closed : {'left', 'right', 'both', 'neither'}, default 'right' 

760 Whether the intervals are closed on the left-side, right-side, both 

761 or neither. 

762 name : str, optional 

763 Name of the resulting IntervalIndex. 

764 copy : bool, default False 

765 By-default copy the data, this is compat only and ignored. 

766 dtype : dtype or None, default None 

767 If None, dtype will be inferred. 

768 

769 Returns 

770 ------- 

771 IntervalIndex 

772 

773 See Also 

774 -------- 

775 interval_range : Function to create a fixed frequency IntervalIndex. 

776 IntervalIndex.from_arrays : Construct an IntervalIndex from a left and 

777 right array. 

778 IntervalIndex.from_breaks : Construct an IntervalIndex from an array of 

779 splits. 

780 

781 Examples 

782 -------- 

783 >>> pd.IntervalIndex.from_tuples([(0, 1), (1, 2)]) 

784 IntervalIndex([(0, 1], (1, 2]], 

785 dtype='interval[int64, right]') 

786 """ 

787 with rewrite_exception("IntervalArray", cls.__name__): 

788 arr = IntervalArray.from_tuples(data, closed=closed, copy=copy, dtype=dtype) 

789 return cls._simple_new(arr, name=name) 

790 

791 # -------------------------------------------------------------------- 

792 # error: Return type "IntervalTree" of "_engine" incompatible with return type 

793 # "Union[IndexEngine, ExtensionEngine]" in supertype "Index" 

794 @cache_readonly 

795 def _engine(self) -> IntervalTree: # type: ignore[override] 

796 # IntervalTree does not supports numpy array unless they are 64 bit 

797 left = self._maybe_convert_i8(self.left) 

798 left = maybe_upcast_numeric_to_64bit(left) 

799 right = self._maybe_convert_i8(self.right) 

800 right = maybe_upcast_numeric_to_64bit(right) 

801 return IntervalTree(left, right, closed=self.closed) 

802 

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

804 """ 

805 return a boolean if this key is IN the index 

806 We *only* accept an Interval 

807 

808 Parameters 

809 ---------- 

810 key : Interval 

811 

812 Returns 

813 ------- 

814 bool 

815 """ 

816 hash(key) 

817 if not isinstance(key, Interval): 

818 if is_valid_na_for_dtype(key, self.dtype): 

819 return self.hasnans 

820 return False 

821 

822 try: 

823 self.get_loc(key) 

824 return True 

825 except KeyError: 

826 return False 

827 

828 def _getitem_slice(self, slobj: slice) -> IntervalIndex: 

829 """ 

830 Fastpath for __getitem__ when we know we have a slice. 

831 """ 

832 res = self._data[slobj] 

833 return type(self)._simple_new(res, name=self._name) 

834 

835 @cache_readonly 

836 def _multiindex(self) -> MultiIndex: 

837 return MultiIndex.from_arrays([self.left, self.right], names=["left", "right"]) 

838 

839 def __reduce__(self): 

840 d = { 

841 "left": self.left, 

842 "right": self.right, 

843 "closed": self.closed, 

844 "name": self.name, 

845 } 

846 return _new_IntervalIndex, (type(self), d), None 

847 

848 @property 

849 def inferred_type(self) -> str: 

850 """Return a string of the type inferred from the values""" 

851 return "interval" 

852 

853 def memory_usage(self, deep: bool = False) -> int: 

854 """ 

855 Memory usage of the values. 

856 

857 Parameters 

858 ---------- 

859 deep : bool, default False 

860 Introspect the data deeply, interrogate 

861 `object` dtypes for system-level memory consumption. 

862 

863 Returns 

864 ------- 

865 bytes used 

866 Returns memory usage of the values in the Index in bytes. 

867 

868 See Also 

869 -------- 

870 numpy.ndarray.nbytes : Total bytes consumed by the elements of the 

871 array. 

872 

873 Notes 

874 ----- 

875 Memory usage does not include memory consumed by elements that 

876 are not components of the array if deep=False or if used on PyPy 

877 

878 Examples 

879 -------- 

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

881 >>> idx.memory_usage() 

882 24 

883 """ 

884 # we don't use an explicit engine 

885 # so return the bytes here 

886 return self.left.memory_usage(deep=deep) + self.right.memory_usage(deep=deep) 

887 

888 # IntervalTree doesn't have a is_monotonic_decreasing, so have to override 

889 # the Index implementation 

890 @cache_readonly 

891 def is_monotonic_decreasing(self) -> bool: 

892 """ 

893 Return True if the IntervalIndex is monotonic decreasing (only equal or 

894 decreasing values), else False 

895 """ 

896 return self[::-1].is_monotonic_increasing 

897 

898 @cache_readonly 

899 def is_unique(self) -> bool: 

900 """ 

901 Return True if the IntervalIndex contains unique elements, else False. 

902 """ 

903 left = self.left 

904 right = self.right 

905 

906 if self.isna().sum() > 1: 

907 return False 

908 

909 if left.is_unique or right.is_unique: 

910 return True 

911 

912 seen_pairs = set() 

913 check_idx = np.where(left.duplicated(keep=False))[0] 

914 for idx in check_idx: 

915 pair = (left[idx], right[idx]) 

916 if pair in seen_pairs: 

917 return False 

918 seen_pairs.add(pair) 

919 

920 return True 

921 

922 @property 

923 def is_overlapping(self) -> bool: 

924 """ 

925 Return True if the IntervalIndex has overlapping intervals, else False. 

926 

927 Two intervals overlap if they share a common point, including closed 

928 endpoints. Intervals that only have an open endpoint in common do not 

929 overlap, and empty intervals contain no points at all, so they never 

930 overlap. 

931 

932 Returns 

933 ------- 

934 bool 

935 Boolean indicating if the IntervalIndex has overlapping intervals. 

936 

937 See Also 

938 -------- 

939 Interval.overlaps : Check whether two Interval objects overlap. 

940 IntervalIndex.overlaps : Check an IntervalIndex elementwise for 

941 overlaps. 

942 

943 Examples 

944 -------- 

945 >>> index = pd.IntervalIndex.from_tuples([(0, 2), (1, 3), (4, 5)]) 

946 >>> index 

947 IntervalIndex([(0, 2], (1, 3], (4, 5]], 

948 dtype='interval[int64, right]') 

949 >>> index.is_overlapping 

950 True 

951 

952 Intervals that share closed endpoints overlap: 

953 

954 >>> index = pd.interval_range(0, 3, closed="both") 

955 >>> index 

956 IntervalIndex([[0, 1], [1, 2], [2, 3]], 

957 dtype='interval[int64, both]') 

958 >>> index.is_overlapping 

959 True 

960 

961 Intervals that only have an open endpoint in common do not overlap: 

962 

963 >>> index = pd.interval_range(0, 3, closed="left") 

964 >>> index 

965 IntervalIndex([[0, 1), [1, 2), [2, 3)], 

966 dtype='interval[int64, left]') 

967 >>> index.is_overlapping 

968 False 

969 

970 Empty intervals never overlap, even when nested inside another interval: 

971 

972 >>> index = pd.IntervalIndex.from_tuples([(0, 3), (1, 1)], closed="left") 

973 >>> index 

974 IntervalIndex([[0, 3), [1, 1)], 

975 dtype='interval[int64, left]') 

976 >>> index.is_overlapping 

977 False 

978 """ 

979 # GH 23309 

980 return self._engine.is_overlapping 

981 

982 def _needs_i8_conversion(self, key) -> bool: 

983 """ 

984 Check if a given key needs i8 conversion. Conversion is necessary for 

985 Timestamp, Timedelta, DatetimeIndex, and TimedeltaIndex keys. An 

986 Interval-like requires conversion if its endpoints are one of the 

987 aforementioned types. 

988 

989 Assumes that any list-like data has already been cast to an Index. 

990 

991 Parameters 

992 ---------- 

993 key : scalar or Index-like 

994 The key that should be checked for i8 conversion 

995 

996 Returns 

997 ------- 

998 bool 

999 """ 

1000 key_dtype = getattr(key, "dtype", None) 

1001 if isinstance(key_dtype, IntervalDtype) or isinstance(key, Interval): 

1002 return self._needs_i8_conversion(key.left) 

1003 

1004 i8_types = (Timestamp, Timedelta, DatetimeIndex, TimedeltaIndex) 

1005 return isinstance(key, i8_types) 

1006 

1007 def _maybe_convert_i8(self, key): 

1008 """ 

1009 Maybe convert a given key to its equivalent i8 value(s). Used as a 

1010 preprocessing step prior to IntervalTree queries (self._engine), which 

1011 expects numeric data. 

1012 

1013 Parameters 

1014 ---------- 

1015 key : scalar or list-like 

1016 The key that should maybe be converted to i8. 

1017 

1018 Returns 

1019 ------- 

1020 scalar or list-like 

1021 The original key if no conversion occurred, int if converted scalar, 

1022 Index with an int64 dtype if converted list-like. 

1023 """ 

1024 if is_list_like(key): 

1025 key = ensure_index(key) 

1026 key = maybe_upcast_numeric_to_64bit(key) 

1027 

1028 if not self._needs_i8_conversion(key): 

1029 return key 

1030 

1031 scalar = is_scalar(key) 

1032 key_dtype = getattr(key, "dtype", None) 

1033 if isinstance(key_dtype, IntervalDtype) or isinstance(key, Interval): 

1034 # convert left/right and reconstruct 

1035 left = self._maybe_convert_i8(key.left) 

1036 right = self._maybe_convert_i8(key.right) 

1037 constructor = Interval if scalar else IntervalIndex.from_arrays 

1038 return constructor(left, right, closed=self.closed) 

1039 

1040 if scalar: 

1041 # Timestamp/Timedelta 

1042 key_dtype, key_i8 = infer_dtype_from_scalar(key) 

1043 if isinstance(key, Period): 

1044 key_i8 = key.ordinal 

1045 elif isinstance(key_i8, Timestamp): 

1046 key_i8 = key_i8._value 

1047 elif isinstance(key_i8, (np.datetime64, np.timedelta64)): 

1048 key_i8 = key_i8.view("i8") 

1049 else: 

1050 # DatetimeIndex/TimedeltaIndex 

1051 key_dtype, key_i8 = key.dtype, Index(key.asi8, copy=False) 

1052 if key.hasnans: 

1053 # convert NaT from its i8 value to np.nan so it's not viewed 

1054 # as a valid value, maybe causing errors (e.g. is_overlapping) 

1055 key_i8 = key_i8.where(~key._isnan) 

1056 

1057 # ensure consistency with IntervalIndex subtype 

1058 # error: Item "ExtensionDtype"/"dtype[Any]" of "Union[dtype[Any], 

1059 # ExtensionDtype]" has no attribute "subtype" 

1060 subtype = self.dtype.subtype # type: ignore[union-attr] 

1061 

1062 if subtype != key_dtype: 

1063 raise ValueError( 

1064 f"Cannot index an IntervalIndex of subtype {subtype} with " 

1065 f"values of dtype {key_dtype}" 

1066 ) 

1067 

1068 return key_i8 

1069 

1070 def _searchsorted_monotonic(self, label, side: Literal["left", "right"] = "left"): 

1071 if not self.is_non_overlapping_monotonic: 

1072 raise KeyError( 

1073 "can only get slices from an IntervalIndex if bounds are " 

1074 "non-overlapping and all monotonic increasing or decreasing" 

1075 ) 

1076 

1077 if isinstance(label, (IntervalMixin, IntervalIndex)): 

1078 raise NotImplementedError("Interval objects are not currently supported") 

1079 

1080 # GH 20921: "not is_monotonic_increasing" for the second condition 

1081 # instead of "is_monotonic_decreasing" to account for single element 

1082 # indexes being both increasing and decreasing 

1083 if (side == "left" and self.left.is_monotonic_increasing) or ( 

1084 side == "right" and not self.left.is_monotonic_increasing 

1085 ): 

1086 sub_idx = self.right 

1087 if self.open_right: 

1088 label = _get_next_label(label) 

1089 else: 

1090 sub_idx = self.left 

1091 if self.open_left: 

1092 label = _get_prev_label(label) 

1093 

1094 return sub_idx._searchsorted_monotonic(label, side) 

1095 

1096 # -------------------------------------------------------------------- 

1097 # Indexing Methods 

1098 

1099 def get_loc(self, key) -> int | slice | np.ndarray: 

1100 """ 

1101 Get integer location, slice or boolean mask for requested label. 

1102 

1103 The `get_loc` method is used to retrieve the integer index, a slice for 

1104 slicing objects, or a boolean mask indicating the presence of the label 

1105 in the `IntervalIndex`. 

1106 

1107 Parameters 

1108 ---------- 

1109 key : label 

1110 The value or range to find in the IntervalIndex. 

1111 

1112 Returns 

1113 ------- 

1114 int if unique index, slice if monotonic index, else mask 

1115 The position or positions found. This could be a single 

1116 number, a range, or an array of true/false values 

1117 indicating the position(s) of the label. 

1118 

1119 See Also 

1120 -------- 

1121 IntervalIndex.get_indexer_non_unique : Compute indexer and 

1122 mask for new index given the current index. 

1123 Index.get_loc : Similar method in the base Index class. 

1124 

1125 Examples 

1126 -------- 

1127 >>> i1, i2 = pd.Interval(0, 1), pd.Interval(1, 2) 

1128 >>> index = pd.IntervalIndex([i1, i2]) 

1129 >>> index.get_loc(1) 

1130 0 

1131 

1132 You can also supply a point inside an interval. 

1133 

1134 >>> index.get_loc(1.5) 

1135 1 

1136 

1137 If a label is in several intervals, you get the locations of all the 

1138 relevant intervals. 

1139 

1140 >>> i3 = pd.Interval(0, 2) 

1141 >>> overlapping_index = pd.IntervalIndex([i1, i2, i3]) 

1142 >>> overlapping_index.get_loc(0.5) 

1143 array([ True, False, True]) 

1144 

1145 Only exact matches will be returned if an interval is provided. 

1146 

1147 >>> index.get_loc(pd.Interval(0, 1)) 

1148 0 

1149 """ 

1150 self._check_indexing_error(key) 

1151 

1152 if isinstance(key, Interval): 

1153 if self.closed != key.closed: 

1154 raise KeyError(key) 

1155 mask = (self.left == key.left) & (self.right == key.right) 

1156 elif is_valid_na_for_dtype(key, self.dtype): 

1157 mask = self.isna() 

1158 else: 

1159 # assume scalar 

1160 op_left = le if self.closed_left else lt 

1161 op_right = le if self.closed_right else lt 

1162 try: 

1163 mask = op_left(self.left, key) & op_right(key, self.right) 

1164 except TypeError as err: 

1165 # scalar is not comparable to II subtype --> invalid label 

1166 raise KeyError(key) from err 

1167 

1168 matches = mask.sum() 

1169 if matches == 0: 

1170 raise KeyError(key) 

1171 if matches == 1: 

1172 return maybe_unbox_numpy_scalar(mask.argmax()) 

1173 

1174 res = lib.maybe_booleans_to_slice(mask.view("u1")) 

1175 if isinstance(res, slice) and res.stop is None: 

1176 # TODO: DO this in maybe_booleans_to_slice? 

1177 res = slice(res.start, len(self), res.step) 

1178 return res 

1179 

1180 def _get_indexer( 

1181 self, 

1182 target: Index, 

1183 method: str | None = None, 

1184 limit: int | None = None, 

1185 tolerance: Any | None = None, 

1186 ) -> npt.NDArray[np.intp]: 

1187 if isinstance(target, IntervalIndex): 

1188 # We only get here with not self.is_overlapping 

1189 # -> at most one match per interval in target 

1190 # want exact matches -> need both left/right to match, so defer to 

1191 # left/right get_indexer, compare elementwise, equality -> match 

1192 if self.left.is_unique and self.right.is_unique: 

1193 indexer = self._get_indexer_unique_sides(target) 

1194 else: 

1195 indexer = self._get_indexer_pointwise(target)[0] 

1196 

1197 elif not (is_object_dtype(target.dtype) or is_string_dtype(target.dtype)): 

1198 # homogeneous scalar index 

1199 # we should always have self._should_partial_index(target) here 

1200 if self.is_monotonic_increasing and self.right.is_monotonic_increasing: 

1201 # GH#47614 - use searchsorted for O(n*log(m)) instead of 

1202 # IntervalTree which scales poorly for large target arrays. 

1203 # right is checked too: an empty interval can nest inside 

1204 # another without overlapping it (GH#26893) 

1205 indexer = self._get_indexer_monotonic(target) 

1206 else: 

1207 target = self._maybe_convert_i8(target) 

1208 indexer = self._engine.get_indexer(cast("np.ndarray", target.values)) 

1209 else: 

1210 # heterogeneous scalar index: defer elementwise to get_loc 

1211 # we should always have self._should_partial_index(target) here 

1212 return self._get_indexer_pointwise(target)[0] 

1213 

1214 return ensure_platform_int(indexer) 

1215 

1216 def get_indexer_non_unique( 

1217 self, target: Axes 

1218 ) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]: 

1219 """ 

1220 Compute indexer and mask for new index given the current index. 

1221 

1222 The indexer should be then used as an input to ndarray.take to align the 

1223 current data to the new index. 

1224 

1225 Parameters 

1226 ---------- 

1227 target : IntervalIndex or list of Intervals 

1228 An iterable containing the values to be used for computing indexer. 

1229 

1230 Returns 

1231 ------- 

1232 indexer : np.ndarray[np.intp] 

1233 Integers from 0 to n - 1 indicating that the index at these 

1234 positions matches the corresponding target values. Missing values 

1235 in the target are marked by -1. 

1236 missing : np.ndarray[np.intp] 

1237 An indexer into the target of the values not found. 

1238 These correspond to the -1 in the indexer array. 

1239 

1240 See Also 

1241 -------- 

1242 Index.get_indexer : Computes indexer and mask for new index given 

1243 the current index. 

1244 Index.get_indexer_for : Returns an indexer even when non-unique. 

1245 

1246 Examples 

1247 -------- 

1248 >>> index = pd.Index(["c", "b", "a", "b", "b"]) 

1249 >>> index.get_indexer_non_unique(["b", "b"]) 

1250 (array([1, 3, 4, 1, 3, 4]), array([], dtype=int64)) 

1251 

1252 In the example below there are no matched values. 

1253 

1254 >>> index = pd.Index(["c", "b", "a", "b", "b"]) 

1255 >>> index.get_indexer_non_unique(["q", "r", "t"]) 

1256 (array([-1, -1, -1]), array([0, 1, 2])) 

1257 

1258 For this reason, the returned ``indexer`` contains only integers equal to -1. 

1259 It demonstrates that there's no match between the index and the ``target`` 

1260 values at these positions. The mask [0, 1, 2] in the return value shows that 

1261 the first, second, and third elements are missing. 

1262 

1263 Notice that the return value is a tuple contains two items. In the example 

1264 below the first item is an array of locations in ``index``. The second 

1265 item is a mask shows that the first and third elements are missing. 

1266 

1267 >>> index = pd.Index(["c", "b", "a", "b", "b"]) 

1268 >>> index.get_indexer_non_unique(["f", "b", "s"]) 

1269 (array([-1, 1, 3, 4, -1]), array([0, 2])) 

1270 """ 

1271 target = ensure_index(target) 

1272 

1273 if not self._should_compare(target) and not self._should_partial_index(target): 

1274 # e.g. IntervalIndex with different closed or incompatible subtype 

1275 # -> no matches 

1276 return self._get_indexer_non_comparable(target, None, unique=False) 

1277 

1278 elif isinstance(target, IntervalIndex): 

1279 if self.left.is_unique and self.right.is_unique: 

1280 # fastpath available even if we don't have self._index_as_unique 

1281 indexer = self._get_indexer_unique_sides(target) 

1282 missing = (indexer == -1).nonzero()[0] 

1283 else: 

1284 return self._get_indexer_pointwise(target) 

1285 

1286 elif is_object_dtype(target.dtype) or not self._should_partial_index(target): 

1287 # target might contain intervals: defer elementwise to get_loc 

1288 return self._get_indexer_pointwise(target) 

1289 

1290 else: 

1291 # Note: this case behaves differently from other Index subclasses 

1292 # because IntervalIndex does partial-int indexing 

1293 target = self._maybe_convert_i8(target) 

1294 indexer, missing = self._engine.get_indexer_non_unique( 

1295 cast("np.ndarray", target.values) # type: ignore[union-attr] 

1296 ) 

1297 

1298 return ensure_platform_int(indexer), ensure_platform_int(missing) 

1299 

1300 def _get_indexer_unique_sides(self, target: IntervalIndex) -> npt.NDArray[np.intp]: 

1301 """ 

1302 _get_indexer specialized to the case where both of our sides are unique. 

1303 """ 

1304 # Caller is responsible for checking 

1305 # `self.left.is_unique and self.right.is_unique` 

1306 

1307 left_indexer = self.left.get_indexer(target.left) 

1308 right_indexer = self.right.get_indexer(target.right) 

1309 indexer = np.where(left_indexer == right_indexer, left_indexer, -1) 

1310 return indexer 

1311 

1312 def _get_indexer_monotonic(self, target: Index) -> npt.NDArray[np.intp]: 

1313 """ 

1314 Use searchsorted on endpoints for O(n*log(m)) scalar lookups on 

1315 a monotonic non-overlapping IntervalIndex, instead of IntervalTree 

1316 which scales poorly for large target arrays. See GH#47614. 

1317 """ 

1318 # Caller is responsible for checking self.is_monotonic_increasing 

1319 closed_right = self.closed in ("right", "both") 

1320 closed_left = self.closed in ("left", "both") 

1321 

1322 # searchsorted on right endpoints to find candidate bin 

1323 side: Literal["left", "right"] = "left" if closed_right else "right" 

1324 indexer = self.right.searchsorted(target, side=side) 

1325 

1326 nbins = len(self) 

1327 past_end = indexer >= nbins 

1328 indexer = np.minimum(indexer, nbins - 1) 

1329 

1330 # Verify values fall within the candidate bin's left bound 

1331 left_values = self.left[indexer] 

1332 if closed_left: 

1333 left_miss = target < left_values 

1334 else: 

1335 left_miss = target <= left_values 

1336 

1337 na_mask = isna(target) 

1338 invalid = past_end | left_miss | na_mask 

1339 indexer = np.where(invalid, -1, indexer) 

1340 return ensure_platform_int(indexer) 

1341 

1342 def _get_indexer_pointwise( 

1343 self, target: Index 

1344 ) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]: 

1345 """ 

1346 pointwise implementation for get_indexer and get_indexer_non_unique. 

1347 """ 

1348 indexer, missing = [], [] 

1349 for i, key in enumerate(target): 

1350 try: 

1351 locs = self.get_loc(key) 

1352 if isinstance(locs, slice): 

1353 # Only needed for get_indexer_non_unique 

1354 locs = np.arange(locs.start, locs.stop, locs.step, dtype="intp") 

1355 elif lib.is_integer(locs): 

1356 locs = np.array(locs, ndmin=1) 

1357 else: 

1358 # otherwise we have ndarray[bool] 

1359 locs = np.where(locs)[0] # type: ignore[arg-type] 

1360 except KeyError: 

1361 missing.append(i) 

1362 locs = np.array([-1]) 

1363 except InvalidIndexError: 

1364 # i.e. non-scalar key e.g. a tuple. 

1365 # see test_append_different_columns_types_raises 

1366 missing.append(i) 

1367 locs = np.array([-1]) 

1368 

1369 indexer.append(locs) 

1370 

1371 concatenated_indexer = np.concatenate(indexer) 

1372 return ensure_platform_int(concatenated_indexer), ensure_platform_int(missing) 

1373 

1374 @cache_readonly 

1375 def _index_as_unique(self) -> bool: 

1376 return not self.is_overlapping and self._engine._na_count < 2 

1377 

1378 _requires_unique_msg = ( 

1379 "cannot handle overlapping indices; use IntervalIndex.get_indexer_non_unique" 

1380 ) 

1381 

1382 def _convert_slice_indexer(self, key: slice, kind: Literal["loc", "getitem"]): 

1383 if not (key.step is None or key.step == 1): 

1384 # GH#31658 if label-based, we require step == 1, 

1385 # if positional, we disallow float start/stop 

1386 msg = "label-based slicing with step!=1 is not supported for IntervalIndex" 

1387 if kind == "loc": 

1388 raise ValueError(msg) 

1389 if kind == "getitem": 

1390 if not is_valid_positional_slice(key): 

1391 # i.e. this cannot be interpreted as a positional slice 

1392 raise ValueError(msg) 

1393 

1394 return super()._convert_slice_indexer(key, kind) 

1395 

1396 @cache_readonly 

1397 def _should_fallback_to_positional(self) -> bool: 

1398 # integer lookups in Series.__getitem__ are unambiguously 

1399 # positional in this case 

1400 # error: Item "ExtensionDtype"/"dtype[Any]" of "Union[dtype[Any], 

1401 # ExtensionDtype]" has no attribute "subtype" 

1402 return self.dtype.subtype.kind in "mM" # type: ignore[union-attr] 

1403 

1404 def _maybe_cast_slice_bound(self, label, side: str): 

1405 return getattr(self, side)._maybe_cast_slice_bound(label, side) 

1406 

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

1408 if not isinstance(dtype, IntervalDtype): 

1409 return False 

1410 common_subtype = find_common_type([self.dtype, dtype]) 

1411 return not is_object_dtype(common_subtype) 

1412 

1413 # -------------------------------------------------------------------- 

1414 

1415 @cache_readonly 

1416 def left(self) -> Index: 

1417 """ 

1418 Return left bounds of the intervals in the IntervalIndex. 

1419 

1420 The left bounds of each interval in the IntervalIndex are 

1421 returned as an Index. The datatype of the left bounds is the 

1422 same as the datatype of the endpoints of the intervals. 

1423 

1424 Returns 

1425 ------- 

1426 Index 

1427 An Index containing the left bounds of the intervals. 

1428 

1429 See Also 

1430 -------- 

1431 IntervalIndex.right : Return the right bounds of the intervals 

1432 in the IntervalIndex. 

1433 IntervalIndex.mid : Return the mid-point of the intervals in 

1434 the IntervalIndex. 

1435 IntervalIndex.length : Return the length of the intervals in 

1436 the IntervalIndex. 

1437 

1438 Examples 

1439 -------- 

1440 >>> iv_idx = pd.IntervalIndex.from_arrays([1, 2, 3], [4, 5, 6], closed="right") 

1441 >>> iv_idx.left 

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

1443 

1444 >>> iv_idx = pd.IntervalIndex.from_tuples( 

1445 ... [(1, 4), (2, 5), (3, 6)], closed="left" 

1446 ... ) 

1447 >>> iv_idx.left 

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

1449 """ 

1450 return Index(self._data.left, copy=False) 

1451 

1452 @cache_readonly 

1453 def right(self) -> Index: 

1454 """ 

1455 Return right bounds of the intervals in the IntervalIndex. 

1456 

1457 The right bounds of each interval in the IntervalIndex are 

1458 returned as an Index. The datatype of the right bounds is the 

1459 same as the datatype of the endpoints of the intervals. 

1460 

1461 Returns 

1462 ------- 

1463 Index 

1464 An Index containing the right bounds of the intervals. 

1465 

1466 See Also 

1467 -------- 

1468 IntervalIndex.left : Return the left bounds of the intervals 

1469 in the IntervalIndex. 

1470 IntervalIndex.mid : Return the mid-point of the intervals in 

1471 the IntervalIndex. 

1472 IntervalIndex.length : Return the length of the intervals in 

1473 the IntervalIndex. 

1474 

1475 Examples 

1476 -------- 

1477 >>> iv_idx = pd.IntervalIndex.from_arrays([1, 2, 3], [4, 5, 6], closed="right") 

1478 >>> iv_idx.right 

1479 Index([4, 5, 6], dtype='int64') 

1480 

1481 >>> iv_idx = pd.IntervalIndex.from_tuples( 

1482 ... [(1, 4), (2, 5), (3, 6)], closed="left" 

1483 ... ) 

1484 >>> iv_idx.right 

1485 Index([4, 5, 6], dtype='int64') 

1486 """ 

1487 return Index(self._data.right, copy=False) 

1488 

1489 @cache_readonly 

1490 def mid(self) -> Index: 

1491 """ 

1492 Return the midpoint of each interval in the IntervalIndex as an Index. 

1493 

1494 Each midpoint is calculated as the average of the left and right bounds 

1495 of each interval. The midpoints are returned as a pandas Index object. 

1496 

1497 Returns 

1498 ------- 

1499 pandas.Index 

1500 An Index containing the midpoints of each interval. 

1501 

1502 See Also 

1503 -------- 

1504 IntervalIndex.left : Return the left bounds of the intervals 

1505 in the IntervalIndex. 

1506 IntervalIndex.right : Return the right bounds of the intervals 

1507 in the IntervalIndex. 

1508 IntervalIndex.length : Return the length of the intervals in 

1509 the IntervalIndex. 

1510 

1511 Notes 

1512 ----- 

1513 The midpoint is the average of the interval bounds, potentially resulting 

1514 in a floating-point number even if bounds are integers. The returned Index 

1515 will have a dtype that accurately holds the midpoints. This computation is 

1516 the same regardless of whether intervals are open or closed. 

1517 

1518 Examples 

1519 -------- 

1520 >>> iv_idx = pd.IntervalIndex.from_arrays([1, 2, 3], [4, 5, 6]) 

1521 >>> iv_idx.mid 

1522 Index([2.5, 3.5, 4.5], dtype='float64') 

1523 

1524 >>> iv_idx = pd.IntervalIndex.from_tuples([(1, 4), (2, 5), (3, 6)]) 

1525 >>> iv_idx.mid 

1526 Index([2.5, 3.5, 4.5], dtype='float64') 

1527 """ 

1528 return Index(self._data.mid, copy=False) 

1529 

1530 @property 

1531 def length(self) -> Index: 

1532 """ 

1533 Calculate the length of each interval in the IntervalIndex. 

1534 

1535 This method returns a new Index containing the lengths of each interval 

1536 in the IntervalIndex. The length of an interval is defined as the difference 

1537 between its end and its start. 

1538 

1539 Returns 

1540 ------- 

1541 Index 

1542 An Index containing the lengths of each interval. 

1543 

1544 See Also 

1545 -------- 

1546 Interval.length : Return the length of the Interval. 

1547 

1548 Examples 

1549 -------- 

1550 >>> intervals = pd.IntervalIndex.from_arrays( 

1551 ... [1, 2, 3], [4, 5, 6], closed="right" 

1552 ... ) 

1553 >>> intervals.length 

1554 Index([3, 3, 3], dtype='int64') 

1555 

1556 >>> intervals = pd.IntervalIndex.from_tuples([(1, 5), (6, 10), (11, 15)]) 

1557 >>> intervals.length 

1558 Index([4, 4, 4], dtype='int64') 

1559 """ 

1560 return Index(self._data.length, copy=False) 

1561 

1562 # -------------------------------------------------------------------- 

1563 # Set Operations 

1564 

1565 def _intersection(self, other, sort: bool = False): 

1566 """ 

1567 intersection specialized to the case with matching dtypes. 

1568 """ 

1569 # For IntervalIndex we also know other.closed == self.closed 

1570 if self.left.is_unique and self.right.is_unique: 

1571 taken = self._intersection_unique(other) 

1572 elif other.left.is_unique and other.right.is_unique and self.isna().sum() <= 1: 

1573 # Swap other/self if other is unique and self does not have 

1574 # multiple NaNs 

1575 taken = other._intersection_unique(self) 

1576 else: 

1577 # duplicates 

1578 taken = self._intersection_non_unique(other) 

1579 

1580 if sort: 

1581 taken = taken.sort_values() 

1582 

1583 return taken 

1584 

1585 def _intersection_unique(self, other: IntervalIndex) -> IntervalIndex: 

1586 """ 

1587 Used when the IntervalIndex does not have any common endpoint, 

1588 no matter left or right. 

1589 Return the intersection with another IntervalIndex. 

1590 Parameters 

1591 ---------- 

1592 other : IntervalIndex 

1593 Returns 

1594 ------- 

1595 IntervalIndex 

1596 """ 

1597 # Note: this is much more performant than super()._intersection(other) 

1598 lindexer = self.left.get_indexer(other.left) 

1599 rindexer = self.right.get_indexer(other.right) 

1600 

1601 match = (lindexer == rindexer) & (lindexer != -1) 

1602 indexer = lindexer.take(match.nonzero()[0]) 

1603 indexer = unique(indexer) 

1604 

1605 return self.take(indexer) 

1606 

1607 def _intersection_non_unique(self, other: IntervalIndex) -> IntervalIndex: 

1608 """ 

1609 Used when the IntervalIndex does have some common endpoints, 

1610 on either sides. 

1611 Return the intersection with another IntervalIndex. 

1612 

1613 Parameters 

1614 ---------- 

1615 other : IntervalIndex 

1616 

1617 Returns 

1618 ------- 

1619 IntervalIndex 

1620 """ 

1621 # Note: this is about 3.25x faster than super()._intersection(other) 

1622 # in IntervalIndexMethod.time_intersection_both_duplicate(1000) 

1623 mask = np.zeros(len(self), dtype=bool) 

1624 

1625 if self.hasnans and other.hasnans: 

1626 first_nan_loc = np.arange(len(self))[self.isna()][0] 

1627 mask[first_nan_loc] = True 

1628 

1629 other_tups = set(zip(other.left, other.right, strict=True)) 

1630 for i, tup in enumerate(zip(self.left, self.right, strict=True)): 

1631 if tup in other_tups: 

1632 mask[i] = True 

1633 

1634 return self[mask] 

1635 

1636 # -------------------------------------------------------------------- 

1637 

1638 def _get_engine_target(self) -> np.ndarray: 

1639 # Note: we _could_ use libjoin functions by either casting to object 

1640 # dtype or constructing tuples (faster than constructing Intervals) 

1641 # but the libjoin fastpaths are no longer fast in these cases. 

1642 raise NotImplementedError( 

1643 "IntervalIndex does not use libjoin fastpaths or pass values to " 

1644 "IndexEngine objects" 

1645 ) 

1646 

1647 def _from_join_target(self, result): 

1648 raise NotImplementedError("IntervalIndex does not use libjoin fastpaths") 

1649 

1650 # TODO: arithmetic operations 

1651 

1652 

1653def _is_valid_endpoint(endpoint) -> bool: 

1654 """ 

1655 Helper for interval_range to check if start/end are valid types. 

1656 """ 

1657 return any( 

1658 [ 

1659 is_number(endpoint), 

1660 isinstance(endpoint, Timestamp), 

1661 isinstance(endpoint, Timedelta), 

1662 endpoint is None, 

1663 ] 

1664 ) 

1665 

1666 

1667def _is_type_compatible(a, b) -> bool: 

1668 """ 

1669 Helper for interval_range to check type compat of start/end/freq. 

1670 """ 

1671 is_ts_compat = lambda x: isinstance(x, (Timestamp, BaseOffset)) 

1672 is_td_compat = lambda x: isinstance(x, (Timedelta, BaseOffset)) 

1673 return ( 

1674 (is_number(a) and is_number(b)) 

1675 or (is_ts_compat(a) and is_ts_compat(b)) 

1676 or (is_td_compat(a) and is_td_compat(b)) 

1677 or com.any_none(a, b) 

1678 ) 

1679 

1680 

1681@set_module("pandas") 

1682def interval_range( 

1683 start=None, 

1684 end=None, 

1685 periods=None, 

1686 freq=None, 

1687 name: Hashable | None = None, 

1688 closed: IntervalClosedType = "right", 

1689) -> IntervalIndex: 

1690 """ 

1691 Return a fixed frequency IntervalIndex. 

1692 

1693 This function generates a sequence of evenly spaced intervals over a 

1694 specified range, supporting numeric, datetime-like, and timedelta 

1695 endpoints. 

1696 

1697 Parameters 

1698 ---------- 

1699 start : numeric or datetime-like, default None 

1700 Left bound for generating intervals. 

1701 end : numeric or datetime-like, default None 

1702 Right bound for generating intervals. 

1703 periods : int, default None 

1704 Number of periods to generate. 

1705 freq : numeric, str, Timedelta, datetime.timedelta, or DateOffset, default None 

1706 The length of each interval. Must be consistent with the type of start 

1707 and end, e.g. 2 for numeric, or '5H' for datetime-like. Default is 1 

1708 for numeric and 'D' for datetime-like. 

1709 name : str, default None 

1710 Name of the resulting IntervalIndex. 

1711 closed : {'left', 'right', 'both', 'neither'}, default 'right' 

1712 Whether the intervals are closed on the left-side, right-side, both 

1713 or neither. 

1714 

1715 Returns 

1716 ------- 

1717 IntervalIndex 

1718 Object with a fixed frequency. 

1719 

1720 See Also 

1721 -------- 

1722 IntervalIndex : An Index of intervals that are all closed on the same side. 

1723 

1724 Notes 

1725 ----- 

1726 Of the four parameters ``start``, ``end``, ``periods``, and ``freq``, 

1727 exactly three must be specified. If ``freq`` is omitted, the resulting 

1728 ``IntervalIndex`` will have ``periods`` linearly spaced elements between 

1729 ``start`` and ``end``, inclusively. 

1730 

1731 To learn more about datetime-like frequency strings, please see 

1732 :ref:`this link<timeseries.offset_aliases>`. 

1733 

1734 Examples 

1735 -------- 

1736 Numeric ``start`` and ``end`` is supported. 

1737 

1738 >>> pd.interval_range(start=0, end=5) 

1739 IntervalIndex([(0, 1], (1, 2], (2, 3], (3, 4], (4, 5]], 

1740 dtype='interval[int64, right]') 

1741 

1742 Additionally, datetime-like input is also supported. 

1743 

1744 >>> pd.interval_range( 

1745 ... start=pd.Timestamp("2017-01-01"), end=pd.Timestamp("2017-01-04") 

1746 ... ) 

1747 IntervalIndex([(2017-01-01 00:00:00, 2017-01-02 00:00:00], 

1748 (2017-01-02 00:00:00, 2017-01-03 00:00:00], 

1749 (2017-01-03 00:00:00, 2017-01-04 00:00:00]], 

1750 dtype='interval[datetime64[us], right]') 

1751 

1752 The ``freq`` parameter specifies the frequency between the left and right. 

1753 endpoints of the individual intervals within the ``IntervalIndex``. For 

1754 numeric ``start`` and ``end``, the frequency must also be numeric. 

1755 

1756 >>> pd.interval_range(start=0, periods=4, freq=1.5) 

1757 IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0]], 

1758 dtype='interval[float64, right]') 

1759 

1760 Similarly, for datetime-like ``start`` and ``end``, the frequency must be 

1761 convertible to a DateOffset. 

1762 

1763 >>> pd.interval_range(start=pd.Timestamp("2017-01-01"), periods=3, freq="MS") 

1764 IntervalIndex([(2017-01-01 00:00:00, 2017-02-01 00:00:00], 

1765 (2017-02-01 00:00:00, 2017-03-01 00:00:00], 

1766 (2017-03-01 00:00:00, 2017-04-01 00:00:00]], 

1767 dtype='interval[datetime64[us], right]') 

1768 

1769 Specify ``start``, ``end``, and ``periods``; the frequency is generated 

1770 automatically (linearly spaced). 

1771 

1772 >>> pd.interval_range(start=0, end=6, periods=4) 

1773 IntervalIndex([(0.0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0]], 

1774 dtype='interval[float64, right]') 

1775 

1776 The ``closed`` parameter specifies which endpoints of the individual 

1777 intervals within the ``IntervalIndex`` are closed. 

1778 

1779 >>> pd.interval_range(end=5, periods=4, closed="both") 

1780 IntervalIndex([[1, 2], [2, 3], [3, 4], [4, 5]], 

1781 dtype='interval[int64, both]') 

1782 """ 

1783 start = maybe_box_datetimelike(start) 

1784 end = maybe_box_datetimelike(end) 

1785 endpoint = start if start is not None else end 

1786 

1787 if freq is None and com.any_none(periods, start, end): 

1788 freq = 1 if is_number(endpoint) else "D" 

1789 

1790 if com.count_not_none(start, end, periods, freq) != 3: 

1791 raise ValueError( 

1792 "Of the four parameters: start, end, periods, and " 

1793 "freq, exactly three must be specified" 

1794 ) 

1795 

1796 if not _is_valid_endpoint(start): 

1797 raise ValueError(f"start must be numeric or datetime-like, got {start}") 

1798 if not _is_valid_endpoint(end): 

1799 raise ValueError(f"end must be numeric or datetime-like, got {end}") 

1800 

1801 periods = validate_periods(periods) 

1802 

1803 if freq is not None and not is_number(freq): 

1804 try: 

1805 freq = to_offset(freq) 

1806 except ValueError as err: 

1807 raise ValueError( 

1808 f"freq must be numeric or convertible to DateOffset, got {freq}" 

1809 ) from err 

1810 

1811 # verify type compatibility 

1812 if not all( 

1813 [ 

1814 _is_type_compatible(start, end), 

1815 _is_type_compatible(start, freq), 

1816 _is_type_compatible(end, freq), 

1817 ] 

1818 ): 

1819 raise TypeError("start, end, freq need to be type compatible") 

1820 

1821 # +1 to convert interval count to breaks count (n breaks = n-1 intervals) 

1822 if periods is not None: 

1823 periods += 1 

1824 

1825 breaks: np.ndarray | TimedeltaIndex | DatetimeIndex 

1826 

1827 if is_number(endpoint): 

1828 dtype: np.dtype = np.dtype("int64") 

1829 if com.all_not_none(start, end, freq): 

1830 if ( 

1831 isinstance(start, (np.integer, np.floating)) 

1832 and isinstance(end, (np.integer, np.floating)) 

1833 and start.dtype == end.dtype 

1834 ): 

1835 dtype = start.dtype 

1836 elif ( 

1837 isinstance(start, (float, np.floating)) 

1838 or isinstance(end, (float, np.floating)) 

1839 or isinstance(freq, (float, np.floating)) 

1840 ): 

1841 dtype = np.dtype("float64") 

1842 # 0.1 ensures we capture end 

1843 breaks = np.arange(start, end + (freq * 0.1), freq) 

1844 breaks = maybe_downcast_numeric(breaks, dtype) 

1845 else: 

1846 # compute the period/start/end if unspecified (at most one) 

1847 if periods is None: 

1848 periods = int((end - start) // freq) + 1 

1849 elif start is None: 

1850 start = end - (periods - 1) * freq 

1851 elif end is None: 

1852 end = start + (periods - 1) * freq 

1853 

1854 breaks = np.linspace(start, end, periods) 

1855 if all(is_integer(x) for x in com.not_none(start, end, freq)): 

1856 # np.linspace always produces float output 

1857 breaks = maybe_downcast_numeric(breaks, dtype) 

1858 # delegate to the appropriate range function 

1859 elif isinstance(endpoint, Timestamp): 

1860 breaks = date_range(start=start, end=end, periods=periods, freq=freq) 

1861 else: 

1862 breaks = timedelta_range(start=start, end=end, periods=periods, freq=freq) 

1863 

1864 return IntervalIndex.from_breaks( 

1865 breaks, 

1866 name=name, 

1867 closed=closed, 

1868 dtype=IntervalDtype(subtype=breaks.dtype, closed=closed), 

1869 )