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

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

609 statements  

1from __future__ import annotations 

2 

3import operator 

4from operator import ( 

5 le, 

6 lt, 

7) 

8import textwrap 

9from typing import ( 

10 TYPE_CHECKING, 

11 Literal, 

12 Self, 

13 TypeAlias, 

14 overload, 

15) 

16 

17import numpy as np 

18 

19from pandas._libs import lib 

20from pandas._libs.interval import ( 

21 VALID_CLOSED, 

22 Interval, 

23 IntervalMixin, 

24 intervals_to_interval_bounds, 

25) 

26from pandas._libs.missing import NA 

27from pandas._typing import ( 

28 ArrayLike, 

29 AxisInt, 

30 Dtype, 

31 IntervalClosedType, 

32 NpDtype, 

33 PositionalIndexer, 

34 ScalarIndexer, 

35 SequenceIndexer, 

36 SortKind, 

37 TimeArrayLike, 

38 npt, 

39) 

40from pandas.compat.numpy import function as nv 

41from pandas.errors import IntCastingNaNError 

42from pandas.util._decorators import set_module 

43 

44from pandas.core.dtypes.cast import ( 

45 LossySetitemError, 

46 maybe_upcast_numeric_to_64bit, 

47) 

48from pandas.core.dtypes.common import ( 

49 is_float_dtype, 

50 is_integer_dtype, 

51 is_list_like, 

52 is_object_dtype, 

53 is_scalar, 

54 is_string_dtype, 

55 needs_i8_conversion, 

56 pandas_dtype, 

57) 

58from pandas.core.dtypes.dtypes import ( 

59 CategoricalDtype, 

60 IntervalDtype, 

61) 

62from pandas.core.dtypes.generic import ( 

63 ABCDataFrame, 

64 ABCDatetimeIndex, 

65 ABCIntervalIndex, 

66 ABCPeriodIndex, 

67) 

68from pandas.core.dtypes.missing import ( 

69 is_valid_na_for_dtype, 

70 isna, 

71 notna, 

72) 

73 

74from pandas.core.algorithms import ( 

75 isin, 

76 take, 

77 unique, 

78) 

79from pandas.core.arrays import ArrowExtensionArray 

80from pandas.core.arrays.base import ( 

81 ExtensionArray, 

82) 

83from pandas.core.arrays.datetimes import DatetimeArray 

84from pandas.core.arrays.timedeltas import TimedeltaArray 

85import pandas.core.common as com 

86from pandas.core.construction import ( 

87 array as pd_array, 

88 ensure_wrapped_if_datetimelike, 

89 extract_array, 

90) 

91from pandas.core.indexers import ( 

92 check_array_indexer, 

93 getitem_returns_view, 

94) 

95from pandas.core.ops import ( 

96 invalid_comparison, 

97 unpack_zerodim_and_defer, 

98) 

99 

100if TYPE_CHECKING: 

101 from collections.abc import ( 

102 Callable, 

103 Iterator, 

104 Sequence, 

105 ) 

106 

107 from pandas import ( 

108 Index, 

109 ) 

110 

111 

112IntervalSide: TypeAlias = TimeArrayLike | np.ndarray 

113IntervalOrNA: TypeAlias = Interval | float 

114 

115_interval_shared_docs: dict[str, str] = {} 

116 

117_shared_docs_kwargs = { 

118 "klass": "IntervalArray", 

119 "qualname": "arrays.IntervalArray", 

120 "name": "", 

121} 

122 

123 

124_interval_shared_docs["class"] = """ 

125%(summary)s 

126 

127Parameters 

128---------- 

129data : array-like (1-dimensional) 

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

131 Interval objects from which to build the %(klass)s. 

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

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

134 neither. 

135dtype : dtype or None, default None 

136 If None, dtype will be inferred. 

137copy : bool, default False 

138 Copy the input data. 

139%(name)s\ 

140verify_integrity : bool, default True 

141 Verify that the %(klass)s is valid. 

142 

143Attributes 

144---------- 

145left 

146right 

147closed 

148mid 

149length 

150is_empty 

151is_non_overlapping_monotonic 

152%(extra_attributes)s\ 

153 

154Methods 

155------- 

156from_arrays 

157from_tuples 

158from_breaks 

159contains 

160overlaps 

161set_closed 

162to_tuples 

163%(extra_methods)s\ 

164 

165See Also 

166-------- 

167Index : The base pandas Index type. 

168Interval : A bounded slice-like interval; the elements of an %(klass)s. 

169interval_range : Function to create a fixed frequency IntervalIndex. 

170cut : Bin values into discrete Intervals. 

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

172 

173Notes 

174----- 

175See the `user guide 

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

177for more. 

178 

179%(examples)s\ 

180""" 

181 

182 

183@set_module("pandas.arrays") 

184class IntervalArray(IntervalMixin, ExtensionArray): 

185 """ 

186 Pandas array for interval data that are closed on the same side. 

187 

188 Parameters 

189 ---------- 

190 data : array-like (1-dimensional) 

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

192 Interval objects from which to build the IntervalArray. 

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

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

195 neither. 

196 dtype : dtype or None, default None 

197 If None, dtype will be inferred. 

198 copy : bool, default False 

199 Copy the input data. 

200 verify_integrity : bool, default True 

201 Verify that the IntervalArray is valid. 

202 

203 Attributes 

204 ---------- 

205 left 

206 right 

207 closed 

208 mid 

209 length 

210 is_empty 

211 is_non_overlapping_monotonic 

212 

213 Methods 

214 ------- 

215 from_arrays 

216 from_tuples 

217 from_breaks 

218 contains 

219 overlaps 

220 set_closed 

221 to_tuples 

222 

223 See Also 

224 -------- 

225 Index : The base pandas Index type. 

226 Interval : A bounded slice-like interval; the elements of an IntervalArray. 

227 interval_range : Function to create a fixed frequency IntervalIndex. 

228 cut : Bin values into discrete Intervals. 

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

230 

231 Notes 

232 ----- 

233 See the `user guide 

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

235 for more. 

236 

237 Examples 

238 -------- 

239 A new ``IntervalArray`` can be constructed directly from an array-like of 

240 ``Interval`` objects: 

241 >>> pd.arrays.IntervalArray([pd.Interval(0, 1), pd.Interval(1, 5)]) 

242 <IntervalArray> 

243 [(0, 1], (1, 5]] 

244 Length: 2, dtype: interval[int64, right] 

245 

246 It may also be constructed using one of the constructor 

247 methods: :meth:`IntervalArray.from_arrays`, 

248 :meth:`IntervalArray.from_breaks`, and :meth:`IntervalArray.from_tuples`. 

249 """ 

250 

251 can_hold_na = True 

252 _na_value = _fill_value = np.nan 

253 

254 @property 

255 def ndim(self) -> Literal[1]: 

256 return 1 

257 

258 # To make mypy recognize the fields 

259 _left: IntervalSide 

260 _right: IntervalSide 

261 _dtype: IntervalDtype 

262 

263 # --------------------------------------------------------------------- 

264 # Constructors 

265 

266 def __new__( 

267 cls, 

268 data, 

269 closed: IntervalClosedType | None = None, 

270 dtype: Dtype | None = None, 

271 copy: bool = False, 

272 verify_integrity: bool = True, 

273 ) -> Self: 

274 data = extract_array(data, extract_numpy=True) 

275 

276 if isinstance(data, cls): 

277 left: IntervalSide = data._left 

278 right: IntervalSide = data._right 

279 closed = closed or data.closed 

280 dtype = IntervalDtype(left.dtype, closed=closed) 

281 else: 

282 # don't allow scalars 

283 if is_scalar(data): 

284 msg = ( 

285 f"{cls.__name__}(...) must be called with a collection " 

286 f"of some kind, {data} was passed" 

287 ) 

288 raise TypeError(msg) 

289 

290 # might need to convert empty or purely na data 

291 data = _maybe_convert_platform_interval(data) 

292 left, right, infer_closed = intervals_to_interval_bounds( 

293 data, validate_closed=closed is None 

294 ) 

295 if left.dtype == object: 

296 left = lib.maybe_convert_objects(left) 

297 right = lib.maybe_convert_objects(right) 

298 closed = closed or infer_closed 

299 

300 left, right, dtype = cls._ensure_simple_new_inputs( 

301 left, 

302 right, 

303 closed=closed, 

304 copy=copy, 

305 dtype=dtype, 

306 ) 

307 

308 if verify_integrity: 

309 cls._validate(left, right, dtype=dtype) 

310 

311 return cls._simple_new( 

312 left, 

313 right, 

314 dtype=dtype, 

315 ) 

316 

317 @classmethod 

318 def _simple_new( 

319 cls, 

320 left: IntervalSide, 

321 right: IntervalSide, 

322 dtype: IntervalDtype, 

323 ) -> Self: 

324 result = IntervalMixin.__new__(cls) 

325 result._left = left 

326 result._right = right 

327 result._dtype = dtype 

328 

329 return result 

330 

331 @classmethod 

332 def _ensure_simple_new_inputs( 

333 cls, 

334 left, 

335 right, 

336 closed: IntervalClosedType | None = None, 

337 copy: bool = False, 

338 dtype: Dtype | None = None, 

339 ) -> tuple[IntervalSide, IntervalSide, IntervalDtype]: 

340 """Ensure correctness of input parameters for cls._simple_new.""" 

341 from pandas.core.indexes.base import ensure_index 

342 

343 left = ensure_index(left, copy=copy) 

344 left = maybe_upcast_numeric_to_64bit(left) 

345 

346 right = ensure_index(right, copy=copy) 

347 right = maybe_upcast_numeric_to_64bit(right) 

348 

349 if closed is None and isinstance(dtype, IntervalDtype): 

350 closed = dtype.closed 

351 

352 closed = closed or "right" 

353 

354 if dtype is not None: 

355 # GH 19262: dtype must be an IntervalDtype to override inferred 

356 dtype = pandas_dtype(dtype) 

357 if isinstance(dtype, IntervalDtype): 

358 if dtype.subtype is not None: 

359 left = left.astype(dtype.subtype) 

360 right = right.astype(dtype.subtype) 

361 else: 

362 msg = f"dtype must be an IntervalDtype, got {dtype}" 

363 raise TypeError(msg) 

364 

365 if dtype.closed is None: 

366 # possibly loading an old pickle 

367 dtype = IntervalDtype(dtype.subtype, closed) 

368 elif closed != dtype.closed: 

369 raise ValueError("closed keyword does not match dtype.closed") 

370 

371 # coerce dtypes to match if needed 

372 if is_float_dtype(left.dtype) and is_integer_dtype(right.dtype): 

373 right = right.astype(left.dtype) 

374 elif is_float_dtype(right.dtype) and is_integer_dtype(left.dtype): 

375 left = left.astype(right.dtype) 

376 

377 if type(left) != type(right): 

378 msg = ( 

379 f"must not have differing left [{type(left).__name__}] and " 

380 f"right [{type(right).__name__}] types" 

381 ) 

382 raise ValueError(msg) 

383 if isinstance(left.dtype, CategoricalDtype) or is_string_dtype(left.dtype): 

384 # GH 19016 

385 msg = ( 

386 "category, object, and string subtypes are not supported " 

387 "for IntervalArray" 

388 ) 

389 raise TypeError(msg) 

390 if isinstance(left, ABCPeriodIndex): 

391 msg = "Period dtypes are not supported, use a PeriodIndex instead" 

392 raise ValueError(msg) 

393 if isinstance(left, ABCDatetimeIndex) and str(left.tz) != str(right.tz): 

394 msg = ( 

395 "left and right must have the same time zone, got " 

396 f"'{left.tz}' and '{right.tz}'" 

397 ) 

398 raise ValueError(msg) 

399 elif needs_i8_conversion(left.dtype) and left.unit != right.unit: 

400 # e.g. m8[s] vs m8[ms], try to cast to a common dtype GH#55714 

401 left_arr, right_arr = left._data._ensure_matching_resos(right._data) 

402 left = ensure_index(left_arr) 

403 right = ensure_index(right_arr) 

404 

405 # For dt64/td64 we want DatetimeArray/TimedeltaArray instead of ndarray 

406 left = ensure_wrapped_if_datetimelike(left) 

407 left = extract_array(left, extract_numpy=True) 

408 right = ensure_wrapped_if_datetimelike(right) 

409 right = extract_array(right, extract_numpy=True) 

410 

411 if isinstance(left, ArrowExtensionArray) or isinstance( 

412 right, ArrowExtensionArray 

413 ): 

414 pass 

415 else: 

416 lbase = getattr(left, "_ndarray", left) 

417 lbase = getattr(lbase, "_data", lbase).base 

418 rbase = getattr(right, "_ndarray", right) 

419 rbase = getattr(rbase, "_data", rbase).base 

420 if lbase is not None and lbase is rbase: 

421 # If these share data, then setitem could corrupt our IA 

422 right = right.copy() 

423 

424 dtype = IntervalDtype(left.dtype, closed=closed) 

425 

426 # Check for mismatched signed/unsigned integer dtypes after casting 

427 left_dtype = left.dtype 

428 right_dtype = right.dtype 

429 if ( 

430 left_dtype.kind in "iu" 

431 and right_dtype.kind in "iu" 

432 and left_dtype.kind != right_dtype.kind 

433 ): 

434 raise TypeError( 

435 f"Left and right arrays must have matching signedness. " 

436 f"Got {left_dtype} and {right_dtype}." 

437 ) 

438 return left, right, dtype 

439 

440 @classmethod 

441 def _from_sequence( 

442 cls, 

443 scalars, 

444 *, 

445 dtype: Dtype | None = None, 

446 copy: bool = False, 

447 ) -> Self: 

448 return cls(scalars, dtype=dtype, copy=copy) 

449 

450 @classmethod 

451 def _from_factorized(cls, values: np.ndarray, original: IntervalArray) -> Self: 

452 return cls._from_sequence(values, dtype=original.dtype) 

453 

454 _interval_shared_docs["from_breaks"] = textwrap.dedent( 

455 """ 

456 Construct an %(klass)s from an array of splits. 

457 

458 Parameters 

459 ---------- 

460 breaks : array-like (1-dimensional) 

461 Left and right bounds for each interval. 

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

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

464 or neither.\ 

465 %(name)s 

466 copy : bool, default False 

467 Copy the data. 

468 dtype : dtype or None, default None 

469 If None, dtype will be inferred. 

470 

471 Returns 

472 ------- 

473 %(klass)s 

474 

475 See Also 

476 -------- 

477 interval_range : Function to create a fixed frequency IntervalIndex. 

478 %(klass)s.from_arrays : Construct from a left and right array. 

479 %(klass)s.from_tuples : Construct from a sequence of tuples. 

480 

481 %(examples)s\ 

482 """ 

483 ) 

484 

485 @classmethod 

486 def from_breaks( 

487 cls, 

488 breaks, 

489 closed: IntervalClosedType | None = "right", 

490 copy: bool = False, 

491 dtype: Dtype | None = None, 

492 ) -> Self: 

493 """ 

494 Construct an IntervalArray from an array of splits. 

495 

496 Parameters 

497 ---------- 

498 breaks : array-like (1-dimensional) 

499 Left and right bounds for each interval. 

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

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

502 or neither. 

503 copy : bool, default False 

504 Copy the data. 

505 dtype : dtype or None, default None 

506 If None, dtype will be inferred. 

507 

508 Returns 

509 ------- 

510 IntervalArray 

511 

512 See Also 

513 -------- 

514 interval_range : Function to create a fixed frequency IntervalIndex. 

515 IntervalArray.from_arrays : Construct from a left and right array. 

516 IntervalArray.from_tuples : Construct from a sequence of tuples. 

517 

518 Examples 

519 -------- 

520 >>> pd.arrays.IntervalArray.from_breaks([0, 1, 2, 3]) 

521 <IntervalArray> 

522 [(0, 1], (1, 2], (2, 3]] 

523 Length: 3, dtype: interval[int64, right] 

524 """ 

525 

526 breaks = _maybe_convert_platform_interval(breaks) 

527 

528 return cls.from_arrays(breaks[:-1], breaks[1:], closed, copy=copy, dtype=dtype) 

529 

530 _interval_shared_docs["from_arrays"] = textwrap.dedent( 

531 """ 

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

533 

534 Parameters 

535 ---------- 

536 left : array-like (1-dimensional) 

537 Left bounds for each interval. 

538 right : array-like (1-dimensional) 

539 Right bounds for each interval. 

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

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

542 or neither.\ 

543 %(name)s 

544 copy : bool, default False 

545 Copy the data. 

546 dtype : dtype, optional 

547 If None, dtype will be inferred. 

548 

549 Returns 

550 ------- 

551 %(klass)s 

552 

553 Raises 

554 ------ 

555 ValueError 

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

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

558 in `right`. 

559 

560 See Also 

561 -------- 

562 interval_range : Function to create a fixed frequency IntervalIndex. 

563 %(klass)s.from_breaks : Construct an %(klass)s from an array of 

564 splits. 

565 %(klass)s.from_tuples : Construct an %(klass)s from an 

566 array-like of tuples. 

567 

568 Notes 

569 ----- 

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

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

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

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

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

575 

576 %(examples)s\ 

577 """ 

578 ) 

579 

580 @classmethod 

581 def from_arrays( 

582 cls, 

583 left, 

584 right, 

585 closed: IntervalClosedType | None = "right", 

586 copy: bool = False, 

587 dtype: Dtype | None = None, 

588 ) -> Self: 

589 """ 

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

591 

592 Parameters 

593 ---------- 

594 left : array-like (1-dimensional) 

595 Left bounds for each interval. 

596 right : array-like (1-dimensional) 

597 Right bounds for each interval. 

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

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

600 or neither. 

601 copy : bool, default False 

602 Copy the data. 

603 dtype : dtype, optional 

604 If None, dtype will be inferred. 

605 

606 Returns 

607 ------- 

608 IntervalArray 

609 

610 Raises 

611 ------ 

612 ValueError 

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

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

615 in `right`. 

616 

617 See Also 

618 -------- 

619 interval_range : Function to create a fixed frequency IntervalIndex. 

620 IntervalArray.from_breaks : Construct an IntervalArray from an array of 

621 splits. 

622 IntervalArray.from_tuples : Construct an IntervalArray from an 

623 array-like of tuples. 

624 

625 Notes 

626 ----- 

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

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

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

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

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

632 

633 Examples 

634 -------- 

635 >>> pd.arrays.IntervalArray.from_arrays([0, 1, 2], [1, 2, 3]) 

636 <IntervalArray> 

637 [(0, 1], (1, 2], (2, 3]] 

638 Length: 3, dtype: interval[int64, right] 

639 """ 

640 left = _maybe_convert_platform_interval(left) 

641 right = _maybe_convert_platform_interval(right) 

642 

643 left, right, dtype = cls._ensure_simple_new_inputs( 

644 left, 

645 right, 

646 closed=closed, 

647 copy=copy, 

648 dtype=dtype, 

649 ) 

650 cls._validate(left, right, dtype=dtype) 

651 

652 return cls._simple_new(left, right, dtype=dtype) 

653 

654 _interval_shared_docs["from_tuples"] = textwrap.dedent( 

655 """ 

656 Construct an %(klass)s from an array-like of tuples. 

657 

658 Parameters 

659 ---------- 

660 data : array-like (1-dimensional) 

661 Array of tuples. 

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

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

664 or neither.\ 

665 %(name)s 

666 copy : bool, default False 

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

668 dtype : dtype or None, default None 

669 If None, dtype will be inferred. 

670 

671 Returns 

672 ------- 

673 %(klass)s 

674 

675 See Also 

676 -------- 

677 interval_range : Function to create a fixed frequency IntervalIndex. 

678 %(klass)s.from_arrays : Construct an %(klass)s from a left and 

679 right array. 

680 %(klass)s.from_breaks : Construct an %(klass)s from an array of 

681 splits. 

682 

683 %(examples)s\ 

684 """ 

685 ) 

686 

687 @classmethod 

688 def from_tuples( 

689 cls, 

690 data, 

691 closed: IntervalClosedType | None = "right", 

692 copy: bool = False, 

693 dtype: Dtype | None = None, 

694 ) -> Self: 

695 """ 

696 Construct an IntervalArray from an array-like of tuples. 

697 

698 Parameters 

699 ---------- 

700 data : array-like (1-dimensional) 

701 Array of tuples. 

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

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

704 or neither. 

705 copy : bool, default False 

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

707 dtype : dtype or None, default None 

708 If None, dtype will be inferred. 

709 

710 Returns 

711 ------- 

712 IntervalArray 

713 

714 See Also 

715 -------- 

716 interval_range : Function to create a fixed frequency IntervalIndex. 

717 IntervalArray.from_arrays : Construct an IntervalArray from a left and 

718 right array. 

719 IntervalArray.from_breaks : Construct an IntervalArray from an array of 

720 splits. 

721 

722 Examples 

723 -------- 

724 >>> pd.arrays.IntervalArray.from_tuples([(0, 1), (1, 2)]) 

725 <IntervalArray> 

726 [(0, 1], (1, 2]] 

727 Length: 2, dtype: interval[int64, right] 

728 """ 

729 if len(data): 

730 left, right = [], [] 

731 else: 

732 # ensure that empty data keeps input dtype 

733 left = right = data 

734 

735 for d in data: 

736 if not isinstance(d, tuple) and isna(d): 

737 lhs = rhs = np.nan 

738 else: 

739 name = cls.__name__ 

740 try: 

741 # need list of length 2 tuples, e.g. [(0, 1), (1, 2), ...] 

742 lhs, rhs = d 

743 except ValueError as err: 

744 msg = f"{name}.from_tuples requires tuples of length 2, got {d}" 

745 raise ValueError(msg) from err 

746 except TypeError as err: 

747 msg = f"{name}.from_tuples received an invalid item, {d}" 

748 raise TypeError(msg) from err 

749 left.append(lhs) 

750 right.append(rhs) 

751 

752 return cls.from_arrays(left, right, closed, copy=False, dtype=dtype) 

753 

754 @classmethod 

755 def _validate(cls, left, right, dtype: IntervalDtype) -> None: 

756 """ 

757 Verify that the IntervalArray is valid. 

758 

759 Checks that 

760 

761 * dtype is correct 

762 * left and right match lengths 

763 * left and right have the same missing values 

764 * left is always below right 

765 """ 

766 if not isinstance(dtype, IntervalDtype): 

767 msg = f"invalid dtype: {dtype}" 

768 raise ValueError(msg) 

769 if len(left) != len(right): 

770 msg = "left and right must have the same length" 

771 raise ValueError(msg) 

772 left_mask = notna(left) 

773 right_mask = notna(right) 

774 if not (left_mask == right_mask).all(): 

775 msg = ( 

776 "missing values must be missing in the same " 

777 "location both left and right sides" 

778 ) 

779 raise ValueError(msg) 

780 if not (left[left_mask] <= right[left_mask]).all(): 

781 msg = "left side of interval must be <= right side" 

782 raise ValueError(msg) 

783 

784 def _shallow_copy(self, left, right) -> Self: 

785 """ 

786 Return a new IntervalArray with the replacement attributes 

787 

788 Parameters 

789 ---------- 

790 left : Index 

791 Values to be used for the left-side of the intervals. 

792 right : Index 

793 Values to be used for the right-side of the intervals. 

794 """ 

795 dtype = IntervalDtype(left.dtype, closed=self.closed) 

796 left, right, dtype = self._ensure_simple_new_inputs(left, right, dtype=dtype) 

797 

798 return self._simple_new(left, right, dtype=dtype) 

799 

800 # --------------------------------------------------------------------- 

801 # Descriptive 

802 

803 @property 

804 def dtype(self) -> IntervalDtype: 

805 return self._dtype 

806 

807 @property 

808 def nbytes(self) -> int: 

809 return self.left.nbytes + self.right.nbytes 

810 

811 @property 

812 def size(self) -> int: 

813 # Avoid materializing self.values 

814 return self.left.size 

815 

816 # --------------------------------------------------------------------- 

817 # EA Interface 

818 

819 def __iter__(self) -> Iterator: 

820 return iter(np.asarray(self)) 

821 

822 def __len__(self) -> int: 

823 return len(self._left) 

824 

825 @overload 

826 def __getitem__(self, key: ScalarIndexer) -> IntervalOrNA: ... 

827 

828 @overload 

829 def __getitem__(self, key: SequenceIndexer) -> Self: ... 

830 

831 def __getitem__(self, key: PositionalIndexer) -> Self | IntervalOrNA: 

832 key = check_array_indexer(self, key) 

833 left = self._left[key] 

834 right = self._right[key] 

835 

836 if not isinstance(left, (np.ndarray, ExtensionArray)): 

837 # scalar 

838 if is_scalar(left) and isna(left): 

839 return self._fill_value 

840 return Interval(left, right, self.closed) 

841 if np.ndim(left) > 1: 

842 # GH#30588 multi-dimensional indexer disallowed 

843 raise ValueError("multi-dimensional indexing not allowed") 

844 # Argument 2 to "_simple_new" of "IntervalArray" has incompatible type 

845 # "Union[Period, Timestamp, Timedelta, NaTType, DatetimeArray, TimedeltaArray, 

846 # ndarray[Any, Any]]"; expected "Union[Union[DatetimeArray, TimedeltaArray], 

847 # ndarray[Any, Any]]" 

848 result = self._simple_new(left, right, dtype=self.dtype) # type: ignore[arg-type] 

849 if getitem_returns_view(self, key): 

850 result._readonly = self._readonly 

851 return result 

852 

853 def __setitem__(self, key, value) -> None: 

854 if self._readonly: 

855 raise ValueError("Cannot modify read-only array") 

856 

857 value_left, value_right = self._validate_setitem_value(value) 

858 key = check_array_indexer(self, key) 

859 

860 self._left[key] = value_left 

861 self._right[key] = value_right 

862 

863 def _cmp_method(self, other, op): 

864 # ensure pandas array for list-like and eliminate non-interval scalars 

865 if is_list_like(other): 

866 if len(self) != len(other): 

867 raise ValueError("Lengths must match to compare") 

868 other = pd_array(other) 

869 elif not isinstance(other, Interval): 

870 # non-interval scalar -> no matches 

871 if other is NA: 

872 # GH#31882 

873 from pandas.core.arrays import BooleanArray 

874 

875 arr = np.empty(self.shape, dtype=bool) 

876 mask = np.ones(self.shape, dtype=bool) 

877 return BooleanArray(arr, mask) 

878 return invalid_comparison(self, other, op) 

879 

880 # determine the dtype of the elements we want to compare 

881 if isinstance(other, Interval): 

882 other_dtype = pandas_dtype("interval") 

883 elif not isinstance(other.dtype, CategoricalDtype): 

884 other_dtype = other.dtype 

885 else: 

886 # for categorical defer to categories for dtype 

887 other_dtype = other.categories.dtype 

888 

889 # extract intervals if we have interval categories with matching closed 

890 if isinstance(other_dtype, IntervalDtype): 

891 if self.closed != other.categories.closed: 

892 return invalid_comparison(self, other, op) 

893 

894 other = other.categories._values.take( 

895 other.codes, allow_fill=True, fill_value=other.categories._na_value 

896 ) 

897 

898 # interval-like -> need same closed and matching endpoints 

899 if isinstance(other_dtype, IntervalDtype): 

900 if self.closed != other.closed: 

901 return invalid_comparison(self, other, op) 

902 elif not isinstance(other, Interval): 

903 other = type(self)(other) 

904 

905 if op is operator.eq: 

906 return (self._left == other.left) & (self._right == other.right) 

907 elif op is operator.ne: 

908 return (self._left != other.left) | (self._right != other.right) 

909 elif op is operator.gt: 

910 return (self._left > other.left) | ( 

911 (self._left == other.left) & (self._right > other.right) 

912 ) 

913 elif op is operator.ge: 

914 return (self == other) | (self > other) 

915 elif op is operator.lt: 

916 return (self._left < other.left) | ( 

917 (self._left == other.left) & (self._right < other.right) 

918 ) 

919 else: 

920 # operator.lt 

921 return (self == other) | (self < other) 

922 

923 # non-interval/non-object dtype -> no matches 

924 if not is_object_dtype(other_dtype): 

925 return invalid_comparison(self, other, op) 

926 

927 # object dtype -> iteratively check for intervals 

928 result = np.zeros(len(self), dtype=bool) 

929 for i, obj in enumerate(other): 

930 try: 

931 result[i] = op(self[i], obj) 

932 except TypeError: 

933 if obj is NA: 

934 # comparison with np.nan returns NA 

935 # github.com/pandas-dev/pandas/pull/37124#discussion_r509095092 

936 result = result.astype(object) 

937 result[i] = NA 

938 else: 

939 raise 

940 return result 

941 

942 @unpack_zerodim_and_defer("__eq__") 

943 def __eq__(self, other): 

944 return self._cmp_method(other, operator.eq) 

945 

946 @unpack_zerodim_and_defer("__ne__") 

947 def __ne__(self, other): 

948 return self._cmp_method(other, operator.ne) 

949 

950 @unpack_zerodim_and_defer("__gt__") 

951 def __gt__(self, other): 

952 return self._cmp_method(other, operator.gt) 

953 

954 @unpack_zerodim_and_defer("__ge__") 

955 def __ge__(self, other): 

956 return self._cmp_method(other, operator.ge) 

957 

958 @unpack_zerodim_and_defer("__lt__") 

959 def __lt__(self, other): 

960 return self._cmp_method(other, operator.lt) 

961 

962 @unpack_zerodim_and_defer("__le__") 

963 def __le__(self, other): 

964 return self._cmp_method(other, operator.le) 

965 

966 def argsort( 

967 self, 

968 *, 

969 ascending: bool = True, 

970 kind: SortKind = "quicksort", 

971 na_position: str = "last", 

972 **kwargs, 

973 ) -> np.ndarray: 

974 ascending = nv.validate_argsort_with_ascending(ascending, (), kwargs) 

975 

976 if ascending and kind == "quicksort" and na_position == "last": 

977 # TODO: in an IntervalIndex we can reuse the cached 

978 # IntervalTree.left_sorter 

979 return np.lexsort((self.right, self.left)) 

980 

981 # TODO: other cases we can use lexsort for? much more performant. 

982 return super().argsort( 

983 ascending=ascending, kind=kind, na_position=na_position, **kwargs 

984 ) 

985 

986 def min(self, *, axis: AxisInt | None = None, skipna: bool = True) -> IntervalOrNA: 

987 nv.validate_minmax_axis(axis, self.ndim) 

988 

989 if not len(self): 

990 return self._na_value 

991 

992 mask = self.isna() 

993 if mask.any(): 

994 if not skipna: 

995 return self._na_value 

996 obj = self[~mask] 

997 else: 

998 obj = self 

999 

1000 indexer = obj.argsort()[0] 

1001 return obj[indexer] 

1002 

1003 def max(self, *, axis: AxisInt | None = None, skipna: bool = True) -> IntervalOrNA: 

1004 nv.validate_minmax_axis(axis, self.ndim) 

1005 

1006 if not len(self): 

1007 return self._na_value 

1008 

1009 mask = self.isna() 

1010 if mask.any(): 

1011 if not skipna: 

1012 return self._na_value 

1013 obj = self[~mask] 

1014 else: 

1015 obj = self 

1016 

1017 indexer = obj.argsort()[-1] 

1018 return obj[indexer] 

1019 

1020 def fillna(self, value, limit: int | None = None, copy: bool = True) -> Self: 

1021 """ 

1022 Fill NA/NaN values using the specified method. 

1023 

1024 Parameters 

1025 ---------- 

1026 value : scalar, dict, Series 

1027 If a scalar value is passed it is used to fill all missing values. 

1028 Alternatively, a Series or dict can be used to fill in different 

1029 values for each index. The value should not be a list. The 

1030 value(s) passed should be either Interval objects or NA/NaN. 

1031 limit : int, default None 

1032 (Not implemented yet for IntervalArray) 

1033 The maximum number of entries where NA values will be filled. 

1034 copy : bool, default True 

1035 Whether to make a copy of the data before filling. If False, then 

1036 the original should be modified and no new memory should be allocated. 

1037 For ExtensionArray subclasses that cannot do this, it is at the 

1038 author's discretion whether to ignore "copy=False" or to raise. 

1039 

1040 Returns 

1041 ------- 

1042 filled : IntervalArray with NA/NaN filled 

1043 """ 

1044 if copy is False: 

1045 raise NotImplementedError 

1046 if limit is not None: 

1047 raise ValueError("limit must be None") 

1048 

1049 value_left, value_right = self._validate_scalar(value) 

1050 

1051 left = self.left.fillna(value=value_left) 

1052 right = self.right.fillna(value=value_right) 

1053 return self._shallow_copy(left, right) 

1054 

1055 def astype(self, dtype, copy: bool = True): 

1056 """ 

1057 Cast to an ExtensionArray or NumPy array with dtype 'dtype'. 

1058 

1059 Parameters 

1060 ---------- 

1061 dtype : str or dtype 

1062 Typecode or data-type to which the array is cast. 

1063 

1064 copy : bool, default True 

1065 Whether to copy the data, even if not necessary. If False, 

1066 a copy is made only if the old dtype does not match the 

1067 new dtype. 

1068 

1069 Returns 

1070 ------- 

1071 array : ExtensionArray or ndarray 

1072 ExtensionArray or NumPy ndarray with 'dtype' for its dtype. 

1073 """ 

1074 from pandas import Index 

1075 

1076 if dtype is not None: 

1077 dtype = pandas_dtype(dtype) 

1078 

1079 if isinstance(dtype, IntervalDtype): 

1080 if dtype == self.dtype: 

1081 return self.copy() if copy else self 

1082 

1083 if is_float_dtype(self.dtype.subtype) and needs_i8_conversion( 

1084 dtype.subtype 

1085 ): 

1086 # This is allowed on the Index.astype but we disallow it here 

1087 msg = ( 

1088 f"Cannot convert {self.dtype} to {dtype}; subtypes are incompatible" 

1089 ) 

1090 raise TypeError(msg) 

1091 

1092 # need to cast to different subtype 

1093 try: 

1094 # We need to use Index rules for astype to prevent casting 

1095 # np.nan entries to int subtypes 

1096 new_left = Index(self._left, copy=False).astype(dtype.subtype) 

1097 new_right = Index(self._right, copy=False).astype(dtype.subtype) 

1098 except IntCastingNaNError: 

1099 # e.g test_subtype_integer 

1100 raise 

1101 except (TypeError, ValueError) as err: 

1102 # e.g. test_subtype_integer_errors f8->u8 can be lossy 

1103 # and raises ValueError 

1104 msg = ( 

1105 f"Cannot convert {self.dtype} to {dtype}; subtypes are incompatible" 

1106 ) 

1107 raise TypeError(msg) from err 

1108 return self._shallow_copy(new_left, new_right) 

1109 else: 

1110 try: 

1111 return super().astype(dtype, copy=copy) 

1112 except (TypeError, ValueError) as err: 

1113 msg = f"Cannot cast {type(self).__name__} to dtype {dtype}" 

1114 raise TypeError(msg) from err 

1115 

1116 def equals(self, other) -> bool: 

1117 if type(self) != type(other): 

1118 return False 

1119 

1120 return bool( 

1121 self.closed == other.closed 

1122 and self.left.equals(other.left) 

1123 and self.right.equals(other.right) 

1124 ) 

1125 

1126 @classmethod 

1127 def _concat_same_type(cls, to_concat: Sequence[IntervalArray]) -> Self: 

1128 """ 

1129 Concatenate multiple IntervalArray 

1130 

1131 Parameters 

1132 ---------- 

1133 to_concat : sequence of IntervalArray 

1134 

1135 Returns 

1136 ------- 

1137 IntervalArray 

1138 """ 

1139 closed_set = {interval.closed for interval in to_concat} 

1140 if len(closed_set) != 1: 

1141 raise ValueError("Intervals must all be closed on the same side.") 

1142 closed = closed_set.pop() 

1143 

1144 left: IntervalSide = np.concatenate([interval.left for interval in to_concat]) 

1145 right: IntervalSide = np.concatenate([interval.right for interval in to_concat]) 

1146 

1147 left, right, dtype = cls._ensure_simple_new_inputs(left, right, closed=closed) 

1148 

1149 return cls._simple_new(left, right, dtype=dtype) 

1150 

1151 def copy(self) -> Self: 

1152 """ 

1153 Return a copy of the array. 

1154 

1155 Returns 

1156 ------- 

1157 IntervalArray 

1158 """ 

1159 left = self._left.copy() 

1160 right = self._right.copy() 

1161 dtype = self.dtype 

1162 return self._simple_new(left, right, dtype=dtype) 

1163 

1164 def isna(self) -> np.ndarray: 

1165 return isna(self._left) 

1166 

1167 def shift(self, periods: int = 1, fill_value: object = None) -> IntervalArray: 

1168 if not len(self) or periods == 0: 

1169 return self.copy() 

1170 

1171 self._validate_scalar(fill_value) 

1172 

1173 # ExtensionArray.shift doesn't work for two reasons 

1174 # 1. IntervalArray.dtype.na_value may not be correct for the dtype. 

1175 # 2. IntervalArray._from_sequence only accepts NaN for missing values, 

1176 # not other values like NaT 

1177 

1178 empty_len = min(abs(periods), len(self)) 

1179 if isna(fill_value): 

1180 from pandas import Index 

1181 

1182 fill_value = Index(self._left, copy=False)._na_value 

1183 empty = IntervalArray.from_breaks( 

1184 [fill_value] * (empty_len + 1), closed=self.closed 

1185 ) 

1186 else: 

1187 empty = self._from_sequence([fill_value] * empty_len, dtype=self.dtype) 

1188 

1189 if periods > 0: 

1190 a = empty 

1191 b = self[:-periods] 

1192 else: 

1193 a = self[abs(periods) :] 

1194 b = empty 

1195 return self._concat_same_type([a, b]) 

1196 

1197 def take( 

1198 self, 

1199 indices, 

1200 *, 

1201 allow_fill: bool = False, 

1202 fill_value=None, 

1203 axis=None, 

1204 **kwargs, 

1205 ) -> Self: 

1206 """ 

1207 Take elements from the IntervalArray. 

1208 

1209 Parameters 

1210 ---------- 

1211 indices : sequence of integers 

1212 Indices to be taken. 

1213 

1214 allow_fill : bool, default False 

1215 How to handle negative values in `indices`. 

1216 

1217 * False: negative values in `indices` indicate positional indices 

1218 from the right (the default). This is similar to 

1219 :func:`numpy.take`. 

1220 

1221 * True: negative values in `indices` indicate 

1222 missing values. These values are set to `fill_value`. Any other 

1223 other negative values raise a ``ValueError``. 

1224 

1225 fill_value : Interval or NA, optional 

1226 Fill value to use for NA-indices when `allow_fill` is True. 

1227 This may be ``None``, in which case the default NA value for 

1228 the type, ``self.dtype.na_value``, is used. 

1229 

1230 For many ExtensionArrays, there will be two representations of 

1231 `fill_value`: a user-facing "boxed" scalar, and a low-level 

1232 physical NA value. `fill_value` should be the user-facing version, 

1233 and the implementation should handle translating that to the 

1234 physical version for processing the take if necessary. 

1235 

1236 axis : any, default None 

1237 Present for compat with IntervalIndex; does nothing. 

1238 

1239 Returns 

1240 ------- 

1241 IntervalArray 

1242 

1243 Raises 

1244 ------ 

1245 IndexError 

1246 When the indices are out of bounds for the array. 

1247 ValueError 

1248 When `indices` contains negative values other than ``-1`` 

1249 and `allow_fill` is True. 

1250 """ 

1251 nv.validate_take((), kwargs) 

1252 

1253 fill_left = fill_right = fill_value 

1254 if allow_fill: 

1255 fill_left, fill_right = self._validate_scalar(fill_value) 

1256 

1257 left_take = take( 

1258 self._left, indices, allow_fill=allow_fill, fill_value=fill_left 

1259 ) 

1260 right_take = take( 

1261 self._right, indices, allow_fill=allow_fill, fill_value=fill_right 

1262 ) 

1263 

1264 return self._shallow_copy(left_take, right_take) 

1265 

1266 def _validate_listlike(self, value): 

1267 # list-like of intervals 

1268 try: 

1269 array = IntervalArray(value) 

1270 self._check_closed_matches(array, name="value") 

1271 value_left, value_right = array.left, array.right 

1272 except TypeError as err: 

1273 # wrong type: not interval or NA 

1274 msg = f"'value' should be an interval type, got {type(value)} instead." 

1275 raise TypeError(msg) from err 

1276 

1277 try: 

1278 self.left._validate_fill_value(value_left) 

1279 except (LossySetitemError, TypeError) as err: 

1280 msg = ( 

1281 "'value' should be a compatible interval type, " 

1282 f"got {type(value)} instead." 

1283 ) 

1284 raise TypeError(msg) from err 

1285 

1286 return value_left, value_right 

1287 

1288 def _validate_scalar(self, value): 

1289 if isinstance(value, Interval): 

1290 self._check_closed_matches(value, name="value") 

1291 left, right = value.left, value.right 

1292 # TODO: check subdtype match like _validate_setitem_value? 

1293 elif is_valid_na_for_dtype(value, self.left.dtype): 

1294 # GH#18295 

1295 left = right = self.left._na_value 

1296 else: 

1297 raise TypeError( 

1298 "can only insert Interval objects and NA into an IntervalArray" 

1299 ) 

1300 return left, right 

1301 

1302 def _validate_setitem_value(self, value): 

1303 if is_valid_na_for_dtype(value, self.left.dtype): 

1304 # na value: need special casing to set directly on numpy arrays 

1305 value = self.left._na_value 

1306 if is_integer_dtype(self.dtype.subtype): 

1307 # can't set NaN on a numpy integer array 

1308 # GH#45484 TypeError, not ValueError, matches what we get with 

1309 # non-NA un-holdable value. 

1310 raise TypeError("Cannot set float NaN to integer-backed IntervalArray") 

1311 value_left, value_right = value, value 

1312 

1313 elif isinstance(value, Interval): 

1314 # scalar interval 

1315 self._check_closed_matches(value, name="value") 

1316 value_left, value_right = value.left, value.right 

1317 self.left._validate_fill_value(value_left) 

1318 self.left._validate_fill_value(value_right) 

1319 

1320 else: 

1321 return self._validate_listlike(value) 

1322 

1323 return value_left, value_right 

1324 

1325 # --------------------------------------------------------------------- 

1326 # Rendering Methods 

1327 

1328 def _formatter(self, boxed: bool = False) -> Callable[[object], str]: 

1329 # returning 'str' here causes us to render as e.g. "(0, 1]" instead of 

1330 # "Interval(0, 1, closed='right')" 

1331 return str 

1332 

1333 # --------------------------------------------------------------------- 

1334 # Vectorized Interval Properties/Attributes 

1335 

1336 @property 

1337 def left(self) -> Index: 

1338 """ 

1339 Return the left endpoints of each Interval in the IntervalArray as an Index. 

1340 

1341 This property provides access to the left endpoints of the intervals 

1342 contained within the IntervalArray. This can be useful for analyses where 

1343 the starting point of each interval is of interest, such as in histogram 

1344 creation, data aggregation, or any scenario requiring the identification 

1345 of the beginning of defined ranges. This property returns a ``pandas.Index`` 

1346 object containing the midpoint for each interval. 

1347 

1348 See Also 

1349 -------- 

1350 arrays.IntervalArray.right : Return the right endpoints of each Interval in 

1351 the IntervalArray as an Index. 

1352 arrays.IntervalArray.mid : Return the midpoint of each Interval in the 

1353 IntervalArray as an Index. 

1354 arrays.IntervalArray.contains : Check elementwise if the Intervals contain 

1355 the value. 

1356 

1357 Examples 

1358 -------- 

1359 

1360 >>> interv_arr = pd.arrays.IntervalArray([pd.Interval(0, 1), pd.Interval(2, 5)]) 

1361 >>> interv_arr 

1362 <IntervalArray> 

1363 [(0, 1], (2, 5]] 

1364 Length: 2, dtype: interval[int64, right] 

1365 >>> interv_arr.left 

1366 Index([0, 2], dtype='int64') 

1367 """ 

1368 from pandas import Index 

1369 

1370 return Index(self._left, copy=False) 

1371 

1372 @property 

1373 def right(self) -> Index: 

1374 """ 

1375 Return the right endpoints of each Interval in the IntervalArray as an Index. 

1376 

1377 This property extracts the right endpoints from each interval contained within 

1378 the IntervalArray. This can be helpful in use cases where you need to work 

1379 with or compare only the upper bounds of intervals, such as when performing 

1380 range-based filtering, determining interval overlaps, or visualizing the end 

1381 boundaries of data segments. 

1382 

1383 See Also 

1384 -------- 

1385 arrays.IntervalArray.left : Return the left endpoints of each Interval in 

1386 the IntervalArray as an Index. 

1387 arrays.IntervalArray.mid : Return the midpoint of each Interval in the 

1388 IntervalArray as an Index. 

1389 arrays.IntervalArray.contains : Check elementwise if the Intervals contain 

1390 the value. 

1391 

1392 Examples 

1393 -------- 

1394 

1395 >>> interv_arr = pd.arrays.IntervalArray([pd.Interval(0, 1), pd.Interval(2, 5)]) 

1396 >>> interv_arr 

1397 <IntervalArray> 

1398 [(0, 1], (2, 5]] 

1399 Length: 2, dtype: interval[int64, right] 

1400 >>> interv_arr.right 

1401 Index([1, 5], dtype='int64') 

1402 """ 

1403 from pandas import Index 

1404 

1405 return Index(self._right, copy=False) 

1406 

1407 @property 

1408 def length(self) -> Index: 

1409 """ 

1410 Return an Index with entries denoting the length of each Interval. 

1411 

1412 The length of an interval is calculated as the difference between 

1413 its `right` and `left` bounds. This property is particularly useful 

1414 when working with intervals where the size of the interval is an important 

1415 attribute, such as in time-series analysis or spatial data analysis. 

1416 

1417 See Also 

1418 -------- 

1419 arrays.IntervalArray.left : Return the left endpoints of each Interval in 

1420 the IntervalArray as an Index. 

1421 arrays.IntervalArray.right : Return the right endpoints of each Interval in 

1422 the IntervalArray as an Index. 

1423 arrays.IntervalArray.mid : Return the midpoint of each Interval in the 

1424 IntervalArray as an Index. 

1425 

1426 Examples 

1427 -------- 

1428 

1429 >>> interv_arr = pd.arrays.IntervalArray([pd.Interval(0, 1), pd.Interval(1, 5)]) 

1430 >>> interv_arr 

1431 <IntervalArray> 

1432 [(0, 1], (1, 5]] 

1433 Length: 2, dtype: interval[int64, right] 

1434 >>> interv_arr.length 

1435 Index([1, 4], dtype='int64') 

1436 """ 

1437 return self.right - self.left 

1438 

1439 @property 

1440 def mid(self) -> Index: 

1441 """ 

1442 Return the midpoint of each Interval in the IntervalArray as an Index. 

1443 

1444 The midpoint of an interval is calculated as the average of its 

1445 ``left`` and ``right`` bounds. This property returns a ``pandas.Index`` object 

1446 containing the midpoint for each interval. 

1447 

1448 See Also 

1449 -------- 

1450 Interval.left : Return left bound for the interval. 

1451 Interval.right : Return right bound for the interval. 

1452 Interval.length : Return the length of each interval. 

1453 

1454 Examples 

1455 -------- 

1456 

1457 >>> interv_arr = pd.arrays.IntervalArray([pd.Interval(0, 1), pd.Interval(1, 5)]) 

1458 >>> interv_arr 

1459 <IntervalArray> 

1460 [(0, 1], (1, 5]] 

1461 Length: 2, dtype: interval[int64, right] 

1462 >>> interv_arr.mid 

1463 Index([0.5, 3.0], dtype='float64') 

1464 """ 

1465 try: 

1466 return 0.5 * (self.left + self.right) 

1467 except TypeError: 

1468 # datetime safe version 

1469 return self.left + 0.5 * self.length 

1470 

1471 _interval_shared_docs["overlaps"] = textwrap.dedent( 

1472 """ 

1473 Check elementwise if an Interval overlaps the values in the %(klass)s. 

1474 

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

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

1477 overlap. 

1478 

1479 Parameters 

1480 ---------- 

1481 other : Interval 

1482 Interval to check against for an overlap. 

1483 

1484 Returns 

1485 ------- 

1486 ndarray 

1487 Boolean array positionally indicating where an overlap occurs. 

1488 

1489 See Also 

1490 -------- 

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

1492 

1493 Examples 

1494 -------- 

1495 %(examples)s 

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

1497 array([ True, True, False]) 

1498 

1499 Intervals that share closed endpoints overlap: 

1500 

1501 >>> intervals.overlaps(pd.Interval(1, 3, closed='left')) 

1502 array([ True, True, True]) 

1503 

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

1505 

1506 >>> intervals.overlaps(pd.Interval(1, 2, closed='right')) 

1507 array([False, True, False]) 

1508 """ 

1509 ) 

1510 

1511 def overlaps(self, other): 

1512 """ 

1513 Check elementwise if an Interval overlaps the values in the IntervalArray. 

1514 

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

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

1517 overlap. 

1518 

1519 Parameters 

1520 ---------- 

1521 other : IntervalArray 

1522 Interval to check against for an overlap. 

1523 

1524 Returns 

1525 ------- 

1526 ndarray 

1527 Boolean array positionally indicating where an overlap occurs. 

1528 

1529 See Also 

1530 -------- 

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

1532 

1533 Examples 

1534 -------- 

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

1536 >>> intervals = pd.arrays.IntervalArray.from_tuples(data) 

1537 >>> intervals 

1538 <IntervalArray> 

1539 [(0, 1], (1, 3], (2, 4]] 

1540 Length: 3, dtype: interval[int64, right] 

1541 

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

1543 array([ True, True, False]) 

1544 

1545 Intervals that share closed endpoints overlap: 

1546 

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

1548 array([ True, True, True]) 

1549 

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

1551 

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

1553 array([False, True, False]) 

1554 """ 

1555 if isinstance(other, (IntervalArray, ABCIntervalIndex)): 

1556 raise NotImplementedError 

1557 if not isinstance(other, Interval): 

1558 msg = f"`other` must be Interval-like, got {type(other).__name__}" 

1559 raise TypeError(msg) 

1560 

1561 # equality is okay if both endpoints are closed (overlap at a point) 

1562 op1 = le if (self.closed_left and other.closed_right) else lt 

1563 op2 = le if (other.closed_left and self.closed_right) else lt 

1564 

1565 # overlaps is equivalent negation of two interval being disjoint: 

1566 # disjoint = (A.left > B.right) or (B.left > A.right) 

1567 # (simplifying the negation allows this to be done in less operations) 

1568 return op1(self.left, other.right) & op2(other.left, self.right) 

1569 

1570 # --------------------------------------------------------------------- 

1571 

1572 @property 

1573 def closed(self) -> IntervalClosedType: 

1574 """ 

1575 String describing the inclusive side the intervals. 

1576 

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

1578 

1579 See Also 

1580 -------- 

1581 IntervalArray.closed : Returns inclusive side of the IntervalArray. 

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

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

1584 

1585 Examples 

1586 -------- 

1587 

1588 For arrays: 

1589 

1590 >>> interv_arr = pd.arrays.IntervalArray([pd.Interval(0, 1), pd.Interval(1, 5)]) 

1591 >>> interv_arr 

1592 <IntervalArray> 

1593 [(0, 1], (1, 5]] 

1594 Length: 2, dtype: interval[int64, right] 

1595 >>> interv_arr.closed 

1596 'right' 

1597 

1598 For Interval Index: 

1599 

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

1601 >>> interv_idx 

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

1603 >>> interv_idx.closed 

1604 'right' 

1605 """ 

1606 return self.dtype.closed 

1607 

1608 _interval_shared_docs["set_closed"] = textwrap.dedent( 

1609 """ 

1610 Return an identical %(klass)s closed on the specified side. 

1611 

1612 Parameters 

1613 ---------- 

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

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

1616 or neither. 

1617 

1618 Returns 

1619 ------- 

1620 %(klass)s 

1621 

1622 %(examples)s\ 

1623 """ 

1624 ) 

1625 

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

1627 """ 

1628 Return an identical IntervalArray closed on the specified side. 

1629 

1630 Parameters 

1631 ---------- 

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

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

1634 or neither. 

1635 

1636 Returns 

1637 ------- 

1638 IntervalArray 

1639 A new IntervalArray with the specified side closures. 

1640 

1641 See Also 

1642 -------- 

1643 IntervalArray.closed : Returns inclusive side of the Interval. 

1644 arrays.IntervalArray.closed : Returns inclusive side of the IntervalArray. 

1645 

1646 Examples 

1647 -------- 

1648 >>> index = pd.arrays.IntervalArray.from_breaks(range(4)) 

1649 >>> index 

1650 <IntervalArray> 

1651 [(0, 1], (1, 2], (2, 3]] 

1652 Length: 3, dtype: interval[int64, right] 

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

1654 <IntervalArray> 

1655 [[0, 1], [1, 2], [2, 3]] 

1656 Length: 3, dtype: interval[int64, both] 

1657 """ 

1658 if closed not in VALID_CLOSED: 

1659 msg = f"invalid option for 'closed': {closed}" 

1660 raise ValueError(msg) 

1661 

1662 left, right = self._left, self._right 

1663 dtype = IntervalDtype(left.dtype, closed=closed) 

1664 return self._simple_new(left, right, dtype=dtype) 

1665 

1666 _interval_shared_docs["is_non_overlapping_monotonic"] = """ 

1667 Return a boolean whether the %(klass)s is non-overlapping and monotonic. 

1668 

1669 Non-overlapping means (no Intervals share points), and monotonic means 

1670 either monotonic increasing or monotonic decreasing. 

1671 

1672 Examples 

1673 -------- 

1674 For arrays: 

1675 

1676 >>> interv_arr = pd.arrays.IntervalArray([pd.Interval(0, 1), pd.Interval(1, 5)]) 

1677 >>> interv_arr 

1678 <IntervalArray> 

1679 [(0, 1], (1, 5]] 

1680 Length: 2, dtype: interval[int64, right] 

1681 >>> interv_arr.is_non_overlapping_monotonic 

1682 True 

1683 

1684 >>> interv_arr = pd.arrays.IntervalArray([pd.Interval(0, 1), 

1685 ... pd.Interval(-1, 0.1)]) 

1686 >>> interv_arr 

1687 <IntervalArray> 

1688 [(0.0, 1.0], (-1.0, 0.1]] 

1689 Length: 2, dtype: interval[float64, right] 

1690 >>> interv_arr.is_non_overlapping_monotonic 

1691 False 

1692 

1693 For Interval Index: 

1694 

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

1696 >>> interv_idx 

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

1698 >>> interv_idx.is_non_overlapping_monotonic 

1699 True 

1700 

1701 >>> interv_idx = pd.interval_range(start=0, end=2, closed='both') 

1702 >>> interv_idx 

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

1704 >>> interv_idx.is_non_overlapping_monotonic 

1705 False 

1706 """ 

1707 

1708 @property 

1709 def is_non_overlapping_monotonic(self) -> bool: 

1710 """ 

1711 Return a boolean whether the IntervalArray/IntervalIndex\ 

1712 is non-overlapping and monotonic. 

1713 

1714 Non-overlapping means (no Intervals share points), and monotonic means 

1715 either monotonic increasing or monotonic decreasing. 

1716 

1717 See Also 

1718 -------- 

1719 overlaps : Check if two IntervalIndex objects overlap. 

1720 

1721 Examples 

1722 -------- 

1723 For arrays: 

1724 

1725 >>> interv_arr = pd.arrays.IntervalArray([pd.Interval(0, 1), pd.Interval(1, 5)]) 

1726 >>> interv_arr 

1727 <IntervalArray> 

1728 [(0, 1], (1, 5]] 

1729 Length: 2, dtype: interval[int64, right] 

1730 >>> interv_arr.is_non_overlapping_monotonic 

1731 True 

1732 

1733 >>> interv_arr = pd.arrays.IntervalArray( 

1734 ... [pd.Interval(0, 1), pd.Interval(-1, 0.1)] 

1735 ... ) 

1736 >>> interv_arr 

1737 <IntervalArray> 

1738 [(0.0, 1.0], (-1.0, 0.1]] 

1739 Length: 2, dtype: interval[float64, right] 

1740 >>> interv_arr.is_non_overlapping_monotonic 

1741 False 

1742 

1743 For Interval Index: 

1744 

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

1746 >>> interv_idx 

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

1748 >>> interv_idx.is_non_overlapping_monotonic 

1749 True 

1750 

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

1752 >>> interv_idx 

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

1754 >>> interv_idx.is_non_overlapping_monotonic 

1755 False 

1756 """ 

1757 # must be increasing (e.g., [0, 1), [1, 2), [2, 3), ... ) 

1758 # or decreasing (e.g., [-1, 0), [-2, -1), [-3, -2), ...) 

1759 # we already require left <= right 

1760 

1761 # strict inequality for closed == 'both'; equality implies overlapping 

1762 # at a point when both sides of intervals are included 

1763 if self.closed == "both": 

1764 return bool( 

1765 (self._right[:-1] < self._left[1:]).all() 

1766 or (self._left[:-1] > self._right[1:]).all() 

1767 ) 

1768 

1769 # non-strict inequality when closed != 'both'; at least one side is 

1770 # not included in the intervals, so equality does not imply overlapping 

1771 return bool( 

1772 (self._right[:-1] <= self._left[1:]).all() 

1773 or (self._left[:-1] >= self._right[1:]).all() 

1774 ) 

1775 

1776 # --------------------------------------------------------------------- 

1777 # Conversion 

1778 

1779 def __array__( 

1780 self, dtype: NpDtype | None = None, copy: bool | None = None 

1781 ) -> np.ndarray: 

1782 """ 

1783 Return the IntervalArray's data as a numpy array of Interval 

1784 objects (with dtype='object') 

1785 """ 

1786 if copy is False: 

1787 raise ValueError( 

1788 "Unable to avoid copy while creating an array as requested." 

1789 ) 

1790 

1791 left = self._left 

1792 right = self._right 

1793 mask = self.isna() 

1794 closed = self.closed 

1795 

1796 result = np.empty(len(left), dtype=object) 

1797 for i, left_value in enumerate(left): 

1798 if mask[i]: 

1799 result[i] = np.nan 

1800 else: 

1801 result[i] = Interval(left_value, right[i], closed) 

1802 return result 

1803 

1804 def __arrow_array__(self, type=None): 

1805 """ 

1806 Convert myself into a pyarrow Array. 

1807 """ 

1808 import pyarrow 

1809 

1810 from pandas.core.arrays.arrow.extension_types import ArrowIntervalType 

1811 

1812 try: 

1813 subtype = pyarrow.from_numpy_dtype(self.dtype.subtype) 

1814 except TypeError as err: 

1815 raise TypeError( 

1816 f"Conversion to arrow with subtype '{self.dtype.subtype}' " 

1817 "is not supported" 

1818 ) from err 

1819 interval_type = ArrowIntervalType(subtype, self.closed) 

1820 storage_array = pyarrow.StructArray.from_arrays( 

1821 [ 

1822 pyarrow.array(self._left, type=subtype, from_pandas=True), 

1823 pyarrow.array(self._right, type=subtype, from_pandas=True), 

1824 ], 

1825 names=["left", "right"], 

1826 ) 

1827 mask = self.isna() 

1828 if mask.any(): 

1829 # if there are missing values, set validity bitmap also on the array level 

1830 null_bitmap = pyarrow.array(~mask).buffers()[1] 

1831 storage_array = pyarrow.StructArray.from_buffers( 

1832 storage_array.type, 

1833 len(storage_array), 

1834 [null_bitmap], 

1835 children=[storage_array.field(0), storage_array.field(1)], 

1836 ) 

1837 

1838 if type is not None: 

1839 if type.equals(interval_type.storage_type): 

1840 return storage_array 

1841 elif isinstance(type, ArrowIntervalType): 

1842 # ensure we have the same subtype and closed attributes 

1843 if not type.equals(interval_type): 

1844 raise TypeError( 

1845 "Not supported to convert IntervalArray to type with " 

1846 f"different 'subtype' ({self.dtype.subtype} vs {type.subtype}) " 

1847 f"and 'closed' ({self.closed} vs {type.closed}) attributes" 

1848 ) 

1849 else: 

1850 raise TypeError( 

1851 f"Not supported to convert IntervalArray to '{type}' type" 

1852 ) 

1853 

1854 return pyarrow.ExtensionArray.from_storage(interval_type, storage_array) 

1855 

1856 _interval_shared_docs["to_tuples"] = textwrap.dedent( 

1857 """ 

1858 Return an %(return_type)s of tuples of the form (left, right). 

1859 

1860 Parameters 

1861 ---------- 

1862 na_tuple : bool, default True 

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

1864 just return ``NA`` as ``nan``. 

1865 

1866 Returns 

1867 ------- 

1868 tuples: %(return_type)s 

1869 %(examples)s\ 

1870 """ 

1871 ) 

1872 

1873 def to_tuples(self, na_tuple: bool = True) -> np.ndarray: 

1874 """ 

1875 Return an ndarray (if self is IntervalArray) or Index \ 

1876 (if self is IntervalIndex) of tuples of the form (left, right). 

1877 

1878 Parameters 

1879 ---------- 

1880 na_tuple : bool, default True 

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

1882 just return ``NA`` as ``nan``. 

1883 

1884 Returns 

1885 ------- 

1886 ndarray or Index 

1887 An ndarray of tuples representing the intervals 

1888 if `self` is an IntervalArray. 

1889 An Index of tuples representing the intervals 

1890 if `self` is an IntervalIndex. 

1891 

1892 See Also 

1893 -------- 

1894 IntervalArray.to_list : Convert IntervalArray to a list of tuples. 

1895 IntervalArray.to_numpy : Convert IntervalArray to a numpy array. 

1896 IntervalArray.unique : Find unique intervals in an IntervalArray. 

1897 

1898 Examples 

1899 -------- 

1900 For :class:`pandas.IntervalArray`: 

1901 

1902 >>> idx = pd.arrays.IntervalArray.from_tuples([(0, 1), (1, 2)]) 

1903 >>> idx 

1904 <IntervalArray> 

1905 [(0, 1], (1, 2]] 

1906 Length: 2, dtype: interval[int64, right] 

1907 >>> idx.to_tuples() 

1908 array([(np.int64(0), np.int64(1)), (np.int64(1), np.int64(2))], 

1909 dtype=object) 

1910 

1911 For :class:`pandas.IntervalIndex`: 

1912 

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

1914 >>> idx 

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

1916 >>> idx.to_tuples() 

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

1918 """ 

1919 tuples = com.asarray_tuplesafe(zip(self._left, self._right, strict=True)) 

1920 if not na_tuple: 

1921 # GH 18756 

1922 tuples = np.where(~self.isna(), tuples, np.nan) 

1923 return tuples 

1924 

1925 # --------------------------------------------------------------------- 

1926 

1927 def _putmask(self, mask: npt.NDArray[np.bool_], value) -> None: 

1928 value_left, value_right = self._validate_setitem_value(value) 

1929 

1930 if isinstance(self._left, np.ndarray): 

1931 np.putmask(self._left, mask, value_left) 

1932 assert isinstance(self._right, np.ndarray) 

1933 np.putmask(self._right, mask, value_right) 

1934 else: 

1935 self._left._putmask(mask, value_left) 

1936 assert not isinstance(self._right, np.ndarray) 

1937 self._right._putmask(mask, value_right) 

1938 

1939 def insert(self, loc: int, item: Interval) -> Self: 

1940 """ 

1941 Return a new IntervalArray inserting new item at location. Follows 

1942 Python numpy.insert semantics for negative values. Only Interval 

1943 objects and NA can be inserted into an IntervalIndex 

1944 

1945 Parameters 

1946 ---------- 

1947 loc : int 

1948 item : Interval 

1949 

1950 Returns 

1951 ------- 

1952 IntervalArray 

1953 """ 

1954 left_insert, right_insert = self._validate_scalar(item) 

1955 

1956 new_left = self.left.insert(loc, left_insert) 

1957 new_right = self.right.insert(loc, right_insert) 

1958 

1959 return self._shallow_copy(new_left, new_right) 

1960 

1961 def delete(self, loc) -> Self: 

1962 new_left: np.ndarray | DatetimeArray | TimedeltaArray 

1963 new_right: np.ndarray | DatetimeArray | TimedeltaArray 

1964 if isinstance(self._left, np.ndarray): 

1965 new_left = np.delete(self._left, loc) 

1966 assert isinstance(self._right, np.ndarray) 

1967 new_right = np.delete(self._right, loc) 

1968 else: 

1969 new_left = self._left.delete(loc) 

1970 assert not isinstance(self._right, np.ndarray) 

1971 new_right = self._right.delete(loc) 

1972 return self._shallow_copy(left=new_left, right=new_right) 

1973 

1974 def repeat( 

1975 self, 

1976 repeats: int | Sequence[int], 

1977 axis: AxisInt | None = None, 

1978 ) -> Self: 

1979 """ 

1980 Repeat elements of an IntervalArray. 

1981 

1982 Returns a new IntervalArray where each element of the current IntervalArray 

1983 is repeated consecutively a given number of times. 

1984 

1985 Parameters 

1986 ---------- 

1987 repeats : int or array of ints 

1988 The number of repetitions for each element. This should be a 

1989 non-negative integer. Repeating 0 times will return an empty 

1990 IntervalArray. 

1991 axis : None 

1992 Must be ``None``. Has no effect but is accepted for compatibility 

1993 with numpy. 

1994 

1995 Returns 

1996 ------- 

1997 IntervalArray 

1998 Newly created IntervalArray with repeated elements. 

1999 

2000 See Also 

2001 -------- 

2002 Series.repeat : Equivalent function for Series. 

2003 Index.repeat : Equivalent function for Index. 

2004 numpy.repeat : Similar method for :class:`numpy.ndarray`. 

2005 ExtensionArray.take : Take arbitrary positions. 

2006 

2007 Examples 

2008 -------- 

2009 >>> cat = pd.Categorical(["a", "b", "c"]) 

2010 >>> cat 

2011 ['a', 'b', 'c'] 

2012 Categories (3, str): ['a', 'b', 'c'] 

2013 >>> cat.repeat(2) 

2014 ['a', 'a', 'b', 'b', 'c', 'c'] 

2015 Categories (3, str): ['a', 'b', 'c'] 

2016 >>> cat.repeat([1, 2, 3]) 

2017 ['a', 'b', 'b', 'c', 'c', 'c'] 

2018 Categories (3, str): ['a', 'b', 'c'] 

2019 """ 

2020 nv.validate_repeat((), {"axis": axis}) 

2021 left_repeat = self.left.repeat(repeats) 

2022 right_repeat = self.right.repeat(repeats) 

2023 return self._shallow_copy(left=left_repeat, right=right_repeat) 

2024 

2025 _interval_shared_docs["contains"] = textwrap.dedent( 

2026 """ 

2027 Check elementwise if the Intervals contain the value. 

2028 

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

2030 of the %(klass)s. 

2031 

2032 Parameters 

2033 ---------- 

2034 other : scalar 

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

2036 

2037 Returns 

2038 ------- 

2039 boolean array 

2040 

2041 See Also 

2042 -------- 

2043 Interval.contains : Check whether Interval object contains value. 

2044 %(klass)s.overlaps : Check if an Interval overlaps the values in the 

2045 %(klass)s. 

2046 

2047 Examples 

2048 -------- 

2049 %(examples)s 

2050 >>> intervals.contains(0.5) 

2051 array([ True, False, False]) 

2052 """ 

2053 ) 

2054 

2055 def contains(self, other): 

2056 """ 

2057 Check elementwise if the Intervals contain the value. 

2058 

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

2060 of the IntervalArray. 

2061 

2062 Parameters 

2063 ---------- 

2064 other : scalar 

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

2066 

2067 Returns 

2068 ------- 

2069 boolean array 

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

2071 

2072 See Also 

2073 -------- 

2074 Interval.contains : Check whether Interval object contains value. 

2075 IntervalArray.overlaps : Check if an Interval overlaps the values in the 

2076 IntervalArray. 

2077 

2078 Examples 

2079 -------- 

2080 >>> intervals = pd.arrays.IntervalArray.from_tuples([(0, 1), (1, 3), (2, 4)]) 

2081 >>> intervals 

2082 <IntervalArray> 

2083 [(0, 1], (1, 3], (2, 4]] 

2084 Length: 3, dtype: interval[int64, right] 

2085 

2086 >>> intervals.contains(0.5) 

2087 array([ True, False, False]) 

2088 """ 

2089 if isinstance(other, Interval): 

2090 raise NotImplementedError("contains not implemented for two intervals") 

2091 

2092 return (self._left < other if self.open_left else self._left <= other) & ( 

2093 other < self._right if self.open_right else other <= self._right 

2094 ) 

2095 

2096 def isin(self, values: ArrayLike) -> npt.NDArray[np.bool_]: 

2097 if isinstance(values, IntervalArray): 

2098 if self.closed != values.closed: 

2099 # not comparable -> no overlap 

2100 return np.zeros(self.shape, dtype=bool) 

2101 

2102 if self.dtype == values.dtype: 

2103 left = self._combined 

2104 right = values._combined 

2105 return np.isin(left, right).ravel() 

2106 

2107 elif needs_i8_conversion(self.left.dtype) ^ needs_i8_conversion( 

2108 values.left.dtype 

2109 ): 

2110 # not comparable -> no overlap 

2111 return np.zeros(self.shape, dtype=bool) 

2112 

2113 return isin(self.astype(object), values.astype(object)) 

2114 

2115 @property 

2116 def _combined(self) -> IntervalSide: 

2117 # error: Item "ExtensionArray" of "ExtensionArray | ndarray[Any, Any]" 

2118 # has no attribute "reshape" [union-attr] 

2119 left = self.left._values.reshape(-1, 1) # type: ignore[union-attr] 

2120 right = self.right._values.reshape(-1, 1) # type: ignore[union-attr] 

2121 # GH#38353 instead of casting to object, operating on a 

2122 # complex128 ndarray is much more performant. 

2123 if needs_i8_conversion(left.dtype): 

2124 # error: Item "ndarray[Any, Any]" of "Any | ndarray[Any, Any]" has 

2125 # no attribute "_concat_same_type" 

2126 comb = left._concat_same_type( # type: ignore[union-attr] 

2127 [left, right], axis=1 

2128 ) 

2129 comb = comb.view("complex128")[:, 0] 

2130 else: 

2131 comb = np.asarray(left.ravel(), dtype="complex128") 

2132 comb.imag = right.ravel() 

2133 return comb 

2134 

2135 def _from_combined(self, combined: np.ndarray) -> IntervalArray: 

2136 """ 

2137 Create a new IntervalArray with our dtype from a 1D complex128 ndarray. 

2138 """ 

2139 

2140 dtype = self._left.dtype 

2141 if needs_i8_conversion(dtype): 

2142 nc = combined.view("i8").reshape(-1, 2) 

2143 assert isinstance(self._left, (DatetimeArray, TimedeltaArray)) 

2144 new_left: DatetimeArray | TimedeltaArray | np.ndarray = type( 

2145 self._left 

2146 )._from_sequence(nc[:, 0], dtype=dtype) 

2147 assert isinstance(self._right, (DatetimeArray, TimedeltaArray)) 

2148 new_right: DatetimeArray | TimedeltaArray | np.ndarray = type( 

2149 self._right 

2150 )._from_sequence(nc[:, 1], dtype=dtype) 

2151 else: 

2152 assert isinstance(dtype, np.dtype) 

2153 new_left = np.real(combined).astype(dtype).ravel() 

2154 new_right = np.imag(combined).astype(dtype).ravel() 

2155 return self._shallow_copy(left=new_left, right=new_right) 

2156 

2157 def unique(self) -> IntervalArray: 

2158 nc = unique(self._combined) 

2159 return self._from_combined(np.asarray(nc)[:, None]) 

2160 

2161 

2162def _maybe_convert_platform_interval(values) -> ArrayLike: 

2163 """ 

2164 Try to do platform conversion, with special casing for IntervalArray. 

2165 Wrapper around maybe_convert_platform that alters the default return 

2166 dtype in certain cases to be compatible with IntervalArray. For example, 

2167 empty lists return with integer dtype instead of object dtype, which is 

2168 prohibited for IntervalArray. 

2169 

2170 Parameters 

2171 ---------- 

2172 values : array-like 

2173 

2174 Returns 

2175 ------- 

2176 array 

2177 """ 

2178 if isinstance(values, (list, tuple)) and len(values) == 0: 

2179 # GH 19016 

2180 # empty lists/tuples get object dtype by default, but this is 

2181 # prohibited for IntervalArray, so coerce to integer instead 

2182 return np.array([], dtype=np.int64) 

2183 elif not is_list_like(values) or isinstance(values, ABCDataFrame): 

2184 # This will raise later, but we avoid passing to maybe_convert_platform 

2185 return values 

2186 elif isinstance(getattr(values, "dtype", None), CategoricalDtype): 

2187 values = np.asarray(values) 

2188 elif not hasattr(values, "dtype") and not isinstance(values, (list, tuple, range)): 

2189 # TODO: should we just cast these to list? 

2190 return values 

2191 else: 

2192 values = extract_array(values, extract_numpy=True) 

2193 

2194 if not hasattr(values, "dtype"): 

2195 values = np.asarray(values) 

2196 if values.dtype.kind in "iu" and values.dtype != np.int64: 

2197 values = values.astype(np.int64) 

2198 return values